Просмотр исходного кода

Merge pull request #5622 from weseek/fix/rendering-contexts

fix: Rendering with contexts
Yuki Takei 4 лет назад
Родитель
Сommit
275f4919e5
26 измененных файлов с 189 добавлено и 179 удалено
  1. 1 1
      packages/app/resource/locales/en_US/sandbox.md
  2. 1 1
      packages/app/resource/locales/ja_JP/sandbox.md
  3. 1 1
      packages/app/resource/locales/zh_CN/sandbox.md
  4. 6 6
      packages/app/src/client/util/GrowiRenderer.js
  5. 0 1
      packages/app/src/client/util/interceptor/detach-code-blocks.js
  6. 3 3
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  7. 3 3
      packages/app/src/components/MyDraftList/Draft.jsx
  8. 5 2
      packages/app/src/components/Navbar/GlobalSearch.tsx
  9. 1 4
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  10. 8 5
      packages/app/src/components/Page.jsx
  11. 3 1
      packages/app/src/components/Page/RevisionLoader.jsx
  12. 7 4
      packages/app/src/components/Page/RevisionRenderer.jsx
  13. 3 3
      packages/app/src/components/PageComment/Comment.jsx
  14. 3 3
      packages/app/src/components/PageComment/CommentEditor.jsx
  15. 9 2
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  16. 0 123
      packages/app/src/components/PageEditor/Preview.jsx
  17. 112 0
      packages/app/src/components/PageEditor/Preview.tsx
  18. 3 2
      packages/app/src/components/PageEditor/PreviewWithSuspense.jsx
  19. 1 0
      packages/app/src/components/PageTimeline.jsx
  20. 1 0
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  21. 10 7
      packages/app/src/stores/ui.tsx
  22. 1 1
      packages/app/src/styles/_on-edit.scss
  23. 1 1
      packages/app/src/styles/theme/_apply-colors.scss
  24. 1 1
      packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js
  25. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js
  26. 4 3
      packages/plugin-pukiwiki-like-linker/src/resource/js/util/PreProcessor/PukiwikiLikeLinker.js

+ 1 - 1
packages/app/resource/locales/en_US/sandbox.md

@@ -256,7 +256,7 @@ Both the page description and link address can be displayed on the page.
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
-[[../Bootstrap4]]  
+[[./Bootstrap4]]  
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 # :pencil: Lists

+ 1 - 1
packages/app/resource/locales/ja_JP/sandbox.md

@@ -255,7 +255,7 @@ ___
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 ```
 
-[[../Bootstrap4]]  
+[[./Bootstrap4]]  
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 
 # :pencil: Lists

+ 1 - 1
packages/app/resource/locales/zh_CN/sandbox.md

@@ -256,7 +256,7 @@ Both the page description and link address can be displayed on the page.
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 ```
 
-[[../Bootstrap4]]  
+[[./Bootstrap4]]  
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 
 # :pencil: Lists

+ 6 - 6
packages/app/src/client/util/GrowiRenderer.js

@@ -135,29 +135,29 @@ export default class GrowiRenderer {
     }
   }
 
-  preProcess(markdown) {
+  preProcess(markdown, context) {
     let processed = markdown;
     for (let i = 0; i < this.preProcessors.length; i++) {
       if (!this.preProcessors[i].process) {
         continue;
       }
-      processed = this.preProcessors[i].process(processed);
+      processed = this.preProcessors[i].process(processed, context);
     }
 
     return processed;
   }
 
-  process(markdown) {
-    return this.md.render(markdown);
+  process(markdown, context) {
+    return this.md.render(markdown, context);
   }
 
-  postProcess(html) {
+  postProcess(html, context) {
     let processed = html;
     for (let i = 0; i < this.postProcessors.length; i++) {
       if (!this.postProcessors[i].process) {
         continue;
       }
-      processed = this.postProcessors[i].process(processed);
+      processed = this.postProcessors[i].process(processed, context);
     }
 
     return processed;

+ 0 - 1
packages/app/src/client/util/interceptor/detach-code-blocks.js

@@ -47,7 +47,6 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
 
     const context = Object.assign(args[0]); // clone
     const targetKey = this.getTargetKey(contextName);
-    const currentPagePath = context.currentPagePath; // eslint-disable-line no-unused-vars
 
     context.dcbContextMap = {};
 

+ 3 - 3
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -66,15 +66,15 @@
         interceptorManager.process('preRender', context)
           .then(() => { return interceptorManager.process('prePreProcess', context) })
           .then(() => {
-            context.markdown = growiRenderer.preProcess(context.markdown);
+            context.markdown = growiRenderer.preProcess(context.markdown, context);
           })
           .then(() => { return interceptorManager.process('postPreProcess', context) })
           .then(() => {
-            context.parsedHTML = growiRenderer.process(context.markdown);
+            context.parsedHTML = growiRenderer.process(context.markdown, context);
           })
           .then(() => { return interceptorManager.process('prePostProcess', context) })
           .then(() => {
-            context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+            context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
           })
           .then(() => { return interceptorManager.process('postPostProcess', context) })
           .then(() => { return interceptorManager.process('preRenderHtml', context) })

+ 3 - 3
packages/app/src/components/MyDraftList/Draft.jsx

@@ -63,16 +63,16 @@ class Draft extends React.Component {
     const interceptorManager = this.props.appContainer.interceptorManager;
     await interceptorManager.process('prePreProcess', context)
       .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
+        context.markdown = growiRenderer.preProcess(context.markdown, context);
       })
       .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown);
+        const parsedHTML = growiRenderer.process(context.markdown, context);
         context.parsedHTML = parsedHTML;
       })
       .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
       })
       .then(() => { return interceptorManager.process('postPostProcess', context) })
       .then(() => {

+ 5 - 2
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -13,6 +13,7 @@ import { IPageWithMeta } from '~/interfaces/page';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 import SearchForm from '../SearchForm';
+import { useCurrentPagePath } from '~/stores/context';
 
 
 type Props = {
@@ -33,6 +34,8 @@ const GlobalSearch: FC<Props> = (props: Props) => {
   const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
   const [isFocused, setFocused] = useState<boolean>(false);
 
+  const { data: currentPagePath } = useCurrentPagePath();
+
   const gotoPage = useCallback((data: IPageWithMeta<IPageSearchMeta>[]) => {
     assert(data.length > 0);
 
@@ -51,12 +54,12 @@ const GlobalSearch: FC<Props> = (props: Props) => {
     // construct search query
     let q = text;
     if (isScopeChildren) {
-      q += ` prefix:${window.location.pathname}`;
+      q += ` prefix:${currentPagePath ?? window.location.pathname}`;
     }
     url.searchParams.append('q', q);
 
     window.location.href = url.href;
-  }, [isScopeChildren, text]);
+  }, [currentPagePath, isScopeChildren, text]);
 
   const scopeLabel = isScopeChildren
     ? t('header_search_box.label.This tree')

+ 1 - 4
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -44,10 +44,7 @@ const PersonalDropdown = (props) => {
   const logoutHandler = () => {
     const { interceptorManager } = appContainer;
 
-    const context = {
-      user,
-      currentPagePath: decodeURIComponent(window.location.pathname),
-    };
+    const context = {};
     interceptorManager.process('logout', context);
 
     window.location.href = '/logout';

+ 8 - 5
packages/app/src/components/Page.jsx

@@ -24,7 +24,7 @@ import {
   useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
 } from '~/stores/ui';
 import { useIsSlackEnabled } from '~/stores/editor';
-import { useSlackChannels } from '~/stores/context';
+import { useCurrentPagePath, useSlackChannels } from '~/stores/context';
 
 const logger = loggerFactory('growi:Page');
 
@@ -143,7 +143,7 @@ class Page extends React.Component {
   }
 
   render() {
-    const { appContainer, pageContainer } = this.props;
+    const { appContainer, pageContainer, pagePath } = this.props;
     const { isMobile } = appContainer;
     const isLoggedIn = appContainer.currentUser != null;
     const { markdown, revisionId } = pageContainer.state;
@@ -152,7 +152,7 @@ class Page extends React.Component {
       <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
 
         { revisionId != null && (
-          <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
+          <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} pagePath={pagePath} />
         )}
 
         { isLoggedIn && (
@@ -170,11 +170,12 @@ class Page extends React.Component {
 }
 
 Page.propTypes = {
+  // TODO: remove this when omitting unstated is completed
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 
-  // TODO: remove this when omitting unstated is completed
+  pagePath: PropTypes.string.isRequired,
   editorMode: PropTypes.string.isRequired,
   isSlackEnabled: PropTypes.bool.isRequired,
   slackChannels: PropTypes.string.isRequired,
@@ -184,6 +185,7 @@ Page.propTypes = {
 };
 
 const PageWrapper = (props) => {
+  const { data: currentPagePath } = useCurrentPagePath();
   const { data: editorMode } = useEditorMode();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: slackChannels } = useSlackChannels();
@@ -191,13 +193,14 @@ const PageWrapper = (props) => {
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupName } = useSelectedGrantGroupName();
 
-  if (editorMode == null) {
+  if (currentPagePath == null || editorMode == null) {
     return null;
   }
 
   return (
     <Page
       {...props}
+      pagePath={currentPagePath}
       editorMode={editorMode}
       isSlackEnabled={isSlackEnabled}
       slackChannels={slackChannels}

+ 3 - 1
packages/app/src/components/Page/RevisionLoader.jsx

@@ -20,7 +20,7 @@ class LegacyRevisionLoader extends React.Component {
     this.logger = loggerFactory('growi:Page:RevisionLoader');
 
     this.state = {
-      markdown: '',
+      markdown: null,
       isLoading: false,
       isLoaded: false,
       errors: null,
@@ -106,6 +106,7 @@ class LegacyRevisionLoader extends React.Component {
       <RevisionRenderer
         growiRenderer={this.props.growiRenderer}
         markdown={markdown}
+        pagePath={this.props.pagePath}
         highlightKeywords={this.props.highlightKeywords}
       />
     );
@@ -123,6 +124,7 @@ LegacyRevisionLoader.propTypes = {
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   pageId: PropTypes.string.isRequired,
+  pagePath: PropTypes.string.isRequired,
   revisionId: PropTypes.string.isRequired,
   lazy: PropTypes.bool,
   onRevisionLoaded: PropTypes.func,

+ 7 - 4
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -28,7 +28,8 @@ class LegacyRevisionRenderer extends React.PureComponent {
   initCurrentRenderingContext() {
     this.currentRenderingContext = {
       markdown: this.props.markdown,
-      currentPagePath: decodeURIComponent(window.location.pathname),
+      pagePath: this.props.pagePath,
+      currentPathname: decodeURIComponent(window.location.pathname),
     };
   }
 
@@ -133,11 +134,11 @@ class LegacyRevisionRenderer extends React.PureComponent {
 
     await interceptorManager.process('preRender', context);
     await interceptorManager.process('prePreProcess', context);
-    context.markdown = growiRenderer.preProcess(context.markdown);
+    context.markdown = growiRenderer.preProcess(context.markdown, context);
     await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = growiRenderer.process(context.markdown);
+    context.parsedHTML = growiRenderer.process(context.markdown, context);
     await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
 
     const isMarkdownEmpty = context.markdown.trim().length === 0;
     if (highlightKeywords != null && highlightKeywords.length > 0 && !isMarkdownEmpty) {
@@ -169,6 +170,7 @@ LegacyRevisionRenderer.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   markdown: PropTypes.string.isRequired,
+  pagePath: PropTypes.string.isRequired,
   highlightKeywords: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
   additionalClassName: PropTypes.string,
 };
@@ -187,6 +189,7 @@ const RevisionRenderer = (props) => {
 RevisionRenderer.propTypes = {
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   markdown: PropTypes.string.isRequired,
+  pagePath: PropTypes.string.isRequired,
   highlightKeywords: PropTypes.arrayOf(PropTypes.string),
   additionalClassName: PropTypes.string,
 };

+ 3 - 3
packages/app/src/components/PageComment/Comment.jsx

@@ -135,11 +135,11 @@ class Comment extends React.PureComponent {
 
     await interceptorManager.process('preRenderComment', context);
     await interceptorManager.process('prePreProcess', context);
-    context.markdown = await growiRenderer.preProcess(context.markdown);
+    context.markdown = await growiRenderer.preProcess(context.markdown, context);
     await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = await growiRenderer.process(context.markdown);
+    context.parsedHTML = await growiRenderer.process(context.markdown, context);
     await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = await growiRenderer.postProcess(context.parsedHTML);
+    context.parsedHTML = await growiRenderer.postProcess(context.parsedHTML, context);
     await interceptorManager.process('postPostProcess', context);
     await interceptorManager.process('preRenderCommentHtml', context);
     this.setState({ html: context.parsedHTML });

+ 3 - 3
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -231,16 +231,16 @@ class CommentEditor extends React.Component {
     interceptorManager.process('preRenderCommnetPreview', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
+        context.markdown = growiRenderer.preProcess(context.markdown, context);
       })
       .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown);
+        const parsedHTML = growiRenderer.process(context.markdown, context);
         context.parsedHTML = parsedHTML;
       })
       .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
       })
       .then(() => { return interceptorManager.process('postPostProcess', context) })
       .then(() => { return interceptorManager.process('preRenderCommentPreviewHtml', context) })

+ 9 - 2
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -38,6 +38,7 @@ class LinkEditModal extends React.PureComponent {
       labelInputValue: '',
       linkerType: Linker.types.markdownLink,
       markdown: null,
+      pagePath: null,
       previewError: '',
       permalink: '',
       isPreviewOpen: false,
@@ -153,6 +154,7 @@ class LinkEditModal extends React.PureComponent {
     const { t } = this.props;
     const path = this.state.linkInputValue;
     let markdown = null;
+    let pagePath = null;
     let permalink = '';
     let previewError = '';
 
@@ -165,6 +167,7 @@ class LinkEditModal extends React.PureComponent {
         const { data } = await this.props.appContainer.apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId });
         const { page } = data;
         markdown = page.revision.body;
+        pagePath = page.path;
         permalink = page.id;
       }
       catch (err) {
@@ -174,7 +177,9 @@ class LinkEditModal extends React.PureComponent {
     else {
       previewError = t('link_edit.page_not_found_in_preview', { path });
     }
-    this.setState({ markdown, previewError, permalink });
+    this.setState({
+      markdown, pagePath, previewError, permalink,
+    });
   }
 
   renderLinkPreview() {
@@ -278,6 +283,8 @@ class LinkEditModal extends React.PureComponent {
 
   renderLinkAndLabelForm() {
     const { t } = this.props;
+    const { pagePath } = this.state;
+
     return (
       <>
         <h3 className="grw-modal-head">{t('link_edit.set_link_and_label')}</h3>
@@ -301,7 +308,7 @@ class LinkEditModal extends React.PureComponent {
                 </button>
                 <Popover trigger="focus" placement="right" isOpen={this.state.isPreviewOpen} target="preview-btn" toggle={this.toggleIsPreviewOpen}>
                   <PopoverBody>
-                    <PreviewWithSuspense setMarkdown={this.setMarkdown} markdown={this.state.markdown} error={this.state.previewError} />
+                    <PreviewWithSuspense setMarkdown={this.setMarkdown} markdown={this.state.markdown} pagePath={pagePath} error={this.state.previewError} />
                   </PopoverBody>
                 </Popover>
               </div>

+ 0 - 123
packages/app/src/components/PageEditor/Preview.jsx

@@ -1,123 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { Subscribe } from 'unstated';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import RevisionBody from '../Page/RevisionBody';
-
-import AppContainer from '~/client/services/AppContainer';
-import EditorContainer from '~/client/services/EditorContainer';
-
-/**
- * Wrapper component for Page/RevisionBody
- */
-class Preview extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      html: '',
-    };
-
-    // get renderer
-    this.growiRenderer = props.appContainer.getRenderer('editor');
-  }
-
-  componentDidMount() {
-    this.initCurrentRenderingContext();
-    this.renderPreview();
-  }
-
-  componentDidUpdate(prevProps) {
-    const { markdown: prevMarkdown } = prevProps;
-    const { markdown } = this.props;
-
-    // render only when props.markdown is updated
-    if (markdown !== prevMarkdown) {
-      this.initCurrentRenderingContext();
-      this.renderPreview();
-      return;
-    }
-
-    const { interceptorManager } = this.props.appContainer;
-
-    interceptorManager.process('postRenderPreviewHtml', this.currentRenderingContext);
-  }
-
-  initCurrentRenderingContext() {
-    this.currentRenderingContext = {
-      markdown: this.props.markdown,
-      currentPagePath: decodeURIComponent(window.location.pathname),
-    };
-  }
-
-  async renderPreview() {
-    const { appContainer } = this.props;
-    const { growiRenderer } = this;
-
-    const { interceptorManager } = appContainer;
-    const context = this.currentRenderingContext;
-
-    await interceptorManager.process('preRenderPreview', context);
-    await interceptorManager.process('prePreProcess', context);
-    context.markdown = growiRenderer.preProcess(context.markdown);
-    await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = growiRenderer.process(context.markdown);
-    await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
-    await interceptorManager.process('postPostProcess', context);
-    await interceptorManager.process('preRenderPreviewHtml', context);
-
-    this.setState({ html: context.parsedHTML });
-  }
-
-  render() {
-    return (
-      <Subscribe to={[EditorContainer]}>
-        { editorContainer => (
-          // eslint-disable-next-line arrow-body-style
-          <div
-            className="page-editor-preview-body"
-            ref={(elm) => {
-              this.previewElement = elm;
-              if (this.props.inputRef != null) {
-                this.props.inputRef(elm);
-              }
-            }}
-            onScroll={(event) => {
-              if (this.props.onScroll != null) {
-                this.props.onScroll(event.target.scrollTop);
-              }
-            }}
-          >
-            <RevisionBody
-              {...this.props}
-              html={this.state.html}
-              renderMathJaxInRealtime={editorContainer.state.previewOptions.renderMathJaxInRealtime}
-            />
-          </div>
-        )}
-      </Subscribe>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const PreviewWrapper = withUnstatedContainers(Preview, [AppContainer]);
-
-Preview.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  markdown: PropTypes.string,
-  inputRef: PropTypes.func,
-  isMathJaxEnabled: PropTypes.bool,
-  renderMathJaxOnInit: PropTypes.bool,
-  onScroll: PropTypes.func,
-};
-
-export default PreviewWrapper;

+ 112 - 0
packages/app/src/components/PageEditor/Preview.tsx

@@ -0,0 +1,112 @@
+import React, {
+  UIEventHandler, useCallback, useEffect, useMemo, useState,
+} from 'react';
+
+import { Subscribe } from 'unstated';
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+import RevisionBody from '../Page/RevisionBody';
+
+import AppContainer from '~/client/services/AppContainer';
+import EditorContainer from '~/client/services/EditorContainer';
+
+
+type Props = {
+  appContainer: AppContainer,
+  editorContainer: EditorContainer,
+
+  markdown?: string,
+  pagePath?: string,
+  inputRef?: React.RefObject<HTMLDivElement>,
+  isMathJaxEnabled?: boolean,
+  renderMathJaxOnInit?: boolean,
+  onScroll?: UIEventHandler<HTMLDivElement>,
+}
+
+
+const Preview = (props: Props): JSX.Element => {
+
+  const {
+    appContainer,
+    markdown, pagePath,
+    inputRef,
+    onScroll,
+  } = props;
+
+  const [html, setHtml] = useState('');
+
+  const { interceptorManager } = appContainer;
+  const growiRenderer = props.appContainer.getRenderer('editor');
+
+  const context = useMemo(() => {
+    return {
+      markdown,
+      pagePath,
+      currentPathname: decodeURIComponent(window.location.pathname),
+      parsedHTML: null,
+    };
+  }, [markdown, pagePath]);
+
+  const renderPreview = useCallback(async() => {
+    if (interceptorManager != null) {
+      await interceptorManager.process('preRenderPreview', context);
+      await interceptorManager.process('prePreProcess', context);
+      context.markdown = growiRenderer.preProcess(context.markdown, context);
+      await interceptorManager.process('postPreProcess', context);
+      context.parsedHTML = growiRenderer.process(context.markdown, context);
+      await interceptorManager.process('prePostProcess', context);
+      context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+      await interceptorManager.process('postPostProcess', context);
+      await interceptorManager.process('preRenderPreviewHtml', context);
+    }
+
+    setHtml(context.parsedHTML ?? '');
+  }, [interceptorManager, context, growiRenderer]);
+
+  useEffect(() => {
+    if (markdown == null) {
+      setHtml('');
+    }
+
+    renderPreview();
+  }, [markdown, renderPreview]);
+
+  useEffect(() => {
+    if (html == null) {
+      return;
+    }
+
+    if (interceptorManager != null) {
+      interceptorManager.process('postRenderPreviewHtml', {
+        ...context,
+        parsedHTML: html,
+      });
+    }
+  }, [context, html, interceptorManager]);
+
+  return (
+    <Subscribe to={[EditorContainer]}>
+      { editorContainer => (
+        <div
+          className="page-editor-preview-body"
+          ref={inputRef}
+          onScroll={onScroll}
+        >
+          <RevisionBody
+            {...props}
+            html={html}
+            renderMathJaxInRealtime={editorContainer.state.previewOptions.renderMathJaxInRealtime}
+          />
+        </div>
+      ) }
+    </Subscribe>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PreviewWrapper = withUnstatedContainers(Preview, [AppContainer]);
+
+export default PreviewWrapper;

+ 3 - 2
packages/app/src/components/PageEditor/PreviewWithSuspense.jsx

@@ -5,7 +5,7 @@ import Preview from './Preview';
 import { withLoadingSppiner } from '../SuspenseUtils';
 
 function PagePreview(props) {
-  if (props.markdown == null) {
+  if (props.markdown == null || props.pagePath == null) {
     if (props.error !== '') {
       return props.error;
     }
@@ -16,7 +16,7 @@ function PagePreview(props) {
 
   return (
     <div className="linkedit-preview">
-      <Preview markdown={props.markdown} />
+      <Preview markdown={props.markdown} pagePath={props.pagePath} />
     </div>
   );
 }
@@ -24,6 +24,7 @@ function PagePreview(props) {
 PagePreview.propTypes = {
   setMarkdown: PropTypes.func,
   markdown: PropTypes.string,
+  pagePath: PropTypes.string,
   error: PropTypes.string,
 };
 

+ 1 - 0
packages/app/src/components/PageTimeline.jsx

@@ -84,6 +84,7 @@ class PageTimeline extends React.Component {
                     lazy
                     growiRenderer={this.growiRenderer}
                     pageId={page._id}
+                    pagePath={page.path}
                     revisionId={page.revision}
                   />
                 </div>

+ 1 - 0
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -62,6 +62,7 @@ const CustomSidebar: FC<Props> = (props: Props) => {
             <RevisionRenderer
               growiRenderer={renderer}
               markdown={markdown}
+              pagePath="/Sidebar"
               additionalClassName="grw-custom-sidebar-content"
             />
           </div>

+ 10 - 7
packages/app/src/stores/ui.tsx

@@ -68,28 +68,28 @@ export const useIsMobile = (): SWRResponse<boolean, Error> => {
   return useStaticSWR<boolean, Error>(key, undefined, configuration);
 };
 
-const updateBodyClassesByEditorMode = (newEditorMode: EditorMode) => {
+const updateBodyClassesByEditorMode = (newEditorMode: EditorMode, isSidebar = false) => {
   switch (newEditorMode) {
     case EditorMode.View:
       $('body').removeClass('on-edit');
       $('body').removeClass('builtin-editor');
       $('body').removeClass('hackmd');
-      $('body').removeClass('pathname-sidebar');
+      $('body').removeClass('editing-sidebar');
       break;
     case EditorMode.Editor:
       $('body').addClass('on-edit');
       $('body').addClass('builtin-editor');
       $('body').removeClass('hackmd');
       // editing /Sidebar
-      if (window.location.pathname === '/Sidebar') {
-        $('body').addClass('pathname-sidebar');
+      if (isSidebar) {
+        $('body').addClass('editing-sidebar');
       }
       break;
     case EditorMode.HackMD:
       $('body').addClass('on-edit');
       $('body').addClass('hackmd');
       $('body').removeClass('builtin-editor');
-      $('body').removeClass('pathname-sidebar');
+      $('body').removeClass('editing-sidebar');
       break;
   }
 };
@@ -133,6 +133,9 @@ export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
   const isEditable = !isLoading && _isEditable;
   const initialData = isEditable ? editorModeByHash : EditorMode.View;
 
+  const { data: currentPagePath } = useCurrentPagePath();
+  const isSidebar = currentPagePath === '/Sidebar';
+
   const swrResponse = useSWRImmutable(
     isLoading ? null : ['editorMode', isEditable],
     null,
@@ -142,7 +145,7 @@ export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
   // initial updating
   if (!isEditorModeLoaded && !isLoading && swrResponse.data != null) {
     if (isEditable) {
-      updateBodyClassesByEditorMode(swrResponse.data);
+      updateBodyClassesByEditorMode(swrResponse.data, isSidebar);
     }
     isEditorModeLoaded = true;
   }
@@ -155,7 +158,7 @@ export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
       if (!isEditable) {
         return Promise.resolve(EditorMode.View); // fixed if not editable
       }
-      updateBodyClassesByEditorMode(editorMode);
+      updateBodyClassesByEditorMode(editorMode, isSidebar);
       updateHashByEditorMode(editorMode);
       return swrResponse.mutate(editorMode, shouldRevalidate);
     },

+ 1 - 1
packages/app/src/styles/_on-edit.scss

@@ -246,7 +246,7 @@ body.on-edit {
   // .builtin-editor .tab-pane#edit
 
   // editing /Sidebar
-  &.pathname-sidebar {
+  &.editing-sidebar {
     .page-editor-preview-body {
       width: 320px;
       padding-top: 0;

+ 1 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -538,7 +538,7 @@ body.on-edit {
 /*
  * Preview for editing /Sidebar
  */
-body.pathname-sidebar {
+body.editing-sidebar {
   .page-editor-preview-body {
     color: $color-sidebar-context;
     background-color: $bgcolor-sidebar-context;

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js

@@ -42,7 +42,7 @@ export default class RefsPostRenderInterceptor extends BasicInterceptor {
         const refsContext = (tagContext.method === 'gallery')
           ? new GalleryContext(tagContext || {})
           : new RefsContext(tagContext || {});
-        refsContext.fromPagePath = context.currentPagePath;
+        refsContext.fromPagePath = context.pagePath ?? context.currentPathname;
 
         this.renderReactDom(refsContext, elem);
       }

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

@@ -43,7 +43,7 @@ export class LsxPostRenderInterceptor extends BasicInterceptor {
       if (elem) {
         // instanciate LsxContext from context
         const lsxContext = new LsxContext(context.lsxContextMap[domId] || {});
-        lsxContext.fromPagePath = context.currentPagePath;
+        lsxContext.fromPagePath = context.pagePath ?? context.currentPathname;
 
         this.renderReactDOM(lsxContext, elem, isPreview);
       }

+ 4 - 3
packages/plugin-pukiwiki-like-linker/src/resource/js/util/PreProcessor/PukiwikiLikeLinker.js

@@ -2,14 +2,15 @@ const path = require('path');
 
 export default class PukiwikiLikeLinker {
 
-  process(markdown) {
+  process(markdown, context) {
+    const currentPath = context.pagePath ?? context.currentPathname;
 
     return markdown
       // see: https://regex101.com/r/k2dwz3/3
       .replace(/\[\[(([^(\]\])]+)>)?(.+?)\]\]/g, (all, group1, group2, group3) => {
         // create url
-        // use 'group3' as is if starts from 'http(s)', otherwise join to 'window.location.pathname'
-        const url = (group3.match(/^(\/|https?:\/\/)/)) ? group3 : path.join(window.location.pathname, group3);
+        // use 'group3' as is if starts from 'http(s)', otherwise join to currentPath
+        const url = (group3.match(/^(\/|https?:\/\/)/)) ? group3 : path.join(currentPath, group3);
         // determine alias string
         // if 'group2' is undefined, use group3
         const alias = group2 || group3;