mirror of https://github.com/requarks/wiki
parent
eef9d442f6
commit
ac39ee08e1
@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"christian-kohler.path-intellisense",
|
||||
"mrmlnc.vscode-puglint",
|
||||
"octref.vetur"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible Node.js debug attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach (Inspector Protocol)",
|
||||
"port": 9229,
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceRoot}\\server.js"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Port",
|
||||
"address": "localhost",
|
||||
"port": 9222
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"eslint.enable": true,
|
||||
"puglint.enable": true,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.tabSize": 2,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"vue"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"server/locales"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
# **PR: Refactor: Extract page conversion logic to dedicated service**
|
||||
## **Description**
|
||||
This Pull Request refactors the Page model by extracting the complex HTML <-> Markdown conversion logic into a dedicated service (PageConverter).
|
||||
### **Changes**
|
||||
- Created server/services/page-converter.js: This new service handles all DOM manipulation (Cheerio) and Markdown generation (Turndown).
|
||||
- Modified server/models/pages.js: Removed dependencies on cheerio, turndown, and turndown-plugin-gfm. The convertPage method now delegates the conversion task to the new service.
|
||||
|
||||
### **Benefits:**
|
||||
- Code Cleanup: The `pages.js` file sheds approximately 80 lines of conversion logic that are unrelated to data persistence (Model responsibility).
|
||||
- Import Performance: Since `pages.js` is a frequently loaded model, removing heavy dependencies like `cheerio` and `turndown` makes its initial load time lighter and more efficient.
|
||||
- Maintainability: Future changes to how Markdown/HTML is generated/converted will only require modifying `page-converter.js`, isolating the logic from the core database model.
|
||||
|
||||
|
||||
## **File Changes**
|
||||
1. [NEW] server/services/page-converter.js
|
||||
2. [MODIFY] server/models/pages.js
|
||||
@ -0,0 +1,176 @@
|
||||
const _ = require('lodash')
|
||||
const TurndownService = require('turndown')
|
||||
const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm
|
||||
const cheerio = require('cheerio')
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
module.exports = class PageConverter {
|
||||
/**
|
||||
* 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')
|
||||
}
|
||||
|
||||
if (ogPage.editorKey === opts.editor) {
|
||||
throw new Error('Page is already using this editor. Nothing to convert.')
|
||||
}
|
||||
|
||||
// -> 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) {
|
||||
// Remove header anchors
|
||||
$('.toc-anchor').remove()
|
||||
|
||||
// Attempt to convert tabsets
|
||||
$('tabset').each((tabI, tabElm) => {
|
||||
const tabHeaders = []
|
||||
// -> Extract templates
|
||||
$(tabElm).children('template').each((tmplI, tmplElm) => {
|
||||
if ($(tmplElm).attr('v-slot:tabs') === '') {
|
||||
$(tabElm).before('<ul class="tabset-headers">' + $(tmplElm).html() + '</ul>')
|
||||
} else {
|
||||
$(tabElm).after('<div class="markdown-tabset">' + $(tmplElm).html() + '</div>')
|
||||
}
|
||||
})
|
||||
// -> Parse tab headers
|
||||
$(tabElm).prev('.tabset-headers').children((i, elm) => {
|
||||
tabHeaders.push($(elm).html())
|
||||
})
|
||||
$(tabElm).prev('.tabset-headers').remove()
|
||||
// -> Inject tab headers
|
||||
$(tabElm).next('.markdown-tabset').children((i, elm) => {
|
||||
if (tabHeaders.length > i) {
|
||||
$(elm).prepend(`<h2>${tabHeaders[i]}</h2>`)
|
||||
}
|
||||
})
|
||||
$(tabElm).next('.markdown-tabset').prepend('<h1>Tabset</h1>')
|
||||
$(tabElm).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('taskList', {
|
||||
filter: (n, o) => {
|
||||
return n.nodeName === 'INPUT' && n.getAttribute('type') === 'checkbox'
|
||||
},
|
||||
replacement: (c, n) => {
|
||||
return n.getAttribute('checked') ? '[x] ' : '[ ] '
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue