Explorar o código

typescriptize RevisionRenderer

Yuki Takei %!s(int64=3) %!d(string=hai) anos
pai
achega
61ab8224c2
Modificáronse 1 ficheiros con 109 adicións e 147 borrados
  1. 109 147
      packages/app/src/components/Page/RevisionRenderer.tsx

+ 109 - 147
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -1,141 +1,124 @@
-import React from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
 
-import PropTypes from 'prop-types';
-
-import AppContainer from '~/client/services/AppContainer';
 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 { useEditorSettings } from '~/stores/editor';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
+import loggerFactory from '~/utils/logger';
 
 import RevisionBody from './RevisionBody';
 
-import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
-class LegacyRevisionRenderer extends React.PureComponent {
 
-  constructor(props) {
-    super(props);
+function getHighlightedBody(body: string, _keywords: string | string[]): string {
+  const normalizedKeywordsArray: string[] = [];
 
-    this.state = {
-      html: '',
-    };
+  const keywords = (typeof _keywords === 'string') ? [_keywords] : _keywords;
 
-    this.renderHtml = this.renderHtml.bind(this);
-    this.getHighlightedBody = this.getHighlightedBody.bind(this);
+  if (keywords.length === 0) {
+    return body;
   }
 
-  initCurrentRenderingContext() {
-    this.currentRenderingContext = {
-      markdown: this.props.markdown,
-      pagePath: this.props.pagePath,
-      renderDrawioInRealtime: this.props.editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(window.location.pathname),
-    };
+  // !!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
   }
-
-  componentDidMount() {
-    this.initCurrentRenderingContext();
-    this.renderHtml();
+  catch (err) {
+    logger.debug('Failed to initialize regex:', err);
   }
 
-  componentDidUpdate(prevProps) {
-    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
-    const { markdown, highlightKeywords } = this.props;
+  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
 
-    // render only when props.markdown is updated
-    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
-      this.initCurrentRenderingContext();
-      this.renderHtml();
-      return;
-    }
+  const insideTagRegex = /<[^<>]*>/g;
+  const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
 
-    const HeaderLink = document.getElementsByClassName('revision-head-link');
-    const HeaderLinkArray = Array.from(HeaderLink);
-    addSmoothScrollEvent(HeaderLinkArray, blinkElem);
+  const insideTagStrs = body.match(insideTagRegex);
+  const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
 
-    const { interceptorManager } = window;
+  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)
 
-    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
+    const arr: string[] = [];
+    insideTagStrs.forEach((str, i) => {
+      arr.push(str);
+      arr.push(betweenTagStrs[i]);
+    });
+    returnBody = arr.join('');
+  }
+  else {
+    // inferior highlighter
+    returnBody = highlighter2(body);
   }
 
-  /**
-   * 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);
-    });
+  return returnBody;
+}
 
-    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);
-    }
+type Props = {
+  growiRenderer: GrowiRenderer,
+  markdown: string,
+  pagePath: string,
+  highlightKeywords?: string | string[],
+  additionalClassName?: string,
+}
 
-    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 RevisionRenderer = (props: Props): JSX.Element => {
 
-    const insideTagRegex = /<[^<>]*>/g;
-    const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
+  const { interceptorManager } = (window as CustomWindow);
+  const {
+    growiRenderer, markdown, pagePath, highlightKeywords,
+  } = props;
 
-    const insideTagStrs = body.match(insideTagRegex);
-    const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
+  const [html, setHtml] = useState('');
 
-    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 { data: editorSettings } = useEditorSettings();
 
-      const arr = [];
-      insideTagStrs.forEach((str, i) => {
-        arr.push(str);
-        arr.push(betweenTagStrs[i]);
-      });
-      returnBody = arr.join('');
-    }
-    else {
-      // inferior highlighter
-      returnBody = highlighter2(body);
-    }
+  const currentRenderingContext = useMemo(() => {
+    return {
+      markdown,
+      parsedHTML: '',
+      pagePath,
+      renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
+      currentPathname: decodeURIComponent(window.location.pathname),
+    };
+  }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
 
-    return returnBody;
-  }
 
-  async renderHtml() {
-    const {
-      appContainer, growiRenderer,
-      highlightKeywords,
-    } = this.props;
+  const renderHtml = useCallback(async() => {
+    if (interceptorManager == null) {
+      return;
+    }
 
-    const { interceptorManager } = window;
-    const context = this.currentRenderingContext;
+    const context = currentRenderingContext;
 
     await interceptorManager.process('preRender', context);
     await interceptorManager.process('prePreProcess', context);
@@ -146,60 +129,39 @@ class LegacyRevisionRenderer extends React.PureComponent {
     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);
+    if (highlightKeywords != null && !isMarkdownEmpty) {
+      context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
     }
     await interceptorManager.process('postPostProcess', context);
     await interceptorManager.process('preRenderHtml', context);
 
-    this.setState({ html: context.parsedHTML });
-  }
-
-  render() {
-    const config = this.props.appContainer.getConfig();
-    const isMathJaxEnabled = !!config.env.MATHJAX;
-
-    return (
-      <RevisionBody
-        html={this.state.html}
-        isMathJaxEnabled={isMathJaxEnabled}
-        additionalClassName={this.props.additionalClassName}
-        renderMathJaxOnInit
-      />
-    );
-  }
-
-}
+    setHtml(context.parsedHTML);
+  }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
 
-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,
-  editorSettings: PropTypes.any,
-};
+  useEffect(() => {
+    if (interceptorManager == null) {
+      return;
+    }
 
-/**
- * Wrapper component for using unstated
- */
-const LegacyRevisionRendererWrapper = withUnstatedContainers(LegacyRevisionRenderer, [AppContainer]);
+    renderHtml()
+      .then(() => {
+        const HeaderLink = document.getElementsByClassName('revision-head-link');
+        const HeaderLinkArray = Array.from(HeaderLink);
+        addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
 
+        interceptorManager.process('postRenderHtml', currentRenderingContext);
+      });
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const RevisionRenderer = (props) => {
-  const { data: editorSettings } = useEditorSettings();
+  }, [currentRenderingContext, interceptorManager, renderHtml]);
 
-  return <LegacyRevisionRendererWrapper {...props} editorSettings={editorSettings} />;
-};
+  return (
+    <RevisionBody
+      html={html}
+      additionalClassName={props.additionalClassName}
+      renderMathJaxOnInit
+    />
+  );
 
-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;