2
0
Эх сурвалжийг харах

feat: Configurable ToC Heading Levels (#5101)

Co-authored-by: Regev Brody <regevbr@gmail.com>
Timo Kruth 3 жил өмнө
parent
commit
d9f4e90e2c

+ 27 - 3
client/components/admin/admin-theme.vue

@@ -1,5 +1,5 @@
 <template lang='pug'>
-  v-container(fluid, grid-list-lg)
+v-container(fluid, grid-list-lg)
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
@@ -51,7 +51,6 @@
                     persistent-hint
                     :hint='$t(`admin:theme.darkModeHint`)'
                     )
-
               v-card.mt-3.animated.fadeInUp.wait-p1s
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title.subtitle-1 {{$t(`admin:theme.options`)}}
@@ -68,7 +67,15 @@
                     hint='Select whether the table of contents is shown on the left, right or not at all.'
                     disabled
                     )
-
+                  v-range-slider(
+                    prepend-icon='mdi-serial-port'
+                    label='Heading Levels in ToC'
+                    hint='The table of contents will show headings from and up to the selected levels.'
+                    v-model='tocRange'
+                    :min='1'
+                    :max='6'
+                    :tick-labels='["H1", "H2", "H3", "H4", "H5", "H6"]'
+                  )
             v-flex(lg6 xs12)
               //- v-card.animated.fadeInUp.wait-p2s
               //-   v-toolbar(color='teal', dark, dense, flat)
@@ -154,6 +161,9 @@ export default {
       config: {
         theme: 'default',
         darkMode: false,
+        minTocLevel: 0,
+        tocLevel: 2,
+        tocCollapseLevel: 2,
         iconset: '',
         injectCSS: '',
         injectHead: '',
@@ -163,6 +173,17 @@ export default {
     }
   },
   computed: {
+    tocRange: {
+      get() {
+        var range = [this.config.minTocLevel, this.config.tocLevel]
+        return range
+      },
+      set(value) {
+        this.config.minTocLevel = value[0]
+        this.config.tocLevel = value[1]
+        this.config.tocCollapseLevel = value[1]
+      }
+    },
     darkMode: sync('site/dark'),
     headers() {
       return [
@@ -209,6 +230,9 @@ export default {
             theme: this.config.theme,
             iconset: this.config.iconset,
             darkMode: this.darkMode,
+            minTocLevel: parseInt(this.config.minTocLevel, 10),
+            tocLevel: parseInt(this.config.tocLevel, 10),
+            tocCollapseLevel: parseInt(this.config.tocCollapseLevel, 10),
             injectCSS: this.config.injectCSS,
             injectHead: this.config.injectHead,
             injectBody: this.config.injectBody

+ 53 - 3
client/components/editor.vue

@@ -144,6 +144,22 @@ export default {
       type: Number,
       default: 0
     },
+    minTocLevel: {
+      type: Number,
+      default: 0
+    },
+    tocLevel: {
+      type: Number,
+      default: 1
+    },
+    tocCollapseLevel: {
+      type: Number,
+      default: 0
+    },
+    doUseTocDefault: {
+      type: Boolean,
+      default: true
+    },
     checkoutDate: {
       type: String,
       default: new Date().toISOString()
@@ -190,6 +206,10 @@ export default {
         this.path !== this.$store.get('page/path'),
         this.savedState.title !== this.$store.get('page/title'),
         this.savedState.description !== this.$store.get('page/description'),
+        this.savedState.minTocLevel !== this.$store.get('page/minTocLevel'),
+        this.savedState.tocLevel !== this.$store.get('page/tocLevel'),
+        this.savedState.tocCollapseLevel !== this.$store.get('page/tocCollapseLevel'),
+        this.savedState.doUseTocDefault !== this.$store.get('page/doUseTocDefault'),
         this.savedState.tags !== this.$store.get('page/tags'),
         this.savedState.isPublished !== this.$store.get('page/isPublished'),
         this.savedState.publishStartDate !== this.$store.get('page/publishStartDate'),
@@ -223,7 +243,10 @@ export default {
     this.$store.set('page/title', this.title)
     this.$store.set('page/scriptCss', this.scriptCss)
     this.$store.set('page/scriptJs', this.scriptJs)
-
+    this.$store.set('page/minTocLevel', this.minTocLevel)
+    this.$store.set('page/tocLevel', this.tocLevel)
+    this.$store.set('page/tocCollapseLevel', this.tocCollapseLevel)
+    this.$store.set('page/doUseTocDefault', this.doUseTocDefault)
     this.$store.set('page/mode', 'edit')
 
     this.setCurrentSavedState()
@@ -303,6 +326,10 @@ export default {
                 $publishStartDate: Date
                 $scriptCss: String
                 $scriptJs: String
+                $minTocLevel: Int!
+                $tocLevel: Int!
+                $tocCollapseLevel: Int!
+                $doUseTocDefault: Boolean!
                 $tags: [String]!
                 $title: String!
               ) {
@@ -319,6 +346,10 @@ export default {
                     publishStartDate: $publishStartDate
                     scriptCss: $scriptCss
                     scriptJs: $scriptJs
+                    minTocLevel: $minTocLevel
+                    tocLevel: $tocLevel
+                    tocCollapseLevel: $tocCollapseLevel
+                    doUseTocDefault: $doUseTocDefault
                     tags: $tags
                     title: $title
                   ) {
@@ -348,6 +379,10 @@ export default {
               publishStartDate: this.$store.get('page/publishStartDate') || '',
               scriptCss: this.$store.get('page/scriptCss'),
               scriptJs: this.$store.get('page/scriptJs'),
+              minTocLevel: this.$store.get('page/minTocLevel'),
+              tocLevel: this.$store.get('page/tocLevel'),
+              tocCollapseLevel: this.$store.get('page/tocCollapseLevel'),
+              doUseTocDefault: this.$store.get('page/doUseTocDefault'),
               tags: this.$store.get('page/tags'),
               title: this.$store.get('page/title')
             }
@@ -391,7 +426,6 @@ export default {
             this.$root.$emit('saveConflict')
             throw new Error(this.$t('editor:conflict.warning'))
           }
-
           let resp = await this.$apollo.mutate({
             mutation: gql`
               mutation (
@@ -407,6 +441,10 @@ export default {
                 $publishStartDate: Date
                 $scriptCss: String
                 $scriptJs: String
+                $minTocLevel: Int
+                $tocLevel: Int
+                $tocCollapseLevel: Int
+                $doUseTocDefault: Boolean
                 $tags: [String]
                 $title: String
               ) {
@@ -424,6 +462,10 @@ export default {
                     publishStartDate: $publishStartDate
                     scriptCss: $scriptCss
                     scriptJs: $scriptJs
+                    minTocLevel: $minTocLevel
+                    tocLevel: $tocLevel
+                    tocCollapseLevel: $tocCollapseLevel
+                    doUseTocDefault: $doUseTocDefault
                     tags: $tags
                     title: $title
                   ) {
@@ -453,6 +495,10 @@ export default {
               publishStartDate: this.$store.get('page/publishStartDate') || '',
               scriptCss: this.$store.get('page/scriptCss'),
               scriptJs: this.$store.get('page/scriptJs'),
+              minTocLevel: this.$store.get('page/minTocLevel'),
+              tocLevel: this.$store.get('page/tocLevel'),
+              tocCollapseLevel: this.$store.get('page/tocCollapseLevel'),
+              doUseTocDefault: this.$store.get('page/doUseTocDefault'),
               tags: this.$store.get('page/tags'),
               title: this.$store.get('page/title')
             }
@@ -535,7 +581,11 @@ export default {
         tags: this.$store.get('page/tags'),
         title: this.$store.get('page/title'),
         css: this.$store.get('page/scriptCss'),
-        js: this.$store.get('page/scriptJs')
+        js: this.$store.get('page/scriptJs'),
+        minTocLevel: this.$store.get('page/minTocLevel'),
+        tocLevel: this.$store.get('page/tocLevel'),
+        tocCollapseLevel: this.$store.get('page/tocCollapseLevel'),
+        doUseTocDefault: this.$store.get('page/doUseTocDefault')
       }
     },
     injectCustomCss: _.debounce(css => {

+ 36 - 5
client/components/editor/editor-modal-properties.vue

@@ -1,5 +1,5 @@
 <template lang='pug'>
-  v-dialog(
+v-dialog(
     v-model='isShown'
     persistent
     width='1000'
@@ -67,6 +67,23 @@
                     :rules='[rules.required, rules.path]'
                     )
           v-divider
+          v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d3` : `lighten-5`')
+            .overline.pb-5 Theme Options
+            v-switch(
+              label='Use Site Defaults'
+              v-model='doUseTocDefault'
+            )
+            v-range-slider(
+              :disabled='doUseTocDefault'
+              prepend-icon='mdi-serial-port'
+              label='Heading Levels in ToC'
+              hint='The table of contents will show headings from and up to the selected levels.'
+              v-model='tocRange'
+              :min='1'
+              :max='6'
+              :tick-labels='["H1", "H2", "H3", "H4", "H5", "H6"]'
+              )
+          v-divider
           v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-4`')
             .overline.pb-5 {{$t('editor:props.categorization')}}
             v-chip-group.radius-5.mb-5(column, v-if='tags && tags.length > 0')
@@ -255,6 +272,7 @@ import 'codemirror/mode/htmlmixed/htmlmixed.js'
 import 'codemirror/mode/css/css.js'
 
 /* global siteLangs, siteConfig */
+// eslint-disable-next-line no-useless-escape
 const filenamePattern = /^(?![\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s])(?!.*[\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]$)[^\#\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]*$/
 
 export default {
@@ -276,10 +294,10 @@ export default {
       currentTab: 0,
       cm: null,
       rules: {
-          required: value => !!value || 'This field is required.',
-          path: value => {
-            return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
-          }
+        required: value => !!value || 'This field is required.',
+        path: value => {
+          return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
+        }
       }
     }
   },
@@ -297,6 +315,19 @@ export default {
     isPublished: sync('page/isPublished'),
     publishStartDate: sync('page/publishStartDate'),
     publishEndDate: sync('page/publishEndDate'),
+    tocRange: {
+      get() {
+        var range = [this.$store.get('page/minTocLevel'), this.$store.get('page/tocLevel')]
+        return range
+        // return [get('page/minTocLevel'), get('page/tocLevel')]
+      },
+      set(value) {
+        this.$store.set('page/minTocLevel', value[0])
+        this.$store.set('page/tocLevel', value[1])
+        this.$store.set('page/tocCollapseLevel', value[1])
+      }
+    },
+    doUseTocDefault: sync('page/doUseTocDefault'),
     scriptJs: sync('page/scriptJs'),
     scriptCss: sync('page/scriptCss'),
     hasScriptPermission: get('page/effectivePermissions@pages.script'),

+ 2 - 2
client/graph/admin/theme/theme-mutation-save.gql

@@ -1,6 +1,6 @@
-mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $injectCSS: String, $injectHead: String, $injectBody: String) {
+mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $minTocLevel: Int!, $tocLevel: Int!, $tocCollapseLevel: Int!, $injectCSS: String, $injectHead: String, $injectBody: String) {
   theming {
-    setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
+    setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, minTocLevel: $minTocLevel, tocLevel: $tocLevel, tocCollapseLevel: $tocCollapseLevel, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
       responseResult {
         succeeded
         errorCode

+ 3 - 0
client/graph/admin/theme/theme-query-config.gql

@@ -4,6 +4,9 @@ query {
       theme
       iconset
       darkMode
+      minTocLevel
+      tocLevel
+      tocCollapseLevel
       injectCSS
       injectHead
       injectBody

+ 4 - 0
client/store/page.js

@@ -17,6 +17,10 @@ const state = {
   editor: '',
   mode: '',
   scriptJs: '',
+  minTocLevel: 0,
+  tocLevel: 2,
+  tocCollapseLevel: 2,
+  doUseTocDefault: true,
   scriptCss: '',
   effectivePermissions: {
     comments: {

+ 81 - 0
client/themes/default/components/page-toc-item.vue

@@ -0,0 +1,81 @@
+<template lang="pug">
+  div
+    template(v-if='level >= minTocLevel')
+      v-list-item(@click='click(item.anchor)', v-if='(item.children.length === 0 && tocCollapseLevel > level) || tocCollapseLevel > level',
+        :key='item.anchor', :class='isNestedLevel ? `pl-9` : `pl-6`')
+        v-icon.pl-0(small, color='grey lighten-1') {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}
+        v-list-item-title.pl-4(v-bind:class='titleClasses') {{item.title}}
+      v-list-group(sub-group, v-else, v-bind:class='{"pl-3": isNestedLevel}')
+        template(v-slot:activator)
+          v-list-item.pl-0(@click='click(item.anchor)', :key='item.anchor')
+            v-list-item-title(v-bind:class='titleClasses') {{item.title}}
+        template(v-if='item.children.length !== 0', v-for='subItem in item.children')
+          page-toc-item(:item='subItem', :level='level + 1', :tocLevel='tocLevel', :minTocLevel='minTocLevel', :tocCollapseLevel='tocCollapseLevel')
+      template(v-if='tocCollapseLevel > level', v-for='subItem in item.children')
+        page-toc-item(:item='subItem', :level='level + 1', :tocLevel='tocLevel', :minTocLevel='minTocLevel', :tocCollapseLevel='tocCollapseLevel')
+    template(v-else, v-for='subItem in item.children')
+      page-toc-item(:item='subItem', :level='level + 1', :tocLevel='tocLevel', :minTocLevel='minTocLevel', :tocCollapseLevel='tocCollapseLevel')
+</template>
+
+<script>
+
+export default {
+  name: 'PageTocItem',
+  props: {
+    item: {
+      type: Object,
+      default: () => {}
+    },
+    minTocLevel: {
+      type: Number,
+      default: 0
+    },
+    tocLevel: {
+      type: Number,
+      default: 2
+    },
+    tocCollapseLevel: {
+      type: Number,
+      default: 2
+    },
+    level: {
+      type: Number,
+      default: 1
+    }
+  },
+  data() {
+    return {
+      scrollOpts: {
+        duration: 1500,
+        offset: 0,
+        easing: 'easeInOutCubic'
+      }
+    }
+  },
+  computed: {
+    isNestedLevel() {
+      return this.level > this.minTocLevel
+    },
+    titleClasses() {
+      return {
+        'caption': this.isNestedLevel,
+        'grey--text': this.isNestedLevel,
+        'text--lighten-1': this.$vuetify.theme.dark && this.isNestedLevel,
+        'text--darken-1': !this.$vuetify.theme.dark && this.isNestedLevel
+      }
+    }
+  },
+  methods: {
+    click (anchor) {
+      this.$vuetify.goTo(anchor, this.scrollOpts)
+    }
+  }
+}
+</script>
+
+<style lang='scss'>
+// Hack to fix animations of multi level nesting v-list-group
+.v-list-group--sub-group.v-list-group--active .v-list-item:not(.v-list-item--active) .v-list-item__icon.v-list-group__header__prepend-icon .v-icon {
+  transform: rotate(0deg)!important;
+}
+</style>

+ 22 - 12
client/themes/default/components/page.vue

@@ -59,18 +59,9 @@
           v-flex.page-col-sd(lg3, xl2, v-if='$vuetify.breakpoint.lgAndUp')
             v-card.mb-5(v-if='tocDecoded.length')
               .overline.pa-5.pb-0(:class='$vuetify.theme.dark ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}}
-              v-list.pb-3(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``')
-                template(v-for='(tocItem, tocIdx) in tocDecoded')
-                  v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)')
-                    v-icon(color='grey', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}
-                    v-list-item-title.px-3 {{tocItem.title}}
-                  //- v-divider(v-if='tocIdx < toc.length - 1 || tocItem.children.length')
-                  template(v-for='tocSubItem in tocItem.children')
-                    v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)')
-                      v-icon.px-3(color='grey lighten-1', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}
-                      v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
-                    //- v-divider(inset, v-if='tocIdx < toc.length - 1')
-
+              v-list.py-0(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``')
+                template(v-for='item in tocDecoded')
+                  page-toc-item(:item='item', :tocLevel='tocLevel', :minTocLevel='minTocLevel', :tocCollapseLevel='tocCollapseLevel')
             v-card.mb-5(v-if='tags.length > 0')
               .pa-5
                 .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}}
@@ -319,6 +310,7 @@
 import { StatusIndicator } from 'vue-status-indicator'
 import Tabset from './tabset.vue'
 import NavSidebar from './nav-sidebar.vue'
+import PageTocItem from './page-toc-item.vue'
 import Prism from 'prismjs'
 import mermaid from 'mermaid'
 import { get, sync } from 'vuex-pathify'
@@ -366,6 +358,7 @@ Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
 export default {
   components: {
     NavSidebar,
+    PageTocItem,
     StatusIndicator
   },
   props: {
@@ -440,6 +433,22 @@ export default {
     commentsExternal: {
       type: Boolean,
       default: false
+    },
+    minTocLevel: {
+      type: Number,
+      default: 0
+    },
+    tocLevel: {
+      type: Number,
+      default: 2
+    },
+    tocCollapseLevel: {
+      type: Number,
+      default: 2
+    },
+    doUseTocDefault: {
+      type: Boolean,
+      default: true
     }
   },
   data() {
@@ -515,6 +524,7 @@ export default {
     hasDeletePagesPermission: get('page/effectivePermissions@pages.delete'),
     hasReadSourcePermission: get('page/effectivePermissions@source.read'),
     hasReadHistoryPermission: get('page/effectivePermissions@history.read'),
+
     hasAnyPagePermissions () {
       return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission ||
         this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission

+ 1 - 1
dev/containers/Dockerfile

@@ -5,7 +5,7 @@ FROM node:14
 LABEL maintainer "requarks.io"
 
 RUN apt-get update && \
-    apt-get install -y bash curl git python make g++ nano openssh-server gnupg && \
+    apt-get install -y bash curl git python make g++ nano openssh-server gnupg cmake && \
     mkdir -p /wiki
 
 WORKDIR /wiki

+ 4 - 0
server/app/data.yml

@@ -55,6 +55,10 @@ defaults:
       theme: 'default'
       iconset: 'md'
       darkMode: false
+      minTocLevel: 0
+      tocLevel: 2
+      tocCollapseLevel: 2
+      doUseTocDefault: true
     auth:
       autoLogin: false
       enforce2FA: false

+ 21 - 0
server/controllers/common.js

@@ -504,6 +504,19 @@ router.get('/*', async (req, res, next) => {
         if (!_.isEmpty(page.extra.js)) {
           injectCode.body = `${injectCode.body}\n${page.extra.js}`
         }
+        const doUseTocDefault = page.doUseTocDefault === true || page.doUseTocDefault === 1
+        var tocLevel
+        var tocCollapseLevel
+        var minTocLevel
+        if (doUseTocDefault) {
+          minTocLevel = WIKI.config.theming.minTocLevel
+          tocLevel = WIKI.config.theming.tocLevel
+          tocCollapseLevel = WIKI.config.theming.tocCollapseLevel
+        } else {
+          minTocLevel = page.minTocLevel || WIKI.config.theming.minTocLevel
+          tocLevel = page.tocLevel || WIKI.config.theming.tocLevel
+          tocCollapseLevel = page.tocCollapseLevel || WIKI.config.theming.tocCollapseLevel
+        }
 
         if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
           // -> Convert page TOC
@@ -515,6 +528,10 @@ router.get('/*', async (req, res, next) => {
           res.render('legacy/page', {
             page,
             sidebar,
+            minTocLevel,
+            tocLevel,
+            tocCollapseLevel,
+            doUseTocDefault,
             injectCode,
             isAuthenticated: req.user && req.user.id !== 2
           })
@@ -546,6 +563,10 @@ router.get('/*', async (req, res, next) => {
           res.render('page', {
             page,
             sidebar,
+            minTocLevel,
+            tocLevel,
+            tocCollapseLevel,
+            doUseTocDefault,
             injectCode,
             comments: commentTmpl,
             effectivePermissions

+ 11 - 0
server/db/migrations-sqlite/2.5.13.js

@@ -0,0 +1,11 @@
+exports.up = async knex => {
+  await knex.schema
+    .alterTable('pages', table => {
+      table.integer('minTocLevel').notNullable().defaultTo(0)
+      table.integer('tocLevel').notNullable().defaultTo(0)
+      table.integer('tocCollapseLevel').notNullable().defaultTo(0)
+      table.boolean('doUseTocDefault').notNullable().defaultTo(true)
+    })
+}
+
+exports.down = knex => { }

+ 11 - 0
server/db/migrations/2.5.13.js

@@ -0,0 +1,11 @@
+exports.up = async knex => {
+  await knex.schema
+    .alterTable('pages', table => {
+      table.integer('minTocLevel').notNullable().defaultTo(0)
+      table.integer('tocLevel').notNullable().defaultTo(0)
+      table.integer('tocCollapseLevel').notNullable().defaultTo(0)
+      table.boolean('doUseTocDefault').notNullable().defaultTo(true)
+    })
+}
+
+exports.down = knex => { }

+ 6 - 0
server/graph/resolvers/theming.js

@@ -24,6 +24,9 @@ module.exports = {
         theme: WIKI.config.theming.theme,
         iconset: WIKI.config.theming.iconset,
         darkMode: WIKI.config.theming.darkMode,
+        minTocLevel: WIKI.config.theming.minTocLevel,
+        tocLevel: WIKI.config.theming.tocLevel,
+        tocCollapseLevel: WIKI.config.theming.tocCollapseLevel,
         injectCSS: new CleanCSS({ format: 'beautify' }).minify(WIKI.config.theming.injectCSS).styles,
         injectHead: WIKI.config.theming.injectHead,
         injectBody: WIKI.config.theming.injectBody
@@ -44,6 +47,9 @@ module.exports = {
           theme: args.theme,
           iconset: args.iconset,
           darkMode: args.darkMode,
+          minTocLevel: args.minTocLevel,
+          tocLevel: args.tocLevel,
+          tocCollapseLevel: args.tocCollapseLevel,
           injectCSS: args.injectCSS || '',
           injectHead: args.injectHead || '',
           injectBody: args.injectBody || ''

+ 12 - 0
server/graph/schemas/page.graphql

@@ -93,6 +93,10 @@ type PageMutation {
     scriptJs: String
     tags: [String]!
     title: String!
+    minTocLevel: Int
+    tocLevel: Int
+    tocCollapseLevel: Int
+    doUseTocDefault: Boolean
   ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
 
   update(
@@ -110,6 +114,10 @@ type PageMutation {
     scriptJs: String
     tags: [String]
     title: String
+    minTocLevel: Int
+    tocLevel: Int
+    tocCollapseLevel: Int
+    doUseTocDefault: Boolean
   ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
 
   convert(
@@ -189,6 +197,10 @@ type Page {
   content: String! @auth(requires: ["read:source", "write:pages", "manage:system"])
   render: String
   toc: String
+  minTocLevel: Int!
+  tocLevel: Int!
+  tocCollapseLevel: Int!
+  doUseTocDefault: Boolean!
   contentType: String!
   createdAt: Date!
   updatedAt: Date!

+ 6 - 0
server/graph/schemas/theming.graphql

@@ -28,6 +28,9 @@ type ThemingMutation {
     theme: String!
     iconset: String!
     darkMode: Boolean!
+    minTocLevel: Int!
+    tocLevel: Int!
+    tocCollapseLevel: Int!
     injectCSS: String
     injectHead: String
     injectBody: String
@@ -42,6 +45,9 @@ type ThemingConfig {
   theme: String!
   iconset: String!
   darkMode: Boolean!
+  minTocLevel: Int!
+  tocLevel: Int!
+  tocCollapseLevel: Int!
   injectCSS: String
   injectHead: String
   injectBody: String

+ 11 - 1
server/helpers/page.js

@@ -81,8 +81,18 @@ module.exports = {
       ['date', page.updatedAt],
       ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
       ['editor', page.editorKey],
-      ['dateCreated', page.createdAt]
+      ['dateCreated', page.createdAt],
+      ['doUseTocDefault', page.doUseTocDefault]
     ]
+    if (page.minTocLevel) {
+      meta.push(['minTocLevel', page.minTocLevel])
+    }
+    if (page.tocLevel) {
+      meta.push(['tocLevel', page.tocLevel])
+    }
+    if (page.tocCollapseLevel) {
+      meta.push(['tocCollapseLevel', page.tocCollapseLevel])
+    }
     switch (page.contentType) {
       case 'markdown':
         return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content

+ 24 - 1
server/models/pages.js

@@ -47,7 +47,10 @@ module.exports = class Page extends Model {
         publishEndDate: {type: 'string'},
         content: {type: 'string'},
         contentType: {type: 'string'},
-
+        minTocLevel: {type: 'integer'},
+        tocLevel: {type: 'integer'},
+        tocCollapseLevel: {type: 'integer'},
+        doUseTocDefault: {type: 'boolean'},
         createdAt: {type: 'string'},
         updatedAt: {type: 'string'}
       }
@@ -161,6 +164,10 @@ module.exports = class Page extends Model {
       },
       title: 'string',
       toc: 'string',
+      minTocLevel: 'uint',
+      tocLevel: 'uint',
+      tocCollapseLevel: 'uint',
+      doUseTocDefault: 'boolean',
       updatedAt: 'string'
     })
   }
@@ -311,6 +318,10 @@ module.exports = class Page extends Model {
       publishStartDate: opts.publishStartDate || '',
       title: opts.title,
       toc: '[]',
+      minTocLevel: opts.minTocLevel || 0,
+      tocLevel: opts.tocLevel || 1,
+      tocCollapseLevel: opts.tocCollapseLevel || 0,
+      doUseTocDefault: opts.doUseTocDefault || true,
       extra: JSON.stringify({
         js: scriptJs,
         css: scriptCss
@@ -430,6 +441,10 @@ module.exports = class Page extends Model {
       publishEndDate: opts.publishEndDate || '',
       publishStartDate: opts.publishStartDate || '',
       title: opts.title,
+      minTocLevel: opts.minTocLevel || 0,
+      tocLevel: opts.tocLevel || 1,
+      tocCollapseLevel: opts.tocCollapseLevel || 0,
+      doUseTocDefault: opts.doUseTocDefault === true || opts.doUseTocDefault === 1,
       extra: JSON.stringify({
         ...ogPage.extra,
         js: scriptJs,
@@ -991,6 +1006,10 @@ module.exports = class Page extends Model {
           'pages.content',
           'pages.render',
           'pages.toc',
+          'pages.minTocLevel',
+          'pages.tocLevel',
+          'pages.tocCollapseLevel',
+          'pages.doUseTocDefault',
           'pages.contentType',
           'pages.createdAt',
           'pages.updatedAt',
@@ -1071,6 +1090,10 @@ module.exports = class Page extends Model {
       tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),
       title: page.title,
       toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
+      minTocLevel: page.minTocLevel,
+      tocLevel: page.tocLevel,
+      tocCollapseLevel: page.tocCollapseLevel,
+      doUseTocDefault: page.doUseTocDefault,
       updatedAt: page.updatedAt
     }))
   }

+ 8 - 1
server/modules/storage/disk/common.js

@@ -92,6 +92,10 @@ module.exports = {
         isPublished: _.get(pageData, 'isPublished', currentPage.isPublished),
         isPrivate: false,
         content: pageData.content,
+        minTocLevel: pageData.minTocLevel,
+        tocLevel: pageData.tocLevel,
+        tocCollapseLevel: pageData.tocCollapseLevel,
+        doUseTocDefault: pageData.doUseTocDefault,
         user: user,
         skipStorage: true
       })
@@ -110,7 +114,10 @@ module.exports = {
         content: pageData.content,
         user: user,
         editor: pageEditor,
-        skipStorage: true
+        skipStorage: true,
+        tocLevel: pageData.tocLevel,
+        tocCollapseLevel: pageData.tocCollapseLevel,
+        doUseTocDefault: pageData.doUseTocDefault
       })
     }
   },

+ 4 - 0
server/setup.js

@@ -126,6 +126,10 @@ module.exports = () => {
       _.set(WIKI.config, 'theming', {
         theme: 'default',
         darkMode: false,
+        minTocLevel: 0,
+        tocLevel: 2,
+        tocCollapseLevel: 2,
+        doUseTocDefault: true,
         iconset: 'mdi',
         injectCSS: '',
         injectHead: '',

+ 4 - 0
server/views/editor.pug

@@ -19,6 +19,10 @@ block body
       script-css=page.extra.css
       script-js=page.extra.js
       init-mode=page.mode
+      :min-toc-level=page.minTocLevel
+      :toc-level=page.tocLevel
+      :toc-collapse-level=page.tocCollapseLevel
+      :do-use-toc-default=page.doUseTocDefault.toString()
       init-editor=page.editorKey
       init-content=page.content
       checkout-date=page.updatedAt

+ 4 - 0
server/views/page.pug

@@ -29,6 +29,10 @@ block body
       comments-enabled=config.features.featurePageComments
       effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')
       comments-external=comments.codeTemplate
+      :min-toc-level=minTocLevel
+      :toc-level=tocLevel
+      :toc-collapse-level=tocCollapseLevel
+      :do-use-toc-default=doUseTocDefault.toString()
       )
       template(slot='contents')
         div!= page.render