ソースを参照

feat: rendering modules + admin rendering

Nicolas Giard 6 年 前
コミット
4cb9f3d9c6
26 ファイル変更413 行追加537 行削除
  1. 67 134
      client/components/admin/admin-rendering.vue
  2. 16 178
      client/components/setup.vue
  3. 12 0
      client/graph/admin/rendering/rendering-mutation-save-renderers.gql
  4. 18 0
      client/graph/admin/rendering/rendering-query-renderers.gql
  5. 1 0
      server/core/kernel.js
  6. 6 1
      server/db/migrations/2.0.0.js
  7. 58 0
      server/graph/resolvers/rendering.js
  8. 55 0
      server/graph/schemas/rendering.graphql
  9. 108 0
      server/models/renderers.js
  10. 6 5
      server/modules/renderer/html-asciinema/definition.yml
  11. 6 5
      server/modules/renderer/html-blockquotes/definition.yml
  12. 8 0
      server/modules/renderer/html-core/definition.yml
  13. 1 0
      server/modules/renderer/html-core/renderer.js
  14. 6 5
      server/modules/renderer/html-mathjax/definition.yml
  15. 6 5
      server/modules/renderer/html-mediaplayers/definition.yml
  16. 6 5
      server/modules/renderer/html-security/definition.yml
  17. 3 2
      server/modules/renderer/markdown-abbr/definition.yml
  18. 3 1
      server/modules/renderer/markdown-core/definition.yml
  19. 3 2
      server/modules/renderer/markdown-emoji/definition.yml
  20. 3 2
      server/modules/renderer/markdown-expandtabs/definition.yml
  21. 3 2
      server/modules/renderer/markdown-footnotes/definition.yml
  22. 3 2
      server/modules/renderer/markdown-mathjax/definition.yml
  23. 3 2
      server/modules/renderer/markdown-mermaid/definition.yml
  24. 3 2
      server/modules/renderer/markdown-plantuml/definition.yml
  25. 3 2
      server/modules/renderer/markdown-tasklists/definition.yml
  26. 6 182
      server/setup.js

+ 67 - 134
client/components/admin/admin-rendering.vue

@@ -6,141 +6,45 @@
         .subheading.grey--text Configure how content is rendered
         v-layout.mt-3(row wrap)
           v-flex(lg3 xs12)
-            v-card
-              v-toolbar(
-                color='primary'
-                dense
-                flat
-                dark
+            v-toolbar(
+              color='primary'
+              dense
+              flat
+              dark
+              )
+              v-icon.mr-2 line_weight
+              .subheading Pipeline
+            v-expansion-panel.adm-rendering-pipeline
+              v-expansion-panel-content(
+                hide-actions
+                v-for='core in cores'
+                :key='core.key'
                 )
-                v-icon.mr-2 line_weight
-                .subheading Pipeline
-              v-toolbar(
-                color='blue'
-                dense
-                dark
-                )
-                v-icon.mr-2 arrow_downward
-                .body-2 Markdown
-                v-spacer
-                v-btn(
-                  icon
-                  @click=''
+                v-toolbar(
+                  slot='header'
+                  color='blue'
+                  dense
+                  dark
+                  flat
                   )
-                  v-icon add
-              v-list(two-line, dense)
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') crop_free
-                  v-list-tile-content
-                    v-list-tile-title Core
-                    v-list-tile-sub-title Basic Markdown parser
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') tag_faces
-                  v-list-tile-content
-                    v-list-tile-title Emoji
-                    v-list-tile-sub-title Convert tags to emojis
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') list
-                  v-list-tile-content
-                    v-list-tile-title Task Lists
-                    v-list-tile-sub-title Parse task lists to checkboxes
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') multiline_chart
-                  v-list-tile-content
-                    v-list-tile-title PlantUML
-                    v-list-tile-sub-title Generate diagrams from PlantUML syntax
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') merge_type
-                  v-list-tile-content
-                    v-list-tile-title Mermaid
-                    v-list-tile-sub-title Generate flowcharts from Mermaid syntax
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') functions
-                  v-list-tile-content
-                    v-list-tile-title Mathjax Pre-Processor
-                    v-list-tile-sub-title Parse Mathjax content from Markdown
-                  v-list-tile-avatar
-                    v-icon(color='red', small) trip_origin
-
-              v-toolbar(
-                color='blue'
-                dense
-                dark
-                )
-                v-icon.mr-2 arrow_downward
-                .body-2 HTML
-                v-spacer
-                v-btn(
-                  icon
-                  @click=''
-                  )
-                  v-icon add
-              v-list(two-line, dense)
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') subscriptions
-                  v-list-tile-content
-                    v-list-tile-title Video Players
-                    v-list-tile-sub-title Embed video players such as Youtube, Vimeo, etc.
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') subtitles
-                  v-list-tile-content
-                    v-list-tile-title Asciinema
-                    v-list-tile-sub-title Embed asciinema players from compatible links
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') volume_up
-                  v-list-tile-content
-                    v-list-tile-title Audio Players
-                    v-list-tile-sub-title Embed audio players for audio content
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') insert_comment
-                  v-list-tile-content
-                    v-list-tile-title Blockquotes
-                    v-list-tile-sub-title Process styled blockquotes
-                  v-list-tile-avatar
-                    v-icon(color='green', small) lens
-                v-divider.my-0
-                v-list-tile(avatar)
-                  v-list-tile-avatar
-                    v-icon(color='grey') functions
-                  v-list-tile-content
-                    v-list-tile-title Mathjax Processor
-                    v-list-tile-sub-title TeX/MathML Math Equations Parser
-                  v-list-tile-avatar
-                    v-icon(color='red', small) trip_origin
+                  .body-2 {{core.input}}
+                  v-icon.mx-2 arrow_forward
+                  .caption {{core.output}}
+                v-list(two-line, dense)
+                  v-list-tile(
+                    avatar
+                    v-for='rdr in core.children'
+                    :key='rdr.key'
+                    )
+                    v-list-tile-avatar
+                      v-icon(color='grey') {{rdr.icon}}
+                    v-list-tile-content
+                      v-list-tile-title {{rdr.title}}
+                      v-list-tile-sub-title {{rdr.description}}
+                    v-list-tile-avatar
+                      v-icon(color='green', small, v-if='rdr.isEnabled') lens
+                      v-icon(color='red', small, v-else) trip_origin
+                  v-divider.my-0
 
           v-flex(lg9 xs12)
             v-card
@@ -201,16 +105,45 @@
 </template>
 
 <script>
+import _ from 'lodash'
+
+import renderersQuery from 'gql/admin/rendering/rendering-query-renderers.gql'
+
 export default {
   data() {
     return {
       linkify: true,
-      codeTheme: 'Light'
+      codeTheme: 'Light',
+      renderers: []
+    }
+  },
+  computed: {
+    cores() {
+      return _.filter(this.renderers, ['dependsOn', null]).map(core => {
+        core.children = _.concat([_.cloneDeep(core)], _.filter(this.renderers, ['dependsOn', core.key]))
+        return core
+      })
+    }
+  },
+  apollo: {
+    renderers: {
+      query: renderersQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => _.cloneDeep(data.rendering.renderers).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})),
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-rendering-refresh')
+      }
     }
   }
 }
 </script>
 
 <style lang='scss'>
+.adm-rendering-pipeline {
+  border-top: 1px solid #FFF;
 
+  .v-expansion-panel__header {
+    padding: 0 0;
+  }
+}
 </style>

+ 16 - 178
client/components/setup.vue

@@ -14,22 +14,14 @@
             small Wiki.js Installation Wizard
           v-divider
           v-stepper-step(step='2' :complete='state > 2')
-            | System Check
-            small Checking your system for compatibility
-          v-divider
-          v-stepper-step(step='3' :complete='state > 3')
-            | General Information
-            small Site Title, Language and Access
-          v-divider
-          v-stepper-step(step='4' :complete='state > 4')
             | Administration Account
             small Create the admin account
           v-divider(v-if='this.conf.upgrade')
-          v-stepper-step(step='5' :complete='state > 5', v-if='this.conf.upgrade')
+          v-stepper-step(step='3' :complete='state > 3', v-if='this.conf.upgrade')
             | Upgrade from Wiki.js 1.x
             small Migrate your existing installation
           v-divider
-          v-stepper-step(:step='this.conf.upgrade ? 6 : 5' :complete='this.conf.upgrade ? state > 5 : state > 6')
+          v-stepper-step(:step='this.conf.upgrade ? 4 : 3' :complete='this.conf.upgrade ? state > 3 : state > 4')
             | Final Steps
             small Finalizing your installation
 
@@ -68,126 +60,13 @@
                 )
             v-divider
             .pt-3.text-xs-center
-              v-btn(color='primary', @click='proceedToSyscheck', :disabled='loading') Start
-
-          //- ==============================================
-          //- SYSTEM CHECK
-          //- ==============================================
-
-          v-stepper-content(step='2')
-            v-card.text-xs-center(flat)
-              svg.icons.is-64: use(xlink:href='#nc-metrics')
-              .subheading System Check
-            v-container
-              v-layout(row, align-center, v-if='loading')
-                v-progress-circular(v-if='loading', indeterminate, color='blue')
-                .body-2.blue--text.ml-3 Checking your system for compatibility...
-              v-alert(type='success', outline, :value='!loading && syscheck.ok') Looks good! No issues so far.
-              v-alert(type='error', outline, :value='!loading && !syscheck.ok') {{ syscheck.error }}
-              v-list.mt-3(two-line, v-if='!loading && syscheck.ok', dense)
-                template(v-for='(rs, n) in syscheck.results')
-                  v-divider(v-if='n > 0', inset)
-                  v-list-tile
-                    v-list-tile-avatar(color='green lighten-5')
-                      v-icon(color='green') check
-                    v-list-tile-content
-                      v-list-tile-title {{rs.title}}
-                      v-list-tile-sub-title {{rs.subtitle}}
-            v-divider
-            .pt-3.text-xs-center
-              v-btn(@click='proceedToWelcome', :disabled='loading') Back
-              v-btn(color='primary', @click='proceedToSyscheck', v-if='!loading && !syscheck.ok') Check Again
-              v-btn(color='red', dark, @click='proceedToGeneral', v-if='!loading && !syscheck.ok') Continue Anyway
-              v-btn(color='primary', @click='proceedToGeneral', v-if='loading || syscheck.ok', :disabled='loading') Continue
-
-          //- ==============================================
-          //- GENERAL
-          //- ==============================================
-
-          v-stepper-content(step='3')
-            v-card.text-xs-center(flat)
-              svg.icons.is-64: use(xlink:href='#nc-webpage')
-              .subheading General Information
-            v-form
-              v-container
-                v-layout(row, wrap)
-                  v-flex(xs12, sm6).pr-3
-                    v-text-field(
-                      outline
-                      background-color='grey lighten-2'
-                      v-model='conf.title',
-                      label='Site Title',
-                      :counter='255',
-                      persistent-hint,
-                      hint='The site title will appear in the top left corner on every page and within the window title bar.',
-                      v-validate='{ required: true, min: 2 }',
-                      data-vv-name='siteTitle',
-                      data-vv-as='Site Title',
-                      data-vv-scope='general',
-                      :error-messages='errors.collect(`siteTitle`)'
-                    )
-                  v-flex.pr-3(xs12, sm6)
-                    v-text-field(
-                      outline
-                      background-color='grey lighten-2'
-                      v-model='conf.port',
-                      label='Server Port',
-                      persistent-hint,
-                      hint='The port on which Wiki.js will listen to. Usually port 80 if connecting directly, or a random port (e.g. 3000) if using a web server in front of it. Set $(PORT) to use the PORT environment variable.',
-                      v-validate='{ required: true }',
-                      data-vv-name='port',
-                      data-vv-as='Port',
-                      data-vv-scope='general',
-                      :error-messages='errors.collect(`port`)'
-                    )
-                v-layout(row, wrap).mt-3
-                  v-flex(xs12, sm6).pr-3
-                    v-text-field(
-                      outline
-                      background-color='grey lighten-2'
-                      v-model='conf.pathContent',
-                      label='Content Data Path',
-                      persistent-hint,
-                      hint='The path where content is stored (markdown files, uploads, etc.)',
-                      v-validate='{ required: true, min: 2 }',
-                      data-vv-name='pathContent',
-                      data-vv-as='Content Data Path',
-                      data-vv-scope='general',
-                      :error-messages='errors.collect(`pathContent`)'
-                    )
-                  v-flex(xs12, sm6)
-                    v-text-field(
-                      outline
-                      background-color='grey lighten-2'
-                      v-model='conf.pathData',
-                      label='Temporary Data Path',
-                      persistent-hint,
-                      hint='The path where temporary data is stored (cache, thumbnails, temporary upload files, etc.)',
-                      v-validate='{ required: true, min: 2 }',
-                      data-vv-name='pathData',
-                      data-vv-as='Temporary Data Path',
-                      data-vv-scope='general',
-                      :error-messages='errors.collect(`pathData`)'
-                    )
-                v-layout(row, wrap).mt-3
-                  v-flex(xs12)
-                    v-checkbox(
-                      color='primary',
-                      v-model='conf.public',
-                      label='Public Access',
-                      persistent-hint,
-                      hint='Should the site be accessible (read only) without login.'
-                    )
-            v-divider
-            .pt-3.text-xs-center
-              v-btn(@click='proceedToSyscheck', :disabled='loading') Back
-              v-btn(color='primary', @click='proceedToAdmin', :disabled='loading') Continue
+              v-btn(color='primary', @click='proceedToAdmin', :disabled='loading') Start
 
           //- ==============================================
           //- ADMINISTRATOR ACCOUNT
           //- ==============================================
 
-          v-stepper-content(step='4')
+          v-stepper-content(step='2')
             v-card.text-xs-center(flat)
               svg.icons.is-64: use(xlink:href='#nc-man-black')
               .subheading Administrator Account
@@ -245,14 +124,14 @@
                       :error-messages='errors.collect(`adminPasswordConfirm`)'
                     )
               .pt-3.text-xs-center
-                v-btn(@click='proceedToGeneral', :disabled='loading') Back
+                v-btn(@click='proceedToWelcome', :disabled='loading') Back
                 v-btn(color='primary', @click='proceedToUpgrade', :disabled='loading') Continue
 
           //- ==============================================
           //- UPGRADE FROM 1.x
           //- ==============================================
 
-          v-stepper-content(step='5', v-if='conf.upgrade')
+          v-stepper-content(step='3', v-if='conf.upgrade')
             v-card.text-xs-center(flat)
               svg.icons.is-64: use(xlink:href='#nc-spaceship')
               .subheading Upgrade from Wiki.js 1.x
@@ -283,7 +162,7 @@
           //- FINAL
           //- ==============================================
 
-          v-stepper-content(:step='conf.upgrade ? 6 : 5')
+          v-stepper-content(:step='conf.upgrade ? 4 : 3')
             v-card.text-xs-center(flat)
               template(v-if='loading')
                 .mt-3(style='width: 64px; display:inline-block;')
@@ -340,11 +219,6 @@ export default {
     return {
       loading: false,
       state: 1,
-      syscheck: {
-        ok: false,
-        error: '',
-        results: []
-      },
       final: {
         ok: false,
         error: '',
@@ -354,13 +228,7 @@ export default {
         adminEmail: '',
         adminPassword: '',
         adminPasswordConfirm: '',
-        lang: siteConfig.lang || 'en',
-        pathData: './data',
-        pathContent: './content',
-        port: siteConfig.port || 80,
-        public: (siteConfig.public === true),
         telemetry: true,
-        title: siteConfig.title || 'Wiki',
         upgrade: false,
         upgMongo: 'mongodb://'
       },
@@ -373,73 +241,43 @@ export default {
       this.state = 1
       this.loading = false
     },
-    proceedToSyscheck () {
-      let self = this
-      this.state = 2
-      this.loading = true
-      this.syscheck = {
-        ok: false,
-        error: '',
-        results: []
-      }
-
-      _.delay(() => {
-        axios.post('/syscheck', self.conf).then(resp => {
-          if (resp.data.ok === true) {
-            self.syscheck.ok = true
-            self.syscheck.results = resp.data.results
-          } else {
-            self.syscheck.ok = false
-            self.syscheck.error = resp.data.error
-          }
-          self.loading = false
-          self.$nextTick()
-        }).catch(err => {
-          window.alert(err.message)
-        })
-      }, 1000)
-    },
-    proceedToGeneral () {
-      this.state = 3
-      this.loading = false
-    },
     async proceedToAdmin () {
-      if (this.state < 4) {
+      if (this.state < 2) {
         const validationSuccess = await this.$validator.validateAll('general')
         if (!validationSuccess) {
-          this.state = 3
+          this.state = 1
           return
         }
       }
-      this.state = 4
+      this.state = 2
       this.loading = false
     },
     async proceedToUpgrade () {
-      if (this.state < 5) {
+      if (this.state < 3) {
         const validationSuccess = await this.$validator.validateAll('admin')
         if (!validationSuccess || this.conf.adminPassword !== this.conf.adminPasswordConfirm) {
-          this.state = 4
+          this.state = 2
           return
         }
       }
 
       if (this.conf.upgrade) {
-        this.state = 5
+        this.state = 3
         this.loading = false
       } else {
         this.proceedToFinal()
       }
     },
     async proceedToFinal () {
-      if (this.conf.upgrade && this.state < 6) {
+      if (this.conf.upgrade && this.state < 4) {
         const validationSuccess = await this.$validator.validateAll('upgrade')
         if (!validationSuccess) {
-          this.state = 5
+          this.state = 3
           return
         }
       }
 
-      this.state = this.conf.upgrade ? 6 : 5
+      this.state = this.conf.upgrade ? 4 : 3
       this.loading = true
       this.final = {
         ok: false,

+ 12 - 0
client/graph/admin/rendering/rendering-mutation-save-renderers.gql

@@ -0,0 +1,12 @@
+mutation($renderers: [RendererInput]) {
+  rendering {
+    updateRenderers(renderers: $renderers) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

+ 18 - 0
client/graph/admin/rendering/rendering-query-renderers.gql

@@ -0,0 +1,18 @@
+{
+  rendering {
+    renderers {
+      isEnabled
+      key
+      title
+      description
+      icon
+      dependsOn
+      input
+      output
+      config {
+        key
+        value
+      }
+    }
+  }
+}

+ 1 - 0
server/core/kernel.js

@@ -51,6 +51,7 @@ module.exports = {
   async postBootMaster() {
     await WIKI.models.authentication.refreshStrategiesFromDisk()
     await WIKI.models.editors.refreshEditorsFromDisk()
+    await WIKI.models.renderers.refreshRenderersFromDisk()
     await WIKI.models.storage.refreshTargetsFromDisk()
 
     await WIKI.auth.activateStrategies()

+ 6 - 1
server/db/migrations/2.0.0.js

@@ -96,7 +96,7 @@ exports.up = knex => {
       table.string('createdAt').notNullable()
       table.string('updatedAt').notNullable()
     })
-    // STORAGE -----------------------------
+    // RENDERERS ---------------------------
     .createTable('renderers', table => {
       table.increments('id').primary()
       table.string('key').notNullable().unique()
@@ -199,14 +199,19 @@ exports.up = knex => {
 exports.down = knex => {
   return knex.schema
     .dropTableIfExists('userGroups')
+    .dropTableIfExists('pageHistoryTags')
+    .dropTableIfExists('pageHistory')
     .dropTableIfExists('pageTags')
     .dropTableIfExists('assets')
     .dropTableIfExists('assetFolders')
     .dropTableIfExists('comments')
+    .dropTableIfExists('editors')
     .dropTableIfExists('groups')
     .dropTableIfExists('locales')
     .dropTableIfExists('pages')
+    .dropTableIfExists('renderers')
     .dropTableIfExists('settings')
+    .dropTableIfExists('storage')
     .dropTableIfExists('tags')
     .dropTableIfExists('users')
 }

+ 58 - 0
server/graph/resolvers/rendering.js

@@ -0,0 +1,58 @@
+const _ = require('lodash')
+const graphHelper = require('../../helpers/graph')
+
+/* global WIKI */
+
+module.exports = {
+  Query: {
+    async rendering() { return {} }
+  },
+  Mutation: {
+    async rendering() { return {} }
+  },
+  RenderingQuery: {
+    async renderers(obj, args, context, info) {
+      let renderers = await WIKI.models.renderers.getRenderers()
+      renderers = renderers.map(rdr => {
+        const rendererInfo = _.find(WIKI.data.renderers, ['key', rdr.key]) || {}
+        return {
+          ...rendererInfo,
+          ...rdr,
+          config: _.sortBy(_.transform(rdr.config, (res, value, key) => {
+            const configData = _.get(rendererInfo.props, key, {})
+            res.push({
+              key,
+              value: JSON.stringify({
+                ...configData,
+                value
+              })
+            })
+          }, []), 'key')
+        }
+      })
+      if (args.filter) { renderers = graphHelper.filter(renderers, args.filter) }
+      if (args.orderBy) { renderers = graphHelper.orderBy(renderers, args.orderBy) }
+      return renderers
+    }
+  },
+  RenderingMutation: {
+    async updateRenderers(obj, args, context) {
+      try {
+        for (let rdr of args.renderers) {
+          await WIKI.models.storage.query().patch({
+            isEnabled: rdr.isEnabled,
+            config: _.reduce(rdr.config, (result, value, key) => {
+              _.set(result, `${value.key}`, value.value)
+              return result
+            }, {})
+          }).where('key', rdr.key)
+        }
+        return {
+          responseResult: graphHelper.generateSuccess('Renderers updated successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    }
+  }
+}

+ 55 - 0
server/graph/schemas/rendering.graphql

@@ -0,0 +1,55 @@
+# ===============================================
+# RENDERING
+# ===============================================
+
+extend type Query {
+  rendering: RenderingQuery
+}
+
+extend type Mutation {
+  rendering: RenderingMutation
+}
+
+# -----------------------------------------------
+# QUERIES
+# -----------------------------------------------
+
+type RenderingQuery {
+  renderers(
+    filter: String
+    orderBy: String
+  ): [Renderer]
+}
+
+# -----------------------------------------------
+# MUTATIONS
+# -----------------------------------------------
+
+type RenderingMutation {
+  updateRenderers(
+    renderers: [RendererInput]
+  ): DefaultResponse
+}
+
+# -----------------------------------------------
+# TYPES
+# -----------------------------------------------
+
+type Renderer {
+  isEnabled: Boolean!
+  key: String!
+  title: String!
+  description: String
+  icon: String
+  dependsOn: String
+  input: String
+  output: String
+  config: [KeyValuePair]
+}
+
+input RendererInput {
+  isEnabled: Boolean!
+  key: String!
+  mode: String!
+  config: [KeyValuePairInput]
+}

+ 108 - 0
server/models/renderers.js

@@ -0,0 +1,108 @@
+const Model = require('objection').Model
+const path = require('path')
+const fs = require('fs-extra')
+const _ = require('lodash')
+const yaml = require('js-yaml')
+const commonHelper = require('../helpers/common')
+
+/* global WIKI */
+
+/**
+ * Renderer model
+ */
+module.exports = class Renderer extends Model {
+  static get tableName() { return 'renderers' }
+
+  static get jsonSchema () {
+    return {
+      type: 'object',
+      required: ['key', 'isEnabled'],
+
+      properties: {
+        id: {type: 'integer'},
+        key: {type: 'string'},
+        isEnabled: {type: 'boolean'},
+        config: {type: 'object'}
+      }
+    }
+  }
+
+  static async getRenderers() {
+    return WIKI.models.renderers.query()
+  }
+
+  static async refreshRenderersFromDisk() {
+    let trx
+    try {
+      const dbRenderers = await WIKI.models.renderers.query()
+
+      // -> Fetch definitions from disk
+      const rendererDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/renderer'))
+      let diskRenderers = []
+      for (let dir of rendererDirs) {
+        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/renderer', dir, 'definition.yml'), 'utf8')
+        diskRenderers.push(yaml.safeLoad(def))
+      }
+      WIKI.data.renderers = diskRenderers.map(renderer => ({
+        ...renderer,
+        props: commonHelper.parseModuleProps(renderer.props)
+      }))
+
+      // -> Insert new Renderers
+      let newRenderers = []
+      for (let renderer of WIKI.data.renderers) {
+        if (!_.some(dbRenderers, ['key', renderer.key])) {
+          newRenderers.push({
+            key: renderer.key,
+            isEnabled: _.get(renderer, 'enabledDefault', true),
+            config: _.transform(renderer.props, (result, value, key) => {
+              _.set(result, key, value.default)
+              return result
+            }, {})
+          })
+        } else {
+          const rendererConfig = _.get(_.find(dbRenderers, ['key', renderer.key]), 'config', {})
+          await WIKI.models.renderers.query().patch({
+            config: _.transform(renderer.props, (result, value, key) => {
+              if (!_.has(result, key)) {
+                _.set(result, key, value.default)
+              }
+              return result
+            }, rendererConfig)
+          }).where('key', renderer.key)
+        }
+      }
+      if (newRenderers.length > 0) {
+        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)
+        for (let renderer of newRenderers) {
+          await WIKI.models.renderers.query(trx).insert(renderer)
+        }
+        await trx.commit()
+        WIKI.logger.info(`Loaded ${newRenderers.length} new renderers: [ OK ]`)
+      } else {
+        WIKI.logger.info(`No new renderers found: [ SKIPPED ]`)
+      }
+    } catch (err) {
+      WIKI.logger.error(`Failed to scan or load new renderers: [ FAILED ]`)
+      WIKI.logger.error(err)
+      if (trx) {
+        trx.rollback()
+      }
+    }
+  }
+
+  static async pageEvent({ event, page }) {
+    const targets = await WIKI.models.storage.query().where('isEnabled', true)
+    if (targets && targets.length > 0) {
+      _.forEach(targets, target => {
+        WIKI.queue.job.syncStorage.add({
+          event,
+          target,
+          page
+        }, {
+          removeOnComplete: true
+        })
+      })
+    }
+  }
+}

+ 6 - 5
server/modules/renderer/html-asciinema/definition.yml

@@ -1,7 +1,8 @@
-key: markdownAbbr
-title: Abbreviations
-description: Parse abbreviations into abbr tags
+key: htmlAsciinema
+title: Asciinema
+description: Embed asciinema players from compatible links
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: subtitles
+enabledDefault: false
+dependsOn: htmlCore
 props: {}

+ 6 - 5
server/modules/renderer/html-blockquotes/definition.yml

@@ -1,7 +1,8 @@
-key: markdownAbbr
-title: Abbreviations
-description: Parse abbreviations into abbr tags
+key: htmlBlockquotes
+title: Blockquotes
+description: Embed audio players for audio content
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: insert_comment
+enabledDefault: true
+dependsOn: htmlCore
 props: {}

+ 8 - 0
server/modules/renderer/html-core/definition.yml

@@ -0,0 +1,8 @@
+key: htmlCore
+title: Core
+description: Basic HTML Parser
+author: requarks.io
+input: html
+output: html
+icon: crop_free
+props: {}

+ 1 - 0
server/modules/renderer/html-core/renderer.js

@@ -0,0 +1 @@
+module.exports = {}

+ 6 - 5
server/modules/renderer/html-mathjax/definition.yml

@@ -1,7 +1,8 @@
-key: markdownAbbr
-title: Abbreviations
-description: Parse abbreviations into abbr tags
+key: htmlMathjax
+title: Mathjax Processor
+description: TeX/MathML Math Equations Parser
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: functions
+enabledDefault: false
+dependsOn: htmlCore
 props: {}

+ 6 - 5
server/modules/renderer/html-mediaplayers/definition.yml

@@ -1,7 +1,8 @@
-key: markdownAbbr
-title: Abbreviations
-description: Parse abbreviations into abbr tags
+key: htmlMedia
+title: Media Players
+description: Embed players such as Youtube, Vimeo, Soundcloud, etc.
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: subscriptions
+enabledDefault: false
+dependsOn: htmlCore
 props: {}

+ 6 - 5
server/modules/renderer/html-security/definition.yml

@@ -1,7 +1,8 @@
-key: markdownAbbr
-title: Abbreviations
-description: Parse abbreviations into abbr tags
+key: htmlSecurity
+title: Security
+description: Filter and strips potentially dangerous content
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: whatshot
+enabledDefault: true
+dependsOn: htmlCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-abbr/definition.yml

@@ -2,6 +2,7 @@ key: markdownAbbr
 title: Abbreviations
 description: Parse abbreviations into abbr tags
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: text_format
+enabledDefault: true
+dependsOn: markdownCore
 props: {}

+ 3 - 1
server/modules/renderer/markdown-core/definition.yml

@@ -2,7 +2,9 @@ key: markdownCore
 title: Core
 description: Basic Markdown Parser
 author: requarks.io
-dependsOn: []
+input: markdown
+output: html
+icon: crop_free
 props:
   linkify:
     type: Boolean

+ 3 - 2
server/modules/renderer/markdown-emoji/definition.yml

@@ -2,6 +2,7 @@ key: markdownEmoji
 title: Emoji
 description: Convert tags to emojis
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: tag_faces
+enabledDefault: true
+dependsOn: markdownCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-expandtabs/definition.yml

@@ -2,6 +2,7 @@ key: markdownExpandtabs
 title: Expand Tabs
 description: Replace tabs with spaces in code blocks
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: space_bar
+enabledDefault: true
+dependsOn: markdownCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-footnotes/definition.yml

@@ -2,6 +2,7 @@ key: markdownFootnotes
 title: Footnotes
 description: Parse footnotes references
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: low_priority
+enabledDefault: true
+dependsOn: markdownCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-mathjax/definition.yml

@@ -2,6 +2,7 @@ key: markdownMathjax
 title: Mathjax Pre-Processor
 description: Pre-parse TeX blocks for Mathjax
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: functions
+enabledDefault: false
+dependsOn: markdownCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-mermaid/definition.yml

@@ -2,6 +2,7 @@ key: markdownMermaid
 title: Mermaid
 description: Generate flowcharts from Mermaid syntax
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: merge_type
+enabledDefault: false
+dependsOn: markdownCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-plantuml/definition.yml

@@ -2,6 +2,7 @@ key: markdownPlantuml
 title: PlantUML
 description: Generate diagrams from PlantUML syntax
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: multiline_chart
+enabledDefault: false
+dependsOn: markdownCore
 props: {}

+ 3 - 2
server/modules/renderer/markdown-tasklists/definition.yml

@@ -2,6 +2,7 @@ key: markdownTasklists
 title: Task Lists
 description: Parse task lists to checkboxes
 author: requarks.io
-dependsOn:
-  - markdownCore
+icon: list
+enabledDefault: true
+dependsOn: markdownCore
 props: {}

+ 6 - 182
server/setup.js

@@ -24,7 +24,6 @@ module.exports = () => {
   const yaml = require('js-yaml')
   const _ = require('lodash')
   const cfgHelper = require('./helpers/config')
-  const filesize = require('filesize.js')
   const crypto = Promise.promisifyAll(require('crypto'))
 
   // ----------------------------------------
@@ -76,174 +75,6 @@ module.exports = () => {
     })
   })
 
-  /**
-   * Perform basic system checks
-   */
-  app.post('/syscheck', (req, res) => {
-    WIKI.telemetry.enabled = (req.body.telemetry === true)
-    WIKI.telemetry.sendEvent('setup', 'start')
-
-    Promise.mapSeries([
-      () => {
-        const semver = require('semver')
-        if (!semver.satisfies(semver.clean(process.version), '>=8.9.0')) {
-          throw new Error('Node.js version is too old. Minimum is 8.9.0.')
-        }
-        return {
-          title: 'Node.js ' + process.version + ' detected.',
-          subtitle: ' Minimum is 8.9.0.'
-        }
-      },
-      () => {
-        return Promise.try(() => {
-          require('crypto')
-        }).catch(err => {
-          throw new Error('Crypto Node.js module is not available.')
-        }).return({
-          title: 'Node.js Crypto module is available.',
-          subtitle: 'Crypto module is required.'
-        })
-      },
-      () => {
-        const exec = require('child_process').exec
-        const semver = require('semver')
-        return new Promise((resolve, reject) => {
-          exec('git --version', (err, stdout, stderr) => {
-            if (err || stdout.length < 3) {
-              reject(new Error('Git is not installed or not reachable from PATH.'))
-            }
-            let gitver = _.head(stdout.match(/[\d]+\.[\d]+(\.[\d]+)?/gi))
-            if (!gitver || !semver.satisfies(semver.clean(gitver), '>=2.7.4')) {
-              reject(new Error('Git version is too old. Minimum is 2.7.4.'))
-            }
-            resolve({
-              title: 'Git ' + gitver + ' detected.',
-              subtitle: 'Minimum is 2.7.4.'
-            })
-          })
-        })
-      },
-      () => {
-        const os = require('os')
-        if (os.totalmem() < 1000 * 1000 * 768) {
-          throw new Error('Not enough memory. Minimum is 768 MB.')
-        }
-        return {
-          title: filesize(os.totalmem()) + ' of system memory available.',
-          subtitle: 'Minimum is 768 MB.'
-        }
-      },
-      () => {
-        let fs = require('fs')
-        return Promise.try(() => {
-          fs.accessSync(path.join(WIKI.ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK)
-        }).catch(err => {
-          throw new Error('config.yml file is not writable by Node.js process or was not created properly.')
-        }).return({
-          title: 'config.yml is writable by the setup process.',
-          subtitle: 'Setup will write to this file.'
-        })
-      }
-    ], test => test()).then(results => {
-      res.json({ ok: true, results })
-    }).catch(err => {
-      res.json({ ok: false, error: err.message })
-    })
-  })
-
-  /**
-   * Check the Git connection
-   */
-  app.post('/gitcheck', (req, res) => {
-    WIKI.telemetry.sendEvent('setup', 'gitcheck')
-
-    const exec = require('execa')
-    const url = require('url')
-
-    const dataDir = path.resolve(WIKI.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData))
-    const gitDir = path.resolve(WIKI.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo))
-
-    let gitRemoteUrl = ''
-
-    if (req.body.gitUseRemote === true) {
-      let urlObj = url.parse(cfgHelper.parseConfigValue(req.body.gitUrl))
-      if (req.body.gitAuthType === 'basic') {
-        urlObj.auth = req.body.gitAuthUser + ':' + req.body.gitAuthPass
-      }
-      gitRemoteUrl = url.format(urlObj)
-    }
-
-    Promise.mapSeries([
-      () => {
-        return fs.ensureDir(dataDir).then(() => 'Data directory path is valid.')
-      },
-      () => {
-        return fs.ensureDir(gitDir).then(() => 'Git directory path is valid.')
-      },
-      () => {
-        return exec.stdout('git', ['init'], { cwd: gitDir }).then(result => {
-          return 'Local git repository has been initialized.'
-        })
-      },
-      () => {
-        if (req.body.gitUseRemote === false) { return false }
-        return exec.stdout('git', ['config', '--local', 'user.name', 'Wiki'], { cwd: gitDir }).then(result => {
-          return 'Git Signature Name has been set successfully.'
-        })
-      },
-      () => {
-        if (req.body.gitUseRemote === false) { return false }
-        return exec.stdout('git', ['config', '--local', 'user.email', req.body.gitServerEmail], { cwd: gitDir }).then(result => {
-          return 'Git Signature Name has been set successfully.'
-        })
-      },
-      () => {
-        if (req.body.gitUseRemote === false) { return false }
-        return exec.stdout('git', ['config', '--local', '--bool', 'http.sslVerify', req.body.gitAuthSSL], { cwd: gitDir }).then(result => {
-          return 'Git SSL Verify flag has been set successfully.'
-        })
-      },
-      () => {
-        if (req.body.gitUseRemote === false) { return false }
-        if (_.includes(['sshenv', 'sshdb'], req.body.gitAuthType)) {
-          req.body.gitAuthSSHKey = path.join(dataDir, 'ssh/key.pem')
-        }
-        if (_.startsWith(req.body.gitAuthType, 'ssh')) {
-          return exec.stdout('git', ['config', '--local', 'core.sshCommand', 'ssh -i "' + req.body.gitAuthSSHKey + '" -o StrictHostKeyChecking=no'], { cwd: gitDir }).then(result => {
-            return 'Git SSH Private Key path has been set successfully.'
-          })
-        } else {
-          return false
-        }
-      },
-      () => {
-        if (req.body.gitUseRemote === false) { return false }
-        return exec.stdout('git', ['remote', 'rm', 'origin'], { cwd: gitDir }).catch(err => {
-          if (_.includes(err.message, 'No such remote') || _.includes(err.message, 'Could not remove')) {
-            return true
-          } else {
-            throw err
-          }
-        }).then(() => {
-          return exec.stdout('git', ['remote', 'add', 'origin', gitRemoteUrl], { cwd: gitDir }).then(result => {
-            return 'Git Remote was added successfully.'
-          })
-        })
-      },
-      () => {
-        if (req.body.gitUseRemote === false) { return false }
-        return exec.stdout('git', ['pull', 'origin', req.body.gitBranch], { cwd: gitDir }).then(result => {
-          return 'Git Pull operation successful.'
-        })
-      }
-    ], step => { return step() }).then(results => {
-      return res.json({ ok: true, results: _.without(results, false) })
-    }).catch(err => {
-      let errMsg = (err.stderr) ? err.stderr.replace(/(error:|warning:|fatal:)/gi, '').replace(/ \s+/g, ' ') : err.message
-      res.json({ ok: false, error: errMsg })
-    })
-  })
-
   /**
    * Finalize
    */
@@ -263,13 +94,6 @@ module.exports = () => {
       let confRaw = await fs.readFileAsync(path.join(WIKI.ROOTPATH, 'config.yml'), 'utf8')
       let conf = yaml.safeLoad(confRaw)
 
-      conf.port = req.body.port
-      conf.paths.data = req.body.pathData
-      conf.paths.content = req.body.pathContent
-
-      confRaw = yaml.safeDump(conf)
-      await fs.writeFileAsync(path.join(WIKI.ROOTPATH, 'config.yml'), confRaw)
-
       // Create directory structure
       await fs.ensureDir(conf.paths.data)
       await fs.ensureDir(path.join(conf.paths.data, 'cache'))
@@ -283,16 +107,13 @@ module.exports = () => {
       _.set(WIKI.config, 'lang.autoUpdate', true)
       _.set(WIKI.config, 'lang.namespacing', false)
       _.set(WIKI.config, 'lang.namespaces', [])
-      _.set(WIKI.config, 'paths.content', req.body.pathContent)
-      _.set(WIKI.config, 'paths.data', req.body.pathData)
-      _.set(WIKI.config, 'port', req.body.port)
-      _.set(WIKI.config, 'public', req.body.public === 'true')
+      _.set(WIKI.config, 'public', false)
       _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
       _.set(WIKI.config, 'telemetry.isEnabled', req.body.telemetry === 'true')
       _.set(WIKI.config, 'telemetry.clientId', WIKI.telemetry.cid)
       _.set(WIKI.config, 'theming.theme', 'default')
       _.set(WIKI.config, 'theming.darkMode', false)
-      _.set(WIKI.config, 'title', req.body.title)
+      _.set(WIKI.config, 'title', 'Wiki.js')
 
       // Save config to DB
       WIKI.logger.info('Persisting config to DB...')
@@ -325,6 +146,9 @@ module.exports = () => {
       await WIKI.models.editors.refreshEditorsFromDisk()
       await WIKI.models.editors.query().patch({ isEnabled: true }).where('key', 'markdown')
 
+      // Load renderers
+      await WIKI.models.renderers.refreshRenderersFromDisk()
+
       // Load storage targets
       await WIKI.models.storage.refreshTargetsFromDisk()
 
@@ -367,7 +191,7 @@ module.exports = () => {
       WIKI.logger.info('Setup is complete!')
       res.json({
         ok: true,
-        redirectPath: WIKI.config.site.path,
+        redirectPath: '/',
         redirectPort: WIKI.config.port
       }).end()