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": {
"test": "node_modules/.bin/mocha -r esm",
"watch": "webpack --watch",
"watch": "webpack --watch --mode=development",
"start": "webpack-dev-server --open --hot",
"build": "webpack --progress --mode=production",
"docker": "webpack --no-color --mode=production"

View File

@ -152,7 +152,7 @@ function showSearchResultsExtended(element, template, searchTool, query, input,
}
return results
})
}).catch(e => console.log('searchtool', e))
}
function formatLineResult(hits, key) {
@ -284,7 +284,7 @@ function Editor(holder, input) {
return td
})
])
})
}).catch(e => console.log('while fetching metakv', e))
})
})
.then(mflatten)
@ -313,8 +313,9 @@ function Editor(holder, input) {
div.classList.add('table-wrapper')
return element.html(div);
})
}).catch(e => console.log('while creating table', e))
})
.catch(e => console.log('transformTable', e))
}
async function transformMathExpression(converted, scope) {
@ -350,7 +351,6 @@ function Editor(holder, input) {
}
let converted = text
let todo;
if (converted === '{{table}}') {
transformTable.call(this, editor, id, element);
@ -368,30 +368,37 @@ function Editor(holder, input) {
})
.catch(e => console.warn(e))
} else {
let re = /^([A-Z0-9 ]+)::\s*(.*)$/i;
let res = text.match(re)
if (res) {
converted = '<span class="metadata-key">[[' + res[1] + ']]</span>'
if (res[2]) {
converted += ': ' + res[2]
}
} else if (text.match(/#\[\[TODO]]/)) {
converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />')
todo = true;
} else if (text.match(/#\[\[DONE]]/)) {
converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />')
todo = false;
}
// let re = /^([A-Z0-9 ]+)::\s*(.*)$/i;
// let res = text.match(re)
// if (res) {
// converted = '<span class="metadata-key">[[' + res[1] + ']]</span>'
// if (res[2]) {
// converted += ': ' + res[2]
// }
// } else if (text.match(/#\[\[TODO]]/)) {
// converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />')
// todo = true;
// } else if (text.match(/#\[\[DONE]]/)) {
// converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />')
// 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'])
try {
const todoItem = $(converted).find(':checkbox')
if (todoItem.length) {
const todo = !todoItem.is(':checked')
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)
@ -436,7 +443,9 @@ function Editor(holder, input) {
addIndicator(
addSaver(editor, saveUrl, page, beforeSave),
indicator
).save().then(() => indicator.done())
).save()
.then(() => indicator.done())
.catch(e => console.log('editor.change', e))
})
// editor.on('rendered', function () {
@ -612,11 +621,12 @@ function Editor(holder, input) {
if (insideSearch) {
let query = value.substring(start + 1, end);
showSearchResults(commandSearch, query, input, value, 'command').then(results => {
if (query.length > 0 && (!results || results.length === 0)) {
searchEnabled = false
}
})
showSearchResults(commandSearch, query, input, value, 'command')
.then(results => {
if (query.length > 0 && (!results || results.length === 0)) {
searchEnabled = false
}
}).catch(e => console.log('showSearchResults', e))
return true
} else if (insideLink) {
let query = value.substring(start, end);
@ -645,15 +655,17 @@ function Editor(holder, input) {
return search.startQuery(res[2])
.then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text')))
.then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render())
} else {
return search.startQuery(res[2])
.then(hits => _.groupBy(hits, (it) => it.title))
.then(hits => _.flatMap(hits, formatLineResult))
.then(results => editor.replaceChildren(id, results))
.catch(e => console.log('search query', e))
.finally(() => editor.render())
}
})
}).catch(e => {})
});
return editor
})
@ -663,7 +675,7 @@ function match(s, re) {
return new Promise((resolve, reject) => {
let res = s.match(re)
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 MarkdownItWikilinks from "./wikilinks";
//import MarkdownItWikilinks from "./wikilinks";
import MarkdownItWikilinks2 from "./wikilinks2";
import MarkdownItMark from "markdown-it-mark";
import MarkdownItKatex from "markdown-it-katex";
const MD = new MarkdownIt({
linkify: true,
linkify: false,
highlight: function (str, lang) {
if (lang === 'mermaid') {
return '<div class="mermaid">' + str + '</div>';
@ -12,14 +13,22 @@ const MD = new MarkdownIt({
return '';
}
})
MD.use(MarkdownItWikilinks({
MD.use(MarkdownItWikilinks2({
baseURL: document.querySelector('body').dataset.baseUrl,
uriSuffix: '',
relativeBaseURL: '/edit/',
htmlAttributes: {
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;

View File

@ -2,7 +2,26 @@
import Plugin from "markdown-it-regexp";
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) => {
const defaults = {
@ -17,7 +36,7 @@ export default (options) => {
},
postProcessPageName: (pageName) => {
pageName = pageName.trim()
pageName = pageName.split('/').map(sanitize).join('/')
pageName = pageName.split('/').map(sanitize).map(sanitize).join('/')
pageName = pageName.replace(/\s+/g, '_')
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}
}]
})
})
})