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

Merge branch 'support/apply-nextjs-2' into support/apply-remark

Yuki Takei 3 жил өмнө
parent
commit
6dc17e186d

+ 0 - 2
packages/app/src/client/legacy/crowi.js

@@ -1,5 +1,3 @@
-const { blinkElem, blinkSectionHeaderAtBoot } = require('../util/blink-section-header');
-
 /* eslint-disable react/jsx-filename-extension */
 /* eslint-disable react/jsx-filename-extension */
 require('jquery.cookie');
 require('jquery.cookie');
 
 

+ 1 - 0
packages/app/src/client/util/blink-section-header.ts

@@ -23,5 +23,6 @@ export const blinkSectionHeaderAtBoot = (): HTMLElement | undefined => {
   const elem = document.getElementById(id);
   const elem = document.getElementById(id);
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
     blinkElem(elem);
     blinkElem(elem);
+    return elem;
   }
   }
 };
 };

+ 16 - 3
packages/app/src/components/Page.jsx

@@ -1,13 +1,16 @@
-import React, { useEffect, useRef } from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-
+import { debounce } from 'throttle-debounce';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import MarkdownTable from '~/client/models/MarkdownTable';
+import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import GrowiRenderer from '~/services/renderer/growi-renderer';
 import GrowiRenderer from '~/services/renderer/growi-renderer';
 import {
 import {
-  useIsGuestUser,
+  useIsGuestUser, useIsBlinkedHeaderAtBoot,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
@@ -188,9 +191,19 @@ export const Page = (props) => {
   const { data: pageTags } = usePageTagsForEditors();
   const { data: pageTags } = usePageTagsForEditors();
   const { data: growiRenderer } = useViewRenderer();
   const { data: growiRenderer } = useViewRenderer();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isBlinkedAtBoot, mutate: mutateBlinkedAtBoot } = useIsBlinkedHeaderAtBoot();
 
 
   const pageRef = useRef(null);
   const pageRef = useRef(null);
 
 
+  useEffect(() => {
+    if (isBlinkedAtBoot) {
+      return;
+    }
+
+    blinkSectionHeaderAtBoot();
+    mutateBlinkedAtBoot(true);
+  }, [mutateBlinkedAtBoot]);
+
   // // set handler to open DrawioModal
   // // set handler to open DrawioModal
   // useEffect(() => {
   // useEffect(() => {
   //   const handler = (beginLineNumber, endLineNumber) => {
   //   const handler = (beginLineNumber, endLineNumber) => {

+ 0 - 197
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -1,197 +0,0 @@
-import React from 'react';
-
-import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
-import PropTypes from 'prop-types';
-
-import { blinkElem } from '~/client/util/blink-section-header';
-import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
-import InterceptorManager from '~/services/interceptor-manager';
-import GrowiRenderer from '~/services/renderer/growi-renderer';
-import { useInterceptorManager } from '~/stores/context';
-import { useEditorSettings } from '~/stores/editor';
-
-import RevisionBody from './RevisionBody';
-
-
-const logger = loggerFactory('components:Page:RevisionRenderer');
-
-class LegacyRevisionRenderer extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      html: '',
-    };
-
-    this.renderHtml = this.renderHtml.bind(this);
-    this.getHighlightedBody = this.getHighlightedBody.bind(this);
-  }
-
-  initCurrentRenderingContext() {
-    this.currentRenderingContext = {
-      markdown: this.props.markdown,
-      pagePath: this.props.pagePath,
-      renderDrawioInRealtime: this.props.editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(window.location.pathname),
-    };
-  }
-
-  componentDidMount() {
-    this.initCurrentRenderingContext();
-    this.renderHtml();
-  }
-
-  componentDidUpdate(prevProps) {
-    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
-    const { markdown, highlightKeywords, interceptorManager } = this.props;
-
-    // render only when props.markdown is updated
-    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
-      this.initCurrentRenderingContext();
-      this.renderHtml();
-      return;
-    }
-
-    const HeaderLink = document.getElementsByClassName('revision-head-link');
-    const HeaderLinkArray = Array.from(HeaderLink);
-    addSmoothScrollEvent(HeaderLinkArray, blinkElem);
-
-    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
-  }
-
-  /**
-   * transplanted from legacy code -- Yuki Takei
-   * @param {string} body html strings
-   * @param {string} keywords
-   */
-  getHighlightedBody(body, keywords) {
-    const normalizedKeywordsArray = [];
-    // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
-    // Separate keywords
-    // - Surrounded by double quotation
-    // - Split by both full-width and half-width spaces
-    // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
-    keywords.forEach((keyword, i) => {
-      if (keyword === '') {
-        return;
-      }
-      const k = keyword
-        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
-        .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
-      normalizedKeywordsArray.push(k);
-    });
-
-    const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
-    const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
-    let keywordRegexp2 = keywordRegxp;
-
-    // for non-chrome browsers compatibility
-    try {
-      // eslint-disable-next-line regex/invalid
-      keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
-    }
-    catch (err) {
-      logger.debug('Failed to initialize regex:', err);
-    }
-
-    const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
-    const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
-
-    const insideTagRegex = /<[^<>]*>/g;
-    const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
-
-    const insideTagStrs = body.match(insideTagRegex);
-    const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
-
-    let returnBody = body;
-    const isSafeHtml = insideTagStrs.length === betweenTagMatches.length + 1; // to check whether is safe to join
-    if (isSafeHtml) {
-      // highlight
-      const betweenTagStrs = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
-
-      const arr = [];
-      insideTagStrs.forEach((str, i) => {
-        arr.push(str);
-        arr.push(betweenTagStrs[i]);
-      });
-      returnBody = arr.join('');
-    }
-    else {
-      // inferior highlighter
-      returnBody = highlighter2(body);
-    }
-
-    return returnBody;
-  }
-
-  async renderHtml() {
-    const {
-      interceptorManager,
-      growiRenderer,
-      highlightKeywords,
-    } = this.props;
-
-    if (interceptorManager == null || growiRenderer == null) {
-      return;
-    }
-
-    const context = this.currentRenderingContext;
-
-    await interceptorManager.process('preRender', 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);
-
-    const isMarkdownEmpty = context.markdown.trim().length === 0;
-    if (highlightKeywords != null && highlightKeywords.length > 0 && !isMarkdownEmpty) {
-      context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
-    }
-    await interceptorManager.process('postPostProcess', context);
-    await interceptorManager.process('preRenderHtml', context);
-
-    this.setState({ html: context.parsedHTML });
-  }
-
-  render() {
-    return (
-      <RevisionBody
-        html={this.state.html}
-        additionalClassName={this.props.additionalClassName}
-        renderMathJaxOnInit
-      />
-    );
-  }
-
-}
-
-LegacyRevisionRenderer.propTypes = {
-  interceptorManager: PropTypes.instanceOf(InterceptorManager).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,
-  editorSettings: PropTypes.any,
-};
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const RevisionRenderer = (props) => {
-  const { data: interceptorManager } = useInterceptorManager();
-  const { data: editorSettings } = useEditorSettings();
-
-  return <LegacyRevisionRenderer {...props} interceptorManager={interceptorManager} editorSettings={editorSettings} />;
-};
-
-RevisionRenderer.propTypes = {
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.arrayOf(PropTypes.string),
-  additionalClassName: PropTypes.string,
-};
-
-export default RevisionRenderer;

+ 169 - 0
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -0,0 +1,169 @@
+import React, {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
+
+import { blinkElem } from '~/client/util/blink-section-header';
+import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { CustomWindow } from '~/interfaces/global';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { useCurrentPathname, useInterceptorManager } from '~/stores/context';
+import { useEditorSettings } from '~/stores/editor';
+import loggerFactory from '~/utils/logger';
+
+import RevisionBody from './RevisionBody';
+
+
+const logger = loggerFactory('components:Page:RevisionRenderer');
+
+
+function getHighlightedBody(body: string, _keywords: string | string[]): string {
+  const normalizedKeywordsArray: string[] = [];
+
+  const keywords = (typeof _keywords === 'string') ? [_keywords] : _keywords;
+
+  if (keywords.length === 0) {
+    return body;
+  }
+
+  // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
+  // Separate keywords
+  // - Surrounded by double quotation
+  // - Split by both full-width and half-width spaces
+  // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
+  keywords.forEach((keyword, i) => {
+    if (keyword === '') {
+      return;
+    }
+    const k = keyword
+      .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
+      .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
+    normalizedKeywordsArray.push(k);
+  });
+
+  const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
+  const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
+  let keywordRegexp2 = keywordRegxp;
+
+  // for non-chrome browsers compatibility
+  try {
+    // eslint-disable-next-line regex/invalid
+    keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
+  }
+  catch (err) {
+    logger.debug('Failed to initialize regex:', err);
+  }
+
+  const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
+  const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
+
+  const insideTagRegex = /<[^<>]*>/g;
+  const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
+
+  const insideTagStrs = body.match(insideTagRegex);
+  const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
+
+  let returnBody = body;
+  const isSafeHtml = insideTagStrs?.length === betweenTagMatches.length + 1; // to check whether is safe to join
+  if (isSafeHtml) {
+    // highlight
+    const betweenTagStrs: string[] = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
+
+    const arr: string[] = [];
+    insideTagStrs.forEach((str, i) => {
+      arr.push(str);
+      arr.push(betweenTagStrs[i]);
+    });
+    returnBody = arr.join('');
+  }
+  else {
+    // inferior highlighter
+    returnBody = highlighter2(body);
+  }
+
+  return returnBody;
+}
+
+
+type Props = {
+  growiRenderer: GrowiRenderer,
+  markdown: string,
+  pagePath: string,
+  highlightKeywords?: string | string[],
+  additionalClassName?: string,
+}
+
+const RevisionRenderer = (props: Props): JSX.Element => {
+
+  const {
+    growiRenderer, markdown, pagePath, highlightKeywords,
+  } = props;
+
+  const [html, setHtml] = useState('');
+
+  const { data: interceptorManager } = useInterceptorManager();
+  const { data: editorSettings } = useEditorSettings();
+  const { data: currentPathname } = useCurrentPathname();
+
+  const currentRenderingContext = useMemo(() => {
+    return {
+      markdown,
+      parsedHTML: '',
+      pagePath,
+      renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
+      currentPathname: decodeURIComponent(currentPathname ?? '/'),
+    };
+  }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
+
+
+  const renderHtml = useCallback(async() => {
+    if (interceptorManager == null) {
+      return;
+    }
+
+    const context = currentRenderingContext;
+
+    await interceptorManager.process('preRender', 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);
+
+    const isMarkdownEmpty = context.markdown.trim().length === 0;
+    if (highlightKeywords != null && !isMarkdownEmpty) {
+      context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
+    }
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderHtml', context);
+
+    setHtml(context.parsedHTML);
+  }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
+
+  useEffect(() => {
+    if (interceptorManager == null) {
+      return;
+    }
+
+    renderHtml()
+      .then(() => {
+        // const HeaderLink = document.getElementsByClassName('revision-head-link');
+        // const HeaderLinkArray = Array.from(HeaderLink);
+        // addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
+
+        // interceptorManager.process('postRenderHtml', currentRenderingContext);
+      });
+
+  }, [currentRenderingContext, interceptorManager, renderHtml]);
+
+  return (
+    <RevisionBody
+      html={html}
+      additionalClassName={props.additionalClassName}
+      renderMathJaxOnInit
+    />
+  );
+
+};
+
+export default RevisionRenderer;

+ 3 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -53,7 +53,8 @@ import {
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
   useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri,
   useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri,
-  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname, useIsSlackConfigured, useGrowiRendererConfig,
+  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
+  useIsSlackConfigured, useGrowiRendererConfig, useIsBlinkedHeaderAtBoot,
 } from '../stores/context';
 } from '../stores/context';
 import { useXss } from '../stores/xss';
 import { useXss } from '../stores/xss';
 
 
@@ -160,6 +161,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   useIsSharedUser(false); // this page cann't be routed for '/share'
   useIsSharedUser(false); // this page cann't be routed for '/share'
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
+  useIsBlinkedHeaderAtBoot(false);
 
 
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -214,6 +214,10 @@ export const useGrowiRendererConfig = (initialData?: GrowiRendererConfig): SWRRe
   return useStaticSWR('growiRendererConfig', initialData);
   return useStaticSWR('growiRendererConfig', initialData);
 };
 };
 
 
+export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isBlinkedAtBoot', initialData);
+};
+
 
 
 /** **********************************************************
 /** **********************************************************
  *                     Computed contexts
  *                     Computed contexts