feat: convert page
This commit is contained in:
		| @@ -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', '') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user