Преглед изворни кода

Merge pull request #4272 from weseek/fix/update-lsx

fix: Update lsx (had broken by lerna import miscue)
Yuki Takei пре 4 година
родитељ
комит
c8f7f1c768

+ 2 - 0
packages/plugin-lsx/src/client-entry.js

@@ -1,9 +1,11 @@
+import { LsxLogoutInterceptor } from './client/js/util/Interceptor/LsxLogoutInterceptor';
 import { LsxPreRenderInterceptor } from './client/js/util/Interceptor/LsxPreRenderInterceptor';
 import { LsxPreRenderInterceptor } from './client/js/util/Interceptor/LsxPreRenderInterceptor';
 import { LsxPostRenderInterceptor } from './client/js/util/Interceptor/LsxPostRenderInterceptor';
 import { LsxPostRenderInterceptor } from './client/js/util/Interceptor/LsxPostRenderInterceptor';
 
 
 export default (appContainer) => {
 export default (appContainer) => {
   // add interceptors
   // add interceptors
   appContainer.interceptorManager.addInterceptors([
   appContainer.interceptorManager.addInterceptors([
+    new LsxLogoutInterceptor(),
     new LsxPreRenderInterceptor(),
     new LsxPreRenderInterceptor(),
     new LsxPostRenderInterceptor(appContainer),
     new LsxPostRenderInterceptor(appContainer),
   ]);
   ]);

+ 7 - 2
packages/plugin-lsx/src/client/css/index.css

@@ -1,6 +1,7 @@
 .lsx .page-list-ul > li > a:not(:hover) {
 .lsx .page-list-ul > li > a:not(:hover) {
   text-decoration: none;
   text-decoration: none;
 }
 }
+
 .lsx .lsx-page-not-exist {
 .lsx .lsx-page-not-exist {
   opacity: 0.6;
   opacity: 0.6;
 }
 }
@@ -10,6 +11,10 @@
 }
 }
 
 
 @keyframes lsx-fadeIn {
 @keyframes lsx-fadeIn {
-  0% {opacity: .2}
-  100% {opacity: .9}
+  0% {
+    opacity: 0.2;
+  }
+  100% {
+    opacity: 0.9;
+  }
 }
 }

+ 76 - 46
packages/plugin-lsx/src/client/js/components/Lsx.jsx

@@ -9,64 +9,86 @@ import { pathUtils } from 'growi-commons';
 import styles from '../../css/index.css';
 import styles from '../../css/index.css';
 
 
 import { LsxContext } from '../util/LsxContext';
 import { LsxContext } from '../util/LsxContext';
-import { LsxCacheHelper } from '../util/LsxCacheHelper';
+import { TagCacheManagerFactory } from '../util/TagCacheManagerFactory';
 import { PageNode } from './PageNode';
 import { PageNode } from './PageNode';
 import { LsxListView } from './LsxPageList/LsxListView';
 import { LsxListView } from './LsxPageList/LsxListView';
 
 
-
 export class Lsx extends React.Component {
 export class Lsx extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
-      isLoading: true,
+      isLoading: false,
       isError: false,
       isError: false,
+      isCacheExists: false,
       nodeTree: undefined,
       nodeTree: undefined,
       errorMessage: '',
       errorMessage: '',
     };
     };
+
+    this.tagCacheManager = TagCacheManagerFactory.getInstance();
   }
   }
 
 
-  componentWillMount() {
-    const lsxContext = this.props.lsxContext;
-    lsxContext.parse();
+  async componentWillMount() {
+    const { lsxContext, forceToFetchData } = this.props;
 
 
-    // check cache exists
-    if (this.props.lsxStateCache) {
+    // get state object cache
+    const stateCache = this.retrieveDataFromCache();
+
+    if (stateCache != null) {
       this.setState({
       this.setState({
-        isLoading: false,
-        nodeTree: this.props.lsxStateCache.nodeTree,
-        isError: this.props.lsxStateCache.isError,
-        errorMessage: this.props.lsxStateCache.errorMessage,
+        isCacheExists: true,
+        nodeTree: stateCache.nodeTree,
+        isError: stateCache.isError,
+        errorMessage: stateCache.errorMessage,
       });
       });
-      return; // go to render()
+
+      // switch behavior by forceToFetchData
+      if (!forceToFetchData) {
+        return; // go to render()
+      }
     }
     }
 
 
+    lsxContext.parse();
+    this.setState({ isLoading: true });
+
     // add slash ensure not to forward match to another page
     // add slash ensure not to forward match to another page
     // ex: '/Java/' not to match to '/JavaScript'
     // ex: '/Java/' not to match to '/JavaScript'
     const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
     const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
 
 
-    this.props.appContainer.apiGet('/plugins/lsx', { pagePath, options: lsxContext.options })
-      .then((res) => {
-        if (res.ok) {
-          const nodeTree = this.generatePageNodeTree(pagePath, res.pages);
-          this.setState({ nodeTree });
-        }
-        else {
-          return Promise.reject(res.error);
-        }
-      })
-      .catch((error) => {
-        this.setState({ isError: true, errorMessage: error.message });
-      })
-      // finally
-      .then(() => {
-        this.setState({ isLoading: false });
-
-        // store to sessionStorage
-        const cacheKey = LsxCacheHelper.generateCacheKeyFromContext(lsxContext);
-        LsxCacheHelper.cacheState(cacheKey, this.state);
+    try {
+      const res = await this.props.appContainer.apiGet('/plugins/lsx', { pagePath, options: lsxContext.options });
+
+      if (res.ok) {
+        const nodeTree = this.generatePageNodeTree(pagePath, res.pages);
+        this.setState({ nodeTree });
+      }
+    }
+    catch (error) {
+      this.setState({ isError: true, errorMessage: error.message });
+    }
+    finally {
+      this.setState({ isLoading: false });
+
+      // store to sessionStorage
+      this.tagCacheManager.cacheState(lsxContext, this.state);
+    }
+  }
+
+  retrieveDataFromCache() {
+    const { lsxContext } = this.props;
+
+    // get state object cache
+    const stateCache = this.tagCacheManager.getStateCache(lsxContext);
+
+    // instanciate PageNode
+    if (stateCache != null && stateCache.nodeTree != null) {
+      stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
+        return PageNode.instanciateFrom(obj);
       });
       });
+    }
+
+    return stateCache;
   }
   }
 
 
   /**
   /**
@@ -170,16 +192,11 @@ export class Lsx extends React.Component {
 
 
   renderContents() {
   renderContents() {
     const lsxContext = this.props.lsxContext;
     const lsxContext = this.props.lsxContext;
+    const {
+      isLoading, isError, isCacheExists, nodeTree,
+    } = this.state;
 
 
-    if (this.state.isLoading) {
-      return (
-        <div className="text-muted">
-          <i className="fa fa-spinner fa-pulse mr-1"></i>
-          <span className="lsx-blink">{lsxContext.tagExpression}</span>
-        </div>
-      );
-    }
-    if (this.state.isError) {
+    if (isError) {
       return (
       return (
         <div className="text-warning">
         <div className="text-warning">
           <i className="fa fa-exclamation-triangle fa-fw"></i>
           <i className="fa fa-exclamation-triangle fa-fw"></i>
@@ -187,9 +204,22 @@ export class Lsx extends React.Component {
         </div>
         </div>
       );
       );
     }
     }
-    // render tree
 
 
-    return <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} />;
+
+    return (
+      <div className={isLoading ? 'lsx-blink' : ''}>
+        { isLoading && (
+          <div className="text-muted">
+            <i className="fa fa-spinner fa-pulse mr-1"></i>
+            {lsxContext.tagExpression}
+            { isCacheExists && <small>&nbsp;(Showing cache..)</small> }
+          </div>
+        ) }
+        { nodeTree && (
+          <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} />
+        ) }
+      </div>
+    );
 
 
   }
   }
 
 
@@ -201,7 +231,7 @@ export class Lsx extends React.Component {
 
 
 Lsx.propTypes = {
 Lsx.propTypes = {
   appContainer: PropTypes.object.isRequired,
   appContainer: PropTypes.object.isRequired,
-
   lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
   lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
-  lsxStateCache: PropTypes.object,
+
+  forceToFetchData: PropTypes.bool,
 };
 };

+ 10 - 6
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxListView.jsx

@@ -10,7 +10,9 @@ export class LsxListView extends React.Component {
   render() {
   render() {
     const listView = this.props.nodeTree.map((pageNode) => {
     const listView = this.props.nodeTree.map((pageNode) => {
       return (
       return (
-        <LsxPage key={pageNode.pagePath} depth={1}
+        <LsxPage
+          key={pageNode.pagePath}
+          depth={1}
           pageNode={pageNode}
           pageNode={pageNode}
           lsxContext={this.props.lsxContext}
           lsxContext={this.props.lsxContext}
         />
         />
@@ -19,12 +21,14 @@ export class LsxListView extends React.Component {
 
 
     // no contents
     // no contents
     if (this.props.nodeTree.length === 0) {
     if (this.props.nodeTree.length === 0) {
-      return <div className="text-muted">
-        <small>
-          <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
+      return (
+        <div className="text-muted">
+          <small>
+            <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
             $lsx(<a href={this.props.lsxContext.pagePath}>{this.props.lsxContext.pagePath}</a>) has no contents
             $lsx(<a href={this.props.lsxContext.pagePath}>{this.props.lsxContext.pagePath}</a>) has no contents
-        </small>
-      </div>;
+          </small>
+        </div>
+      );
     }
     }
 
 
     return (
     return (

+ 3 - 1
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx

@@ -57,7 +57,9 @@ export class LsxPage extends React.Component {
     if (this.state.hasChildren) {
     if (this.state.hasChildren) {
       const pages = pageNode.children.map((pageNode) => {
       const pages = pageNode.children.map((pageNode) => {
         return (
         return (
-          <LsxPage key={pageNode.pagePath} depth={this.props.depth + 1}
+          <LsxPage
+            key={pageNode.pagePath}
+            depth={this.props.depth + 1}
             pageNode={pageNode}
             pageNode={pageNode}
             lsxContext={this.props.lsxContext}
             lsxContext={this.props.lsxContext}
           />
           />

+ 1 - 0
packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx

@@ -22,6 +22,7 @@ export class PagePathWrapper extends React.Component {
 PagePathWrapper.propTypes = {
 PagePathWrapper.propTypes = {
   pagePath: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   isExists: PropTypes.bool.isRequired,
   isExists: PropTypes.bool.isRequired,
+  excludePathString: PropTypes.string,
 };
 };
 
 
 PagePathWrapper.defaultProps = {
 PagePathWrapper.defaultProps = {

+ 33 - 0
packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js

@@ -0,0 +1,33 @@
+import { BasicInterceptor } from 'growi-commons';
+
+import { TagCacheManagerFactory } from '../TagCacheManagerFactory';
+
+/**
+ * The interceptor for lsx
+ *
+ *  replace lsx tag to a React target element
+ */
+export class LsxLogoutInterceptor extends BasicInterceptor {
+
+  /**
+   * @inheritdoc
+   */
+  isInterceptWhen(contextName) {
+    return (
+      contextName === 'logout'
+    );
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async process(contextName, ...args) {
+    const context = Object.assign(args[0]); // clone
+
+    TagCacheManagerFactory.getInstance().clearAllStateCaches();
+
+    // resolve
+    return context;
+  }
+
+}

+ 12 - 17
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js

@@ -3,8 +3,8 @@ import ReactDOM from 'react-dom';
 
 
 import { BasicInterceptor } from 'growi-commons';
 import { BasicInterceptor } from 'growi-commons';
 
 
+import { LsxContext } from '../LsxContext';
 import { Lsx } from '../../components/Lsx';
 import { Lsx } from '../../components/Lsx';
-import { LsxCacheHelper } from '../LsxCacheHelper';
 
 
 /**
 /**
  * The interceptor for lsx
  * The interceptor for lsx
@@ -31,35 +31,30 @@ export class LsxPostRenderInterceptor extends BasicInterceptor {
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  process(contextName, ...args) {
+  async process(contextName, ...args) {
     const context = Object.assign(args[0]); // clone
     const context = Object.assign(args[0]); // clone
 
 
-    if (context.lsxContextMap == null) {
-      return Promise.resolve();
-    }
+    const isPreview = (contextName === 'postRenderPreviewHtml');
 
 
     // forEach keys of lsxContextMap
     // forEach keys of lsxContextMap
-    Object.keys(context.lsxContextMap).forEach((renderId) => {
-      const elem = document.getElementById(renderId);
+    Object.keys(context.lsxContextMap).forEach((domId) => {
+      const elem = document.getElementById(domId);
 
 
       if (elem) {
       if (elem) {
-        // get LsxContext instance from context
-        const lsxContext = context.lsxContextMap[renderId];
+        // instanciate LsxContext from context
+        const lsxContext = new LsxContext(context.lsxContextMap[domId] || {});
+        lsxContext.fromPagePath = context.currentPagePath;
 
 
-        // check cache exists
-        const cacheKey = LsxCacheHelper.generateCacheKeyFromContext(lsxContext);
-        const lsxStateCache = LsxCacheHelper.getStateCache(cacheKey);
-
-        this.renderReactDOM(lsxContext, lsxStateCache, elem);
+        this.renderReactDOM(lsxContext, elem, isPreview);
       }
       }
     });
     });
 
 
-    return Promise.resolve();
+    return;
   }
   }
 
 
-  renderReactDOM(lsxContext, lsxStateCache, elem) {
+  renderReactDOM(lsxContext, elem, isPreview) {
     ReactDOM.render(
     ReactDOM.render(
-      <Lsx appContainer={this.appContainer} lsxContext={lsxContext} lsxStateCache={lsxStateCache} />,
+      <Lsx appContainer={this.appContainer} lsxContext={lsxContext} forceToFetchData={!isPreview} />,
       elem,
       elem,
     );
     );
   }
   }

+ 33 - 62
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js

@@ -1,7 +1,5 @@
-import { BasicInterceptor } from 'growi-commons';
-
-import { LsxContext } from '../LsxContext';
-import { LsxCacheHelper } from '../LsxCacheHelper';
+import ReactDOM from 'react-dom';
+import { customTagUtils, BasicInterceptor } from 'growi-commons';
 
 
 /**
 /**
  * The interceptor for lsx
  * The interceptor for lsx
@@ -10,6 +8,12 @@ import { LsxCacheHelper } from '../LsxCacheHelper';
  */
  */
 export class LsxPreRenderInterceptor extends BasicInterceptor {
 export class LsxPreRenderInterceptor extends BasicInterceptor {
 
 
+  constructor() {
+    super();
+
+    this.previousPreviewContext = null;
+  }
+
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
@@ -23,75 +27,42 @@ export class LsxPreRenderInterceptor extends BasicInterceptor {
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  process(contextName, ...args) {
+  isProcessableParallel() {
+    return false;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async process(contextName, ...args) {
     const context = Object.assign(args[0]); // clone
     const context = Object.assign(args[0]); // clone
     const parsedHTML = context.parsedHTML;
     const parsedHTML = context.parsedHTML;
-    const currentPagePath = context.currentPagePath;
-    this.initializeCache(contextName);
-
-    context.lsxContextMap = {};
-
-    // TODO retrieve from args for interceptor
-    const fromPagePath = currentPagePath;
 
 
-    // see: https://regex101.com/r/NQq3s9/7
-    const pattern = /\$lsx(\((.*?)\)(?=\s|<br>|\$lsx))|\$lsx(\((.*)\)(?!\s|<br>|\$lsx))/g;
-    context.parsedHTML = parsedHTML.replace(pattern, (all, group1, group2, group3, group4) => {
-      const tagExpression = all;
-      let lsxArgs = group2 || group4 || '';
-      lsxArgs = lsxArgs.trim();
+    const tagPattern = /ls|lsx/;
+    const result = customTagUtils.findTagAndReplace(tagPattern, parsedHTML);
 
 
-      // create contexts
-      const lsxContext = new LsxContext();
-      lsxContext.currentPagePath = currentPagePath;
-      lsxContext.tagExpression = tagExpression;
-      lsxContext.fromPagePath = fromPagePath;
-      lsxContext.lsxArgs = lsxArgs;
+    context.parsedHTML = result.html;
+    context.lsxContextMap = result.tagContextMap;
 
 
-      const renderId = `lsx-${this.createRandomStr(8)}`;
-
-      context.lsxContextMap[renderId] = lsxContext;
-
-      // return replace strings
-      return this.createReactTargetDom(renderId);
-    });
+    // unmount
+    if (contextName === 'preRenderPreviewHtml') {
+      this.unmountPreviousReactDOMs(context);
+    }
 
 
     // resolve
     // resolve
-    return Promise.resolve(context);
+    return context;
   }
   }
 
 
-  createReactTargetDom(renderId) {
-    return `<div id="${renderId}"></div>`;
-  }
-
-  /**
-   * initialize cache
-   *  when contextName is 'preRenderHtml'         -> clear cache
-   *  when contextName is 'preRenderPreviewHtml'  -> doesn't clear cache
-   *
-   * @param {string} contextName
-   *
-   * @memberOf LsxPreRenderInterceptor
-   */
-  initializeCache(contextName) {
-    if (contextName === 'preRenderHtml') {
-      LsxCacheHelper.clearAllStateCaches();
+  unmountPreviousReactDOMs(newContext) {
+    if (this.previousPreviewContext != null) {
+      // forEach keys of lsxContextMap
+      Object.keys(this.previousPreviewContext.lsxContextMap).forEach((domId) => {
+        const elem = document.getElementById(domId);
+        ReactDOM.unmountComponentAtNode(elem);
+      });
     }
     }
-  }
 
 
-  /**
-   * @see http://qiita.com/ryounagaoka/items/4736c225bdd86a74d59c
-   *
-   * @param {number} length
-   * @return random strings
-   */
-  createRandomStr(length) {
-    const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
-    let generated = '';
-    for (let i = 0; i < length; i++) {
-      generated += bag[Math.floor(Math.random() * bag.length)];
-    }
-    return generated;
+    this.previousPreviewContext = newContext;
   }
   }
 
 
 }
 }

+ 0 - 87
packages/plugin-lsx/src/client/js/util/LsxCacheHelper.js

@@ -1,87 +0,0 @@
-import { PageNode } from '../components/PageNode';
-
-export class LsxCacheHelper {
-
-  /**
-   * @private
-   */
-  static retrieveFromSessionStorage() {
-    return JSON.parse(sessionStorage.getItem('lsx-cache')) || {};
-  }
-
-  /**
-   * stringify and save obj
-   *
-   * @static
-   * @param {object} cacheObj
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static saveToSessionStorage(cacheObj) {
-    sessionStorage.setItem('lsx-cache', JSON.stringify(cacheObj));
-  }
-
-  /**
-   * generate cache key for storing to storage
-   *
-   * @static
-   * @param {LsxContext} lsxContext
-   * @returns
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static generateCacheKeyFromContext(lsxContext) {
-    return `${lsxContext.fromPagePath}__${lsxContext.lsxArgs}`;
-  }
-
-  /**
-   *
-   *
-   * @static
-   * @param {string} key
-   * @returns
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static getStateCache(key) {
-    const cacheObj = LsxCacheHelper.retrieveFromSessionStorage();
-    const stateCache = cacheObj[key];
-
-    if (stateCache != null && stateCache.nodeTree != null) {
-      // instanciate PageNode
-      stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
-        return PageNode.instanciateFrom(obj);
-      });
-    }
-
-    return stateCache;
-  }
-
-  /**
-   * store state object of React Component with specified key
-   *
-   * @static
-   * @param {string} key
-   * @param {object} lsxState state object of React Component
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static cacheState(key, lsxState) {
-    const cacheObj = LsxCacheHelper.retrieveFromSessionStorage();
-    cacheObj[key] = lsxState;
-
-    LsxCacheHelper.saveToSessionStorage(cacheObj);
-  }
-
-  /**
-   * clear all state caches
-   *
-   * @static
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static clearAllStateCaches() {
-    LsxCacheHelper.saveToSessionStorage({});
-  }
-
-}

+ 23 - 83
packages/plugin-lsx/src/client/js/util/LsxContext.js

@@ -1,19 +1,23 @@
 import * as url from 'url';
 import * as url from 'url';
 
 
-import { pathUtils } from 'growi-commons';
+import { customTagUtils, pathUtils } from 'growi-commons';
 
 
-export class LsxContext {
+const { TagContext, ArgsParser, OptionParser } = customTagUtils;
+
+export class LsxContext extends TagContext {
+
+  /**
+   * @param {object|TagContext|LsxContext} initArgs
+   */
+  constructor(initArgs) {
+    super(initArgs);
 
 
-  constructor() {
-    this.currentPagePath = null;
-    this.tagExpression = null;
     this.fromPagePath = null;
     this.fromPagePath = null;
-    this.lsxArgs = null;
 
 
     // initialized after parse()
     // initialized after parse()
     this.isParsed = null;
     this.isParsed = null;
     this.pagePath = null;
     this.pagePath = null;
-    this.options = null;
+    this.options = {};
   }
   }
 
 
   parse() {
   parse() {
@@ -21,40 +25,17 @@ export class LsxContext {
       return;
       return;
     }
     }
 
 
-    // initialize
-    let specifiedPath;
-    this.options = {};
-
-    if (this.lsxArgs.length > 0) {
-      const splittedArgs = this.lsxArgs.split(',');
-      let firstArgsKey; let
-        firstArgsValue;
-
-      splittedArgs.forEach((arg, index) => {
-        const trimedArg = arg.trim();
+    const parsedResult = ArgsParser.parse(this.args);
+    this.options = parsedResult.options;
 
 
-        // parse string like 'key1=value1, key2=value2, ...'
-        // see https://regex101.com/r/pYHcOM/1
-        const match = trimedArg.match(/([^=]+)=?(.+)?/);
-        const key = match[1];
-        const value = match[2] || true;
-        this.options[key] = value;
-
-        if (index === 0) {
-          firstArgsKey = key;
-          firstArgsValue = value;
-        }
-      });
-
-      // determine specifiedPath
-      // order:
-      //   1: lsx(prefix=..., ...)
-      //   2: lsx(firstArgs, ...)
-      //   3: fromPagePath
-      specifiedPath = this.options.prefix
-          || ((firstArgsValue === true) ? firstArgsKey : undefined)
-          || this.fromPagePath;
-    }
+    // determine specifiedPath
+    // order:
+    //   1: lsx(prefix=..., ...)
+    //   2: lsx(firstArgs, ...)
+    //   3: fromPagePath
+    const specifiedPath = this.options.prefix
+        || ((parsedResult.firstArgsValue === true) ? parsedResult.firstArgsKey : undefined)
+        || this.fromPagePath;
 
 
     // resolve pagePath
     // resolve pagePath
     //   when `fromPagePath`=/hoge and `specifiedPath`=./fuga,
     //   when `fromPagePath`=/hoge and `specifiedPath`=./fuga,
@@ -71,51 +52,10 @@ export class LsxContext {
   }
   }
 
 
   getOptDepth() {
   getOptDepth() {
-    // eslint-disable-next-line eqeqeq
-    if (this.options.depth == undefined) {
-      return undefined;
-    }
-    return this.parseNum(this.options.depth);
-  }
-
-  parseNum(str) {
-    // eslint-disable-next-line eqeqeq
-    if (str == undefined) {
-      return undefined;
-    }
-
-    // see: https://regex101.com/r/w4KCwC/3
-    const match = str.match(/^(-?[0-9]+)(([:+]{1})(-?[0-9]+)?)?$/);
-    if (!match) {
+    if (this.options.depth === undefined) {
       return undefined;
       return undefined;
     }
     }
-
-    // determine start
-    let start;
-    let end;
-
-    // has operator
-    // eslint-disable-next-line eqeqeq
-    if (match[3] != undefined) {
-      start = +match[1];
-      const operator = match[3];
-
-      // determine end
-      if (operator === ':') {
-        end = +match[4] || -1; // set last(-1) if undefined
-      }
-      else if (operator === '+') {
-        end = +match[4] || 0; // plus zero if undefined
-        end += start;
-      }
-    }
-    // don't have operator
-    else {
-      start = 1;
-      end = +match[1];
-    }
-
-    return { start, end };
+    return OptionParser.parseRange(this.options.depth);
   }
   }
 
 
 }
 }

+ 39 - 0
packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js

@@ -0,0 +1,39 @@
+import { TagCacheManager } from 'growi-commons';
+
+const LSX_STATE_CACHE_NS = 'lsx-state-cache';
+
+
+// validate growi-commons version
+function validateGrowiCommonsVersion() {
+  // TagCacheManager was created on growi-commons@4.0.7
+  if (TagCacheManager == null) {
+    throw new Error(
+      'This version of \'growi-plugin-lsx\' requires \'growi-commons >= 4.0.7\'.\n'
+      + 'To resolve this, please process  either a) or b).\n'
+      + '\n'
+      + 'a) Use \'growi-plugin-lsx@3.0.0\'\n'
+      + 'b) Edit \'package.json\' of growi and upgrade \'growi-commons\' to v4.0.7 or above.',
+    );
+  }
+}
+
+
+let _instance;
+export class TagCacheManagerFactory {
+
+  static getInstance() {
+    validateGrowiCommonsVersion();
+
+    if (_instance == null) {
+      // create generateCacheKey implementation
+      const generateCacheKey = (lsxContext) => {
+        return `${lsxContext.fromPagePath}__${lsxContext.args}`;
+      };
+
+      _instance = new TagCacheManager(LSX_STATE_CACHE_NS, generateCacheKey);
+    }
+
+    return _instance;
+  }
+
+}

+ 81 - 15
packages/plugin-lsx/src/server/routes/lsx.js

@@ -2,6 +2,9 @@ const { customTagUtils } = require('growi-commons');
 
 
 const { OptionParser } = customTagUtils;
 const { OptionParser } = customTagUtils;
 
 
+
+const DEFAULT_PAGES_NUM = 50;
+
 class Lsx {
 class Lsx {
 
 
   /**
   /**
@@ -16,6 +19,11 @@ class Lsx {
    * @memberOf Lsx
    * @memberOf Lsx
    */
    */
   static addDepthCondition(query, pagePath, optionsDepth) {
   static addDepthCondition(query, pagePath, optionsDepth) {
+    // when option strings is 'depth=', the option value is true
+    if (optionsDepth == null || optionsDepth === true) {
+      throw new Error('The value of depth option is invalid.');
+    }
+
     const range = OptionParser.parseRange(optionsDepth);
     const range = OptionParser.parseRange(optionsDepth);
     const start = range.start;
     const start = range.start;
     const end = range.end;
     const end = range.end;
@@ -40,12 +48,21 @@ class Lsx {
    * @static
    * @static
    * @param {any} query
    * @param {any} query
    * @param {any} pagePath
    * @param {any} pagePath
-   * @param {any} optionsNum
+   * @param {number|string} optionsNum
    * @returns
    * @returns
    *
    *
    * @memberOf Lsx
    * @memberOf Lsx
    */
    */
   static addNumCondition(query, pagePath, optionsNum) {
   static addNumCondition(query, pagePath, optionsNum) {
+    // when option strings is 'num=', the option value is true
+    if (optionsNum == null || optionsNum === true) {
+      throw new Error('The value of num option is invalid.');
+    }
+
+    if (typeof optionsNum === 'number') {
+      return query.limit(optionsNum);
+    }
+
     const range = OptionParser.parseRange(optionsNum);
     const range = OptionParser.parseRange(optionsNum);
     const start = range.start;
     const start = range.start;
     const end = range.end;
     const end = range.end;
@@ -60,6 +77,47 @@ class Lsx {
     return query.skip(skip).limit(limit);
     return query.skip(skip).limit(limit);
   }
   }
 
 
+  /**
+   * add filter condition that filter fetched pages
+   *
+   * @static
+   * @param {any} query
+   * @param {any} pagePath
+   * @param {any} optionsFilter
+   * @param {boolean} isExceptFilter
+   * @returns
+   *
+   * @memberOf Lsx
+   */
+  static addFilterCondition(query, pagePath, optionsFilter, isExceptFilter = false) {
+    // when option strings is 'filter=', the option value is true
+    if (optionsFilter == null || optionsFilter === true) {
+      throw new Error('filter option require value in regular expression.');
+    }
+
+    let filterPath = '';
+    if (optionsFilter.charAt(0) === '^') {
+      // move '^' to the first of path
+      filterPath = new RegExp(`^${pagePath}${optionsFilter.slice(1, optionsFilter.length)}`);
+    }
+    else {
+      filterPath = new RegExp(`^${pagePath}.*${optionsFilter}`);
+    }
+
+    if (isExceptFilter) {
+      return query.and({
+        path: { $not: filterPath },
+      });
+    }
+    return query.and({
+      path: filterPath,
+    });
+  }
+
+  static addExceptCondition(query, pagePath, optionsFilter) {
+    return this.addFilterCondition(query, pagePath, optionsFilter, true);
+  }
+
   /**
   /**
    * add sort condition(sort key & sort order)
    * add sort condition(sort key & sort order)
    *
    *
@@ -75,7 +133,10 @@ class Lsx {
    *
    *
    * @memberOf Lsx
    * @memberOf Lsx
    */
    */
-  static addSortCondition(query, pagePath, optionsSort = 'path', optionsReverse) {
+  static addSortCondition(query, pagePath, optionsSortArg, optionsReverse) {
+    // init sort key
+    const optionsSort = optionsSortArg || 'path';
+
     // the default sort order
     // the default sort order
     let isReversed = false;
     let isReversed = false;
 
 
@@ -126,19 +187,18 @@ module.exports = (crowi, app) => {
     }
     }
 
 
     const builder = new Page.PageQueryBuilder(baseQuery);
     const builder = new Page.PageQueryBuilder(baseQuery);
-    builder.addConditionToListWithDescendants(pagePath, {})
+    if (builder.addConditionToListOnlyDescendants == null) { // for Backward compatibility (<= GROWI v4.0.x)
+      builder.addConditionToListWithDescendants(pagePath);
+    }
+    else {
+      builder.addConditionToListOnlyDescendants(pagePath);
+    }
+
+    builder
       .addConditionToExcludeTrashed()
       .addConditionToExcludeTrashed()
       .addConditionToExcludeRedirect();
       .addConditionToExcludeRedirect();
 
 
-    let promisifiedBuilder = Promise.resolve(builder);
-
-    if (user != null) {
-      const UserGroupRelation = crowi.model('UserGroupRelation');
-      const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-      promisifiedBuilder = builder.addConditionToFilteringByViewer(user, userGroups);
-    }
-
-    return promisifiedBuilder;
+    return Page.addConditionToFilteringByViewerForList(builder, user);
   }
   }
 
 
   actions.listPages = async(req, res) => {
   actions.listPages = async(req, res) => {
@@ -154,10 +214,16 @@ module.exports = (crowi, app) => {
       if (options.depth != null) {
       if (options.depth != null) {
         query = Lsx.addDepthCondition(query, pagePath, options.depth);
         query = Lsx.addDepthCondition(query, pagePath, options.depth);
       }
       }
-      // num
-      if (options.num != null) {
-        query = Lsx.addNumCondition(query, pagePath, options.num);
+      // filter
+      if (options.filter != null) {
+        query = Lsx.addFilterCondition(query, pagePath, options.filter);
       }
       }
+      if (options.except != null) {
+        query = Lsx.addExceptCondition(query, pagePath, options.except);
+      }
+      // num
+      const optionsNum = options.num || DEFAULT_PAGES_NUM;
+      query = Lsx.addNumCondition(query, pagePath, optionsNum);
       // sort
       // sort
       query = Lsx.addSortCondition(query, pagePath, options.sort, options.reverse);
       query = Lsx.addSortCondition(query, pagePath, options.sort, options.reverse);