|
@@ -9,6 +9,9 @@ const striptags = require('striptags')
|
|
|
const emojiRegex = require('emoji-regex')
|
|
|
const he = require('he')
|
|
|
const CleanCSS = require('clean-css')
|
|
|
+const TurndownService = require('turndown')
|
|
|
+const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm
|
|
|
+const cheerio = require('cheerio')
|
|
|
|
|
|
/* global WIKI */
|
|
|
|
|
@@ -140,6 +143,7 @@ module.exports = class Page extends Model {
|
|
|
creatorId: 'uint',
|
|
|
creatorName: 'string',
|
|
|
description: 'string',
|
|
|
+ editorKey: 'string',
|
|
|
isPrivate: 'boolean',
|
|
|
isPublished: 'boolean',
|
|
|
publishEndDate: 'string',
|
|
@@ -471,6 +475,134 @@ module.exports = class Page extends Model {
|
|
|
return page
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Convert an Existing Page
|
|
|
+ *
|
|
|
+ * @param {Object} opts Page Properties
|
|
|
+ * @returns {Promise} Promise of the Page Model Instance
|
|
|
+ */
|
|
|
+ static async convertPage(opts) {
|
|
|
+ // -> Fetch original page
|
|
|
+ const ogPage = await WIKI.models.pages.query().findById(opts.id)
|
|
|
+ if (!ogPage) {
|
|
|
+ throw new Error('Invalid Page Id')
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Check for page access
|
|
|
+ if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
|
|
|
+ locale: ogPage.localeCode,
|
|
|
+ path: ogPage.path
|
|
|
+ })) {
|
|
|
+ throw new WIKI.Error.PageUpdateForbidden()
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Check content type
|
|
|
+ const sourceContentType = ogPage.contentType
|
|
|
+ const targetContentType = _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text')
|
|
|
+ const shouldConvert = sourceContentType !== targetContentType
|
|
|
+ let convertedContent = null
|
|
|
+
|
|
|
+ // -> Convert content
|
|
|
+ if (shouldConvert) {
|
|
|
+ // -> Markdown => HTML
|
|
|
+ if (sourceContentType === 'markdown' && targetContentType === 'html') {
|
|
|
+ if (!ogPage.render) {
|
|
|
+ throw new Error('Aborted conversion because rendered page content is empty!')
|
|
|
+ }
|
|
|
+ convertedContent = ogPage.render
|
|
|
+
|
|
|
+ const $ = cheerio.load(convertedContent, {
|
|
|
+ decodeEntities: true
|
|
|
+ })
|
|
|
+
|
|
|
+ if ($.root().children().length > 0) {
|
|
|
+ $('.toc-anchor').remove()
|
|
|
+
|
|
|
+ convertedContent = $.html('body').replace('<body>', '').replace('</body>', '').replace(/&#x([0-9a-f]{1,6});/ig, (entity, code) => {
|
|
|
+ code = parseInt(code, 16)
|
|
|
+
|
|
|
+ // Don't unescape ASCII characters, assuming they're encoded for a good reason
|
|
|
+ if (code < 0x80) return entity
|
|
|
+
|
|
|
+ return String.fromCodePoint(code)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> HTML => Markdown
|
|
|
+ } else if (sourceContentType === 'html' && targetContentType === 'markdown') {
|
|
|
+ const td = new TurndownService({
|
|
|
+ bulletListMarker: '-',
|
|
|
+ codeBlockStyle: 'fenced',
|
|
|
+ emDelimiter: '*',
|
|
|
+ fence: '```',
|
|
|
+ headingStyle: 'atx',
|
|
|
+ hr: '---',
|
|
|
+ linkStyle: 'inlined',
|
|
|
+ preformattedCode: true,
|
|
|
+ strongDelimiter: '**'
|
|
|
+ })
|
|
|
+
|
|
|
+ td.use(turndownPluginGfm)
|
|
|
+
|
|
|
+ td.keep(['kbd'])
|
|
|
+
|
|
|
+ td.addRule('subscript', {
|
|
|
+ filter: ['sub'],
|
|
|
+ replacement: c => `~${c}~`
|
|
|
+ })
|
|
|
+
|
|
|
+ td.addRule('superscript', {
|
|
|
+ filter: ['sup'],
|
|
|
+ replacement: c => `^${c}^`
|
|
|
+ })
|
|
|
+
|
|
|
+ td.addRule('underline', {
|
|
|
+ filter: ['u'],
|
|
|
+ replacement: c => `_${c}_`
|
|
|
+ })
|
|
|
+
|
|
|
+ td.addRule('removeTocAnchors', {
|
|
|
+ filter: (n, o) => {
|
|
|
+ return n.nodeName === 'A' && n.classList.contains('toc-anchor')
|
|
|
+ },
|
|
|
+ replacement: c => ''
|
|
|
+ })
|
|
|
+
|
|
|
+ convertedContent = td.turndown(ogPage.content)
|
|
|
+ // -> Unsupported
|
|
|
+ } else {
|
|
|
+ throw new Error('Unsupported source / destination content types combination.')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Create version snapshot
|
|
|
+ if (shouldConvert) {
|
|
|
+ await WIKI.models.pageHistory.addVersion({
|
|
|
+ ...ogPage,
|
|
|
+ isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,
|
|
|
+ action: 'updated',
|
|
|
+ versionDate: ogPage.updatedAt
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Update page
|
|
|
+ await WIKI.models.pages.query().patch({
|
|
|
+ contentType: targetContentType,
|
|
|
+ editorKey: opts.editor,
|
|
|
+ ...(convertedContent ? { content: convertedContent } : {})
|
|
|
+ }).where('id', ogPage.id)
|
|
|
+ const page = await WIKI.models.pages.getPageFromDb(ogPage.id)
|
|
|
+
|
|
|
+ await WIKI.models.pages.deletePageFromCache(page.hash)
|
|
|
+ WIKI.events.outbound.emit('deletePageFromCache', page.hash)
|
|
|
+
|
|
|
+ // -> Update on Storage
|
|
|
+ await WIKI.models.storage.pageEvent({
|
|
|
+ event: 'updated',
|
|
|
+ page
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Move a Page
|
|
|
*
|
|
@@ -872,6 +1004,7 @@ module.exports = class Page extends Model {
|
|
|
creatorId: page.creatorId,
|
|
|
creatorName: page.creatorName,
|
|
|
description: page.description,
|
|
|
+ editorKey: page.editorKey,
|
|
|
extra: {
|
|
|
css: _.get(page, 'extra.css', ''),
|
|
|
js: _.get(page, 'extra.js', '')
|