Improve links and checkboxes
All checks were successful
continuous-integration/drone/push Build is passing

Use the markdown parser to handle links and checkboxes instead of
internal javascript.

Also add catch clauses to the promises in the editor.
This commit is contained in:
Peter Stuifzand 2021-10-29 21:40:49 +02:00
parent a2c83d87b6
commit 70f1b62646
7 changed files with 333 additions and 39 deletions

View File

@ -59,7 +59,7 @@
}, },
"scripts": { "scripts": {
"test": "node_modules/.bin/mocha -r esm", "test": "node_modules/.bin/mocha -r esm",
"watch": "webpack --watch", "watch": "webpack --watch --mode=development",
"start": "webpack-dev-server --open --hot", "start": "webpack-dev-server --open --hot",
"build": "webpack --progress --mode=production", "build": "webpack --progress --mode=production",
"docker": "webpack --no-color --mode=production" "docker": "webpack --no-color --mode=production"

View File

@ -152,7 +152,7 @@ function showSearchResultsExtended(element, template, searchTool, query, input,
} }
return results return results
}) }).catch(e => console.log('searchtool', e))
} }
function formatLineResult(hits, key) { function formatLineResult(hits, key) {
@ -284,7 +284,7 @@ function Editor(holder, input) {
return td return td
}) })
]) ])
}) }).catch(e => console.log('while fetching metakv', e))
}) })
}) })
.then(mflatten) .then(mflatten)
@ -313,8 +313,9 @@ function Editor(holder, input) {
div.classList.add('table-wrapper') div.classList.add('table-wrapper')
return element.html(div); return element.html(div);
}) }).catch(e => console.log('while creating table', e))
}) })
.catch(e => console.log('transformTable', e))
} }
async function transformMathExpression(converted, scope) { async function transformMathExpression(converted, scope) {
@ -350,7 +351,6 @@ function Editor(holder, input) {
} }
let converted = text let converted = text
let todo;
if (converted === '{{table}}') { if (converted === '{{table}}') {
transformTable.call(this, editor, id, element); transformTable.call(this, editor, id, element);
@ -368,30 +368,37 @@ function Editor(holder, input) {
}) })
.catch(e => console.warn(e)) .catch(e => console.warn(e))
} else { } else {
let re = /^([A-Z0-9 ]+)::\s*(.*)$/i; // let re = /^([A-Z0-9 ]+)::\s*(.*)$/i;
let res = text.match(re) // let res = text.match(re)
if (res) { // if (res) {
converted = '<span class="metadata-key">[[' + res[1] + ']]</span>' // converted = '<span class="metadata-key">[[' + res[1] + ']]</span>'
if (res[2]) { // if (res[2]) {
converted += ': ' + res[2] // converted += ': ' + res[2]
} // }
} else if (text.match(/#\[\[TODO]]/)) { // } else if (text.match(/#\[\[TODO]]/)) {
converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />') // converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />')
todo = true; // todo = true;
} else if (text.match(/#\[\[DONE]]/)) { // } else if (text.match(/#\[\[DONE]]/)) {
converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />') // converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />')
todo = false; // todo = false;
} // }
MD.options.html = true MD.options.html = true
converted = MD.renderInline(converted) converted = MD.renderInline(converted)
MD.options.html = false MD.options.html = false
} }
if (todo !== undefined) { try {
element.toggleClass('todo--done', todo === false) const todoItem = $(converted).find(':checkbox')
element.toggleClass('todo--todo', todo === true) if (todoItem.length) {
} else { const todo = !todoItem.is(':checked')
element.removeClass(['todo--todo', 'todo--done']) element.toggleClass('todo--done', todo === false)
element.toggleClass('todo--todo', todo === true)
} else {
element.removeClass(['todo--todo', 'todo--done'])
}
} catch (e) {
// problem with $(converted) is that it could be treated as a jQuery selector expression instead of normal text
// the wiki text is not quite like selectors
} }
element.html(converted) element.html(converted)
@ -436,7 +443,9 @@ function Editor(holder, input) {
addIndicator( addIndicator(
addSaver(editor, saveUrl, page, beforeSave), addSaver(editor, saveUrl, page, beforeSave),
indicator indicator
).save().then(() => indicator.done()) ).save()
.then(() => indicator.done())
.catch(e => console.log('editor.change', e))
}) })
// editor.on('rendered', function () { // editor.on('rendered', function () {
@ -612,11 +621,12 @@ function Editor(holder, input) {
if (insideSearch) { if (insideSearch) {
let query = value.substring(start + 1, end); let query = value.substring(start + 1, end);
showSearchResults(commandSearch, query, input, value, 'command').then(results => { showSearchResults(commandSearch, query, input, value, 'command')
if (query.length > 0 && (!results || results.length === 0)) { .then(results => {
searchEnabled = false if (query.length > 0 && (!results || results.length === 0)) {
} searchEnabled = false
}) }
}).catch(e => console.log('showSearchResults', e))
return true return true
} else if (insideLink) { } else if (insideLink) {
let query = value.substring(start, end); let query = value.substring(start, end);
@ -645,15 +655,17 @@ function Editor(holder, input) {
return search.startQuery(res[2]) return search.startQuery(res[2])
.then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text'))) .then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text')))
.then(results => editor.replaceChildren(id, results)) .then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render()) .finally(() => editor.render())
} else { } else {
return search.startQuery(res[2]) return search.startQuery(res[2])
.then(hits => _.groupBy(hits, (it) => it.title)) .then(hits => _.groupBy(hits, (it) => it.title))
.then(hits => _.flatMap(hits, formatLineResult)) .then(hits => _.flatMap(hits, formatLineResult))
.then(results => editor.replaceChildren(id, results)) .then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render()) .finally(() => editor.render())
} }
}) }).catch(e => {})
}); });
return editor return editor
}) })
@ -663,7 +675,7 @@ function match(s, re) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let res = s.match(re) let res = s.match(re)
if (res) resolve(res) if (res) resolve(res)
else reject() else reject(s + ' did not match ' + re)
}); });
} }

View File

@ -1,10 +1,11 @@
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
import MarkdownItWikilinks from "./wikilinks"; //import MarkdownItWikilinks from "./wikilinks";
import MarkdownItWikilinks2 from "./wikilinks2";
import MarkdownItMark from "markdown-it-mark"; import MarkdownItMark from "markdown-it-mark";
import MarkdownItKatex from "markdown-it-katex"; import MarkdownItKatex from "markdown-it-katex";
const MD = new MarkdownIt({ const MD = new MarkdownIt({
linkify: true, linkify: false,
highlight: function (str, lang) { highlight: function (str, lang) {
if (lang === 'mermaid') { if (lang === 'mermaid') {
return '<div class="mermaid">' + str + '</div>'; return '<div class="mermaid">' + str + '</div>';
@ -12,14 +13,22 @@ const MD = new MarkdownIt({
return ''; return '';
} }
}) })
MD.use(MarkdownItWikilinks2({
MD.use(MarkdownItWikilinks({
baseURL: document.querySelector('body').dataset.baseUrl, baseURL: document.querySelector('body').dataset.baseUrl,
uriSuffix: '', uriSuffix: '',
relativeBaseURL: '/edit/', relativeBaseURL: '/edit/',
htmlAttributes: { htmlAttributes: {
class: 'wiki-link' class: 'wiki-link'
}, },
})).use(MarkdownItMark).use(MarkdownItKatex) }))
// MD.use(MarkdownItWikilinks({
// baseURL: document.querySelector('body').dataset.baseUrl,
// uriSuffix: '',
// relativeBaseURL: '/edit/',
// htmlAttributes: {
// class: 'wiki-link'
// },
// }))
// MD.use(MarkdownItMark).use(MarkdownItKatex)
export default MD; export default MD;

View File

@ -2,7 +2,26 @@
import Plugin from "markdown-it-regexp"; import Plugin from "markdown-it-regexp";
import extend from "extend"; import extend from "extend";
import sanitize from "sanitize-filename";
var illegalRe = /[\/\?<>\\\*\|"]/g;
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
var reservedRe = /^\.+$/;
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
var windowsTrailingRe = /[\. ]+$/;
function sanitize(input) {
const replacement = '';
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
var sanitized = input
.replace(illegalRe, replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement);
return sanitized;
}
export default (options) => { export default (options) => {
const defaults = { const defaults = {
@ -17,7 +36,7 @@ export default (options) => {
}, },
postProcessPageName: (pageName) => { postProcessPageName: (pageName) => {
pageName = pageName.trim() pageName = pageName.trim()
pageName = pageName.split('/').map(sanitize).join('/') pageName = pageName.split('/').map(sanitize).map(sanitize).join('/')
pageName = pageName.replace(/\s+/g, '_') pageName = pageName.replace(/\s+/g, '_')
return pageName return pageName
}, },

75
editor/src/wikilinks2.js Normal file
View File

@ -0,0 +1,75 @@
'use strict'
var util = require('util')
function Plugin(options) {
var self = function (md) {
self.options = options
self.init(md)
}
self.__proto__ = Plugin.prototype
self.id = 'wikilinks'
return self
}
util.inherits(Plugin, Function)
Plugin.prototype.init = function (md) {
md.inline.ruler.push(this.id, this.parse.bind(this))
md.renderer.rules[this.id] = this.render.bind(this)
}
export function linkParser(id, state) {
let input = state.src.slice(state.pos);
const match = /^#?\[\[/.exec(input)
if (!match) {
return false
}
let prefixLength = match[0].length
let p = state.pos + prefixLength
let open = 2
while (p < state.src.length - 1 && open > 0) {
if (state.src[p] === '[' && state.src[p + 1] === '[') {
open += 2
p += 2
} else if (state.src[p] === ']' && state.src[p + 1] === ']') {
open -= 2
p += 2
} else {
p++
}
}
if (open === 0) {
let link = state.src.slice(state.pos + prefixLength, p - 2)
let token = state.push(id, '', 0)
token.meta = {match: link, tag: prefixLength === 3}
state.pos = p
return true
}
return false
}
Plugin.prototype.parse = function (state, silent) {
return linkParser(this.id, state)
}
Plugin.prototype.render = function (tokens, id, options, env) {
let {match: link, tag} = tokens[id].meta
if (tag) {
if (link === 'TODO') {
return '<input type="checkbox" class="checkbox">';
} else if (link === 'DONE') {
return '<input type="checkbox" class="checkbox" checked>';
}
}
return '<a href="/' + link.replace(' ', '_') + '" class="wiki-link">' + link + '</a>';
}
export default (options) => {
return Plugin(options);
}

View File

@ -0,0 +1,62 @@
/*
* Wiki - A wiki with editor
* Copyright (c) 2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import assert from 'assert'
import MarkdownIt from "markdown-it";
import MarkdownItWikilinks2 from "../src/wikilinks2";
const MD = new MarkdownIt({
linkify: false,
highlight: function (str, lang) {
if (lang === 'mermaid') {
return '<div class="mermaid">' + str + '</div>';
}
return '';
}
})
MD.use(MarkdownItWikilinks2({
baseURL: 'http://localhost/',
uriSuffix: '',
relativeBaseURL: '/edit/',
htmlAttributes: {
class: 'wiki-link'
},
}))
describe('MD', function () {
it('parseLinks', function () {
assert.deepStrictEqual(MD.renderInline("#[[TODO]]"), '<input type="checkbox" class="checkbox">')
assert.deepStrictEqual(MD.renderInline("#[[TODO]] #[[DONE]]"), '<input type="checkbox" class="checkbox"> <input type="checkbox" class="checkbox" checked>')
})
it('parseLinks 2', function () {
assert.deepStrictEqual(MD.renderInline("#[[TODO]] #[[DONE]]"), '<input type="checkbox" class="checkbox"> <input type="checkbox" class="checkbox" checked>')
})
it('parseLinks 3', function () {
assert.deepStrictEqual(MD.renderInline("test #[[TODO]] test2"), 'test <input type="checkbox" class="checkbox"> test2')
})
it('parseLinks 4', function () {
assert.deepStrictEqual(MD.renderInline("test [[test]] [[test2]] [[test3]]"), 'test <a href="/test" class="wiki-link">test</a> <a href="/test2" class="wiki-link">test2</a> <a href="/test3" class="wiki-link">test3</a>')
})
it('parseLinks 5', function () {
assert.deepStrictEqual(MD.renderInline("test [[test]]"), 'test <a href="/test" class="wiki-link">test</a>')
})
it('parseLinks 6', function () {
assert.deepStrictEqual(MD.renderInline("test [[test]] [[test2]]"), 'test <a href="/test" class="wiki-link">test</a> <a href="/test2" class="wiki-link">test2</a>')
})
})

View File

@ -0,0 +1,117 @@
/*
* Wiki - A wiki with editor
* Copyright (c) 2021 Peter Stuifzand
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import assert from 'assert'
import {linkParser} from '../src/wikilinks2'
describe('linkParser', function () {
it('parse', function () {
let state = {src: '', pos: 0, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), false);
assert.deepStrictEqual(state, {
src: '',
pos: 0,
tokens: []
})
})
it('parse 2', function () {
let state = {src: '[[Link]]', pos: 0, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '[[Link]]',
pos: 8,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'Link', tag: false}
}]
})
})
it('parse 3', function () {
let state = {src: '1234[[Link]]', pos: 4, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234[[Link]]',
pos: 12,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'Link', tag: false}
}]
})
})
it('parse 4', function () {
let state = {src: '1234#[[TODO]]', pos: 4, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234#[[TODO]]',
pos: 13,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'TODO', tag: true}
}]
})
})
it('parse text and two links', function () {
let state = {src: '1234 [[Link]] [[Link2]]', pos: 5, tokens: []};
state.__proto__.push = function (id, s, x) {
let token = {id, s, x};
this.tokens.push(token)
return token
}
assert.deepStrictEqual(linkParser('test', state), true);
assert.deepStrictEqual(state, {
src: '1234 [[Link]] [[Link2]]',
pos: 13,
tokens: [{
id: 'test',
s: '',
x: 0,
meta: {match:'Link', tag: false}
}]
})
})
})