From 70f1b626460f9effd6dd2cfc9b8a5a14cafcc2e6 Mon Sep 17 00:00:00 2001 From: Peter Stuifzand Date: Fri, 29 Oct 2021 21:40:49 +0200 Subject: [PATCH] Improve links and checkboxes Use the markdown parser to handle links and checkboxes instead of internal javascript. Also add catch clauses to the promises in the editor. --- editor/package.json | 2 +- editor/src/editor.js | 74 ++++++++++++--------- editor/src/markdown.js | 19 ++++-- editor/src/wikilinks.js | 23 ++++++- editor/src/wikilinks2.js | 75 +++++++++++++++++++++ editor/test/markdown.test.js | 62 +++++++++++++++++ editor/test/wikilinks2.test.js | 117 +++++++++++++++++++++++++++++++++ 7 files changed, 333 insertions(+), 39 deletions(-) create mode 100644 editor/src/wikilinks2.js create mode 100644 editor/test/markdown.test.js create mode 100644 editor/test/wikilinks2.test.js diff --git a/editor/package.json b/editor/package.json index f7c5b63..e0f03be 100644 --- a/editor/package.json +++ b/editor/package.json @@ -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" diff --git a/editor/src/editor.js b/editor/src/editor.js index 1e9a5d8..fe9917c 100644 --- a/editor/src/editor.js +++ b/editor/src/editor.js @@ -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 = '[[' + res[1] + ']]' - if (res[2]) { - converted += ': ' + res[2] - } - } else if (text.match(/#\[\[TODO]]/)) { - converted = converted.replace('#[[TODO]]', '') - todo = true; - } else if (text.match(/#\[\[DONE]]/)) { - converted = converted.replace('#[[DONE]]', '') - todo = false; - } + // let re = /^([A-Z0-9 ]+)::\s*(.*)$/i; + // let res = text.match(re) + // if (res) { + // converted = '[[' + res[1] + ']]' + // if (res[2]) { + // converted += ': ' + 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']) + 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) }); } diff --git a/editor/src/markdown.js b/editor/src/markdown.js index 0da4d81..81f3658 100644 --- a/editor/src/markdown.js +++ b/editor/src/markdown.js @@ -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 '
' + str + '
'; @@ -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; diff --git a/editor/src/wikilinks.js b/editor/src/wikilinks.js index 17fd6f7..2836237 100644 --- a/editor/src/wikilinks.js +++ b/editor/src/wikilinks.js @@ -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 }, diff --git a/editor/src/wikilinks2.js b/editor/src/wikilinks2.js new file mode 100644 index 0000000..ab8b35a --- /dev/null +++ b/editor/src/wikilinks2.js @@ -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 ''; + } else if (link === 'DONE') { + return ''; + } + } + return '' + link + ''; +} + +export default (options) => { + return Plugin(options); +} diff --git a/editor/test/markdown.test.js b/editor/test/markdown.test.js new file mode 100644 index 0000000..25ff763 --- /dev/null +++ b/editor/test/markdown.test.js @@ -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 . + */ + +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 '
' + str + '
'; + } + 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]]"), '') + assert.deepStrictEqual(MD.renderInline("#[[TODO]] #[[DONE]]"), ' ') + }) + it('parseLinks 2', function () { + assert.deepStrictEqual(MD.renderInline("#[[TODO]] #[[DONE]]"), ' ') + }) + it('parseLinks 3', function () { + assert.deepStrictEqual(MD.renderInline("test #[[TODO]] test2"), 'test test2') + }) + it('parseLinks 4', function () { + assert.deepStrictEqual(MD.renderInline("test [[test]] [[test2]] [[test3]]"), 'test test test2 test3') + }) + it('parseLinks 5', function () { + assert.deepStrictEqual(MD.renderInline("test [[test]]"), 'test test') + }) + it('parseLinks 6', function () { + assert.deepStrictEqual(MD.renderInline("test [[test]] [[test2]]"), 'test test test2') + }) +}) diff --git a/editor/test/wikilinks2.test.js b/editor/test/wikilinks2.test.js new file mode 100644 index 0000000..b1e43c9 --- /dev/null +++ b/editor/test/wikilinks2.test.js @@ -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 . + */ + +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} + }] + }) + }) +})