Sfoglia il codice sorgente

Merge branch 'support/apply-nextjs-PageComment-integrate' into support/ts-fc-CommnetControl

jam411 3 anni fa
parent
commit
7cfe399267

+ 5 - 1
packages/app/next.config.js

@@ -33,7 +33,9 @@ const setupTranspileModules = () => {
     'unified',
     'comma-separated-tokens',
     'decode-named-character-reference',
+    'hastscript',
     'html-void-elements',
+    'longest-streak',
     'property-information',
     'space-separated-tokens',
     'trim-lines',
@@ -41,7 +43,9 @@ const setupTranspileModules = () => {
     'vfile',
     'zwitch',
     'emoticon',
-    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
+    'direction', // for hast-util-select
+    'bcp-47-match', // for hast-util-select
+    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
 
   logger.info('{bold:Listing scoped packages for transpiling:}');

+ 3 - 0
packages/app/package.json

@@ -108,6 +108,7 @@
     "express-webpack-assets": "^0.1.0",
     "extensible-custom-error": "^0.0.7",
     "graceful-fs": "^4.1.11",
+    "hast-util-select": "^5.0.2",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "i18next-chained-backend": "^3.0.2",
@@ -155,6 +156,7 @@
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-katex": "^6.0.2",
     "rehype-raw": "^6.1.1",
     "rehype-sanitize": "^5.0.1",
     "rehype-slug": "^5.0.1",
@@ -162,6 +164,7 @@
     "remark-breaks": "^3.0.2",
     "remark-emoji": "^3.0.2",
     "remark-gfm": "^3.0.1",
+    "remark-math": "^5.1.1",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",

+ 3 - 0
packages/app/src/components/CommonStyles/katex.module.scss

@@ -0,0 +1,3 @@
+.katex-container :global {
+  @import '~katex/dist/katex.min';
+}

+ 28 - 2
packages/app/src/components/Navbar/AuthorInfo.module.scss

@@ -1,5 +1,31 @@
+@use '~/styles/bootstrap/init' as bs;
 
-.grw-author-info-skelton {
+$author-font-size: 12px;
+$date-font-size: 11px;
+
+.grw-author-info :global {
+  li {
+    font-size: $author-font-size;
+    list-style: none;
+  }
+
+  .text-date {
+    font-size: $date-font-size;
+  }
+
+  .picture {
+    width: 22px;
+    height: 22px;
+    border: 1px solid bs.$gray-300;
+
+    &.picture-xs {
+      width: 14px;
+      height: 14px;
+    }
+  }
+}
+
+.grw-author-info-skelton :global {
   width: 139px;
-  height: 32.84px;
+  height: calc((#{$author-font-size} + #{$date-font-size}) * #{bs.$line-height-base});
 }

+ 10 - 32
packages/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -88,42 +88,20 @@
       font-size: 16px;
     }
 
-    ul.authors {
-      li {
-        font-size: 12px;
-        list-style: none;
-      }
+    .user-list-popover {
+      max-width: 200px;
 
-      .text-date {
-        font-size: 11px;
-      }
+      .user-list-content {
+        direction: rtl;
 
-      .picture {
-        width: 22px;
-        height: 22px;
-        border: 1px solid bs.$gray-300;
-
-        &.picture-xs {
-          width: 14px;
-          height: 14px;
+        .liker-user-count,
+        .seen-user-count {
+          font-size: 12px;
+          font-weight: bolder;
         }
       }
-
-      .user-list-popover {
-        max-width: 200px;
-
-        .user-list-content {
-          direction: rtl;
-
-          .liker-user-count,
-          .seen-user-count {
-            font-size: 12px;
-            font-weight: bolder;
-          }
-        }
-        .cls-1 {
-          isolation: isolate;
-        }
+      .cls-1 {
+        isolation: isolate;
       }
     }
   }

+ 1 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -103,7 +103,7 @@ export const GrowiSubNavigation = (props: Props): JSX.Element => {
 
         {/* Page Authors */}
         { (showPageAuthors && !isCompactMode) && (
-          <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3">
+          <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
             <li className="pb-1">
               <AuthorInfo user={creator as IUser} date={createdAt} locate="subnav" />
             </li>

+ 1 - 1
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -35,7 +35,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
 
       <div id="edit-tags-btn-wrapper-for-tooltip">
         <a
-          className={`btn btn-link btn-edit-tags p-0 text-muted ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          className={`btn btn-link btn-edit-tags p-0 text-muted d-flex ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
           onClick={openEditorHandler}
         >
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}

+ 6 - 1
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -13,6 +13,8 @@ import loggerFactory from '~/utils/logger';
 
 // import RevisionBody from './RevisionBody';
 
+import katexStyles from '../CommonStyles/katex.module.scss';
+
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
@@ -100,7 +102,10 @@ const RevisionRenderer = (props: Props): JSX.Element => {
   } = props;
 
   return (
-    <ReactMarkdown {...rendererOptions} className={`wiki ${additionalClassName ?? ''}`}>
+    <ReactMarkdown
+      {...rendererOptions}
+      className={`wiki katex-container ${katexStyles['katex-container']} ${additionalClassName ?? ''}`}
+    >
       {markdown}
     </ReactMarkdown>
   );

+ 5 - 2
packages/app/src/components/Page/TagLabels.module.scss

@@ -1,8 +1,10 @@
 @use '~/styles/bootstrap/init' as bs;
 
+$grw-tag-label-font-size: 12px;
+
 .grw-tag-labels :global {
   .grw-tag-label {
-    font-size: 12px;
+    font-size: $grw-tag-label-font-size;
     font-weight: normal;
     border-radius: bs.$border-radius;
   }
@@ -11,5 +13,6 @@
 
 .grw-tag-labels-skelton :global {
   width: 137px;
-  height: 21.99px;
+  height: calc(#{$grw-tag-label-font-size} + #{bs.$badge-padding-y} * 2);
+  font-size: $grw-tag-label-font-size; // set font-size to use the same em value in bs.$badge-padding-y(https://getbootstrap.jp/docs/5.0/components/badge/#variables)
 }

+ 2 - 2
packages/app/src/components/Page/TagLabels.tsx

@@ -27,7 +27,7 @@ const TagLabels:FC<Props> = (props: Props) => {
 
   return (
     <>
-      <form className={`${styles['grw-tag-labels']} grw-tag-labels form-inline`}>
+      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`}>
         <i className="tag-icon icon-tag mr-2"></i>
         { tags == null
           ? (
@@ -41,7 +41,7 @@ const TagLabels:FC<Props> = (props: Props) => {
             />
           )
         }
-      </form>
+      </div>
 
       <TagEditModal
         tags={tags}

+ 1 - 1
packages/app/src/components/PageComment.tsx

@@ -17,7 +17,7 @@ import { useSWRxPageComment } from '../stores/comment';
 import { Comment } from './PageComment/Comment';
 import { CommentEditor } from './PageComment/CommentEditor';
 import { CommentEditorLazyRenderer } from './PageComment/CommentEditorLazyRenderer';
-import DeleteCommentModal from './PageComment/DeleteCommentModal';
+import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
 import { ReplyComments } from './PageComment/ReplyComments';
 
 type Props = {

+ 0 - 70
packages/app/src/components/PageComment/DeleteCommentModal.jsx

@@ -1,70 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import {
-  Button, Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { format } from 'date-fns';
-
-import { UserPicture } from '@growi/ui';
-import Username from '../User/Username';
-
-export default class DeleteCommentModal extends React.Component {
-
-  /*
-   * the threshold for omitting body
-   */
-  static get OMIT_BODY_THRES() { return 400 }
-
-  UNSAFE_componentWillMount() {
-  }
-
-  render() {
-    if (this.props.comment === undefined) {
-      return <div></div>;
-    }
-
-    const comment = this.props.comment;
-    const commentDate = format(new Date(comment.createdAt), 'yyyy/MM/dd HH:mm');
-
-    // generate body
-    let commentBody = comment.comment;
-    if (commentBody.length > DeleteCommentModal.OMIT_BODY_THRES) { // omit
-      commentBody = `${commentBody.substr(0, DeleteCommentModal.OMIT_BODY_THRES)}...`;
-    }
-    commentBody = <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
-
-    return (
-      <Modal isOpen={this.props.isShown} toggle={this.props.cancel} className="page-comment-delete-modal">
-        <ModalHeader tag="h4" toggle={this.props.cancel} className="bg-danger text-light">
-          <span>
-            <i className="icon-fw icon-fire"></i>
-            Delete comment?
-          </span>
-        </ModalHeader>
-        <ModalBody>
-          <UserPicture user={comment.creator} size="xs" /> <strong><Username user={comment.creator}></Username></strong> wrote on {commentDate}:
-          <p className="card well comment-body mt-2 p-2">{commentBody}</p>
-        </ModalBody>
-        <ModalFooter>
-          <span className="text-danger">{this.props.errorMessage}</span>&nbsp;
-          <Button onClick={this.props.cancel}>Cancel</Button>
-          <Button color="danger" onClick={this.props.confirmedToDelete}>
-            <i className="icon icon-fire"></i>
-            Delete
-          </Button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-DeleteCommentModal.propTypes = {
-  isShown: PropTypes.bool.isRequired,
-  comment: PropTypes.object,
-  errorMessage: PropTypes.string,
-  cancel: PropTypes.func.isRequired, // for cancel evnet handling
-  confirmedToDelete: PropTypes.func.isRequired, // for confirmed event handling
-};

+ 64 - 0
packages/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -0,0 +1,64 @@
+import React from 'react';
+
+import { UserPicture } from '@growi/ui';
+import { format } from 'date-fns';
+import {
+  Button, Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { ICommentHasId } from '../../interfaces/comment';
+import Username from '../User/Username';
+
+
+type DeleteCommentModalProps = {
+  isShown: boolean,
+  comment: ICommentHasId,
+  errorMessage: string,
+  cancel: () => void, // for cancel evnet handling
+  confirmedToDelete: () => void, // for confirmed event handling
+}
+
+export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element => {
+
+  const {
+    isShown, comment, errorMessage, cancel, confirmedToDelete,
+  } = props;
+
+  /*
+   * the threshold for omitting body
+   */
+  const OMIT_BODY_THRES = 400;
+
+  const commentDate = format(new Date(comment.createdAt), 'yyyy/MM/dd HH:mm');
+
+  // generate body
+  let commentBody = comment.comment;
+  if (commentBody.length > OMIT_BODY_THRES) { // omit
+    commentBody = `${commentBody.substr(0, OMIT_BODY_THRES)}...`;
+  }
+  const commentBodyElement = <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
+
+  return (
+    <Modal isOpen={isShown} toggle={cancel} className="page-comment-delete-modal">
+      <ModalHeader tag="h4" toggle={cancel} className="bg-danger text-light">
+        <span>
+          <i className="icon-fw icon-fire"></i>
+          Delete comment?
+        </span>
+      </ModalHeader>
+      <ModalBody>
+        <UserPicture user={comment.creator} size="xs" /> <strong><Username user={comment.creator}></Username></strong> wrote on {commentDate}:
+        <p className="card well comment-body mt-2 p-2">{commentBodyElement}</p>
+      </ModalBody>
+      <ModalFooter>
+        <span className="text-danger">{errorMessage}</span>&nbsp;
+        <Button onClick={cancel}>Cancel</Button>
+        <Button color="danger" onClick={confirmedToDelete}>
+          <i className="icon icon-fire"></i>
+          Delete
+        </Button>
+      </ModalFooter>
+    </Modal>
+  );
+
+};

+ 5 - 0
packages/app/src/components/PageContentFooter.module.scss

@@ -0,0 +1,5 @@
+// TODO: Should Soft Coding see: https://github.com/weseek/growi/pull/6404
+.page-content-footer-skelton :global {
+  width: 300px;
+  height: 20px;
+}

+ 3 - 1
packages/app/src/components/PageContentFooter.tsx

@@ -6,10 +6,12 @@ import { useSWRxCurrentPage } from '~/stores/page';
 
 import { Skelton } from './Skelton';
 
+import styles from './PageContentFooter.module.scss';
+
 export const PageContentFooter = memo((): JSX.Element => {
 
   const AuthorInfo = dynamic(() => import('./Navbar/AuthorInfo'),
-    { ssr: false, loading: () => <Skelton width={300} height={20} additionalClass={'mb-3'} /> });
+    { ssr: false, loading: () => <Skelton additionalClass={`${styles['page-content-footer-skelton']} mb-3`} /> });
 
   const { data: page } = useSWRxCurrentPage();
 

+ 4 - 0
packages/app/src/interfaces/services/renderer.ts

@@ -1,3 +1,5 @@
+import { HastNode } from 'hast-util-select';
+
 import { XssOptionConfig } from '~/services/xss/xssOption';
 
 // export type GrowiHydratedEnv = {
@@ -18,3 +20,5 @@ export type RendererConfig = {
   plantumlUri: string | null,
   blockdiagUri: string | null,
 } & XssOptionConfig;
+
+export type RehypePlugin = (option: any) => (node: HastNode) => void

+ 39 - 0
packages/app/src/services/renderer/rehype-plugins/add-class.ts

@@ -0,0 +1,39 @@
+// See: https://github.com/martypdx/rehype-add-classes for the original implementation.
+// Re-implemeted in TypeScript.
+
+import { selectAll, HastNode, Element } from 'hast-util-select';
+
+import { RehypePlugin } from '~/interfaces/services/renderer';
+
+export type SelectorName = string; // e.g. 'h1'
+export type ClassName = string; // e.g. 'header'
+export type Additions = Record<SelectorName, ClassName>;
+export type AdditionsEntry = [SelectorName, ClassName];
+
+const generateWriter = (className: string) => (element: Element) => {
+  const { properties } = element;
+
+  if (properties == null) {
+    return;
+  }
+
+  if (properties.className == null) {
+    properties.className = className;
+    return;
+  }
+
+  properties.className += ` ${className}`;
+};
+
+const adder = (entry: AdditionsEntry) => {
+  const [selectorName, className] = entry;
+  const writer = generateWriter(className);
+
+  return (node: HastNode) => selectAll(selectorName, node).forEach(writer);
+};
+
+export const addClass: RehypePlugin = (additions) => {
+  const adders = Object.entries(additions).map(adder);
+
+  return node => adders.forEach(a => a(node));
+};

+ 8 - 0
packages/app/src/services/renderer/renderer.tsx

@@ -1,4 +1,5 @@
 import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import katex from 'rehype-katex';
 import raw from 'rehype-raw';
 import sanitize, { defaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
@@ -6,10 +7,12 @@ import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import gfm from 'remark-gfm';
+import math from 'remark-math';
 
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { RendererConfig } from '~/interfaces/services/renderer';
+import { addClass } from '~/services/renderer/rehype-plugins/add-class';
 import loggerFactory from '~/utils/logger';
 
 // import CsvToTable from './PreProcessor/CsvToTable';
@@ -225,6 +228,9 @@ const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererCo
           '*': ['className', 'class'],
         },
       }],
+      [addClass, {
+        table: 'table table-bordered',
+      }],
     ],
     components: {
       a: NextLink,
@@ -244,6 +250,7 @@ export const generateViewOptions = (
   // add remark plugins
   if (remarkPlugins != null) {
     remarkPlugins.push(emoji);
+    remarkPlugins.push(math);
     if (config.isEnabledLinebreaks) {
       remarkPlugins.push(breaks);
     }
@@ -251,6 +258,7 @@ export const generateViewOptions = (
 
   // store toc node
   if (rehypePlugins != null) {
+    rehypePlugins.push(katex);
     rehypePlugins.push([toc, {
       nav: false,
       headings: ['h1', 'h2', 'h3'],

+ 163 - 1
yarn.lock

@@ -4344,6 +4344,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/katex@^0.11.0":
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.1.tgz#34de04477dcf79e2ef6c8d23b41a3d81f9ebeaf5"
+  integrity sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==
+
 "@types/ldapjs@^1.0.9":
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/@types/ldapjs/-/ldapjs-1.0.11.tgz#34077176af2b06186bd54e4a38ceb6e852387fa4"
@@ -5693,6 +5698,11 @@ batch@0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
 
+bcp-47-match@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-2.0.2.tgz#3323e221eb5b40ddc3b91ed29d847ab459d549c4"
+  integrity sha512-zy5swVXwQ25ttElhoN9Dgnqm6VFlMkeDNljvHSGqGNr4zClUosdFzxD+fQHJVmx3g3KY+r//wV/fmBHsa1ErnA==
+
 bcrypt-pbkdf@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@@ -5853,6 +5863,11 @@ body-parser@1.19.0:
     raw-body "2.4.0"
     type-is "~1.6.17"
 
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
 bootstrap@^4.6.1:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.1.tgz#bc25380c2c14192374e8dec07cf01b2742d222a2"
@@ -6937,7 +6952,7 @@ commander@^6.2.0, commander@^6.2.1:
   resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
   integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
 
-commander@^8.1.0:
+commander@^8.0.0, commander@^8.1.0:
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
@@ -7553,6 +7568,11 @@ css-in-js-utils@^2.0.0:
     hyphenate-style-name "^1.0.2"
     isobject "^3.0.1"
 
+css-selector-parser@^1.0.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759"
+  integrity sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==
+
 cssesc@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -8068,6 +8088,11 @@ dir-glob@^3.0.1:
   dependencies:
     path-type "^4.0.0"
 
+direction@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/direction/-/direction-2.0.1.tgz#71800dd3c4fa102406502905d3866e65bdebb985"
+  integrity sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==
+
 disparity@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/disparity/-/disparity-3.0.0.tgz#605288e8ebf38c5ccfe1e0dbc49ca6f724096500"
@@ -10862,6 +10887,14 @@ hast-util-heading-rank@^2.0.0:
   dependencies:
     "@types/hast" "^2.0.0"
 
+hast-util-is-element@^2.0.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz#fc0b0dc7cef3895e839b8d66979d57b0338c68f3"
+  integrity sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    "@types/unist" "^2.0.0"
+
 hast-util-parse-selector@^2.0.0:
   version "2.2.5"
   resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
@@ -10898,6 +10931,28 @@ hast-util-sanitize@^4.0.0:
   dependencies:
     "@types/hast" "^2.0.0"
 
+hast-util-select@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/hast-util-select/-/hast-util-select-5.0.2.tgz#8c603ebacf0f47e154c5fa2e5b7efc520813866b"
+  integrity sha512-QGN5o7N8gq1BhUX96ApLE8izOXlf+IPkOVGXcp9Dskdd3w0OqZrn6faPAmS0/oVogwJOd0lWFSYmBK75e+030g==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    "@types/unist" "^2.0.0"
+    bcp-47-match "^2.0.0"
+    comma-separated-tokens "^2.0.0"
+    css-selector-parser "^1.0.0"
+    direction "^2.0.0"
+    hast-util-has-property "^2.0.0"
+    hast-util-is-element "^2.0.0"
+    hast-util-to-string "^2.0.0"
+    hast-util-whitespace "^2.0.0"
+    not "^0.1.0"
+    nth-check "^2.0.0"
+    property-information "^6.0.0"
+    space-separated-tokens "^2.0.0"
+    unist-util-visit "^4.0.0"
+    zwitch "^2.0.0"
+
 hast-util-to-parse5@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.0.0.tgz#a39808e69005d10afeed1866029a1fb137df3f7c"
@@ -10917,6 +10972,15 @@ hast-util-to-string@^2.0.0:
   dependencies:
     "@types/hast" "^2.0.0"
 
+hast-util-to-text@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz#b7699a75f7a61af6e0befb67660cd78460d96dc6"
+  integrity sha512-7S3mOBxACy8syL45hCn3J7rHqYaXkxRfsX6LXEU5Shz4nt4GxdjtMUtG+T6G/ZLUHd7kslFAf14kAN71bz30xA==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    hast-util-is-element "^2.0.0"
+    unist-util-find-after "^4.0.0"
+
 hast-util-whitespace@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz#4fc1086467cc1ef5ba20673cb6b03cec3a970f1c"
@@ -12959,6 +13023,20 @@ kareem@2.3.2:
   resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93"
   integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==
 
+katex@^0.13.0:
+  version "0.13.24"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.24.tgz#fe55455eb455698cb24b911a353d16a3c855d905"
+  integrity sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==
+  dependencies:
+    commander "^8.0.0"
+
+katex@^0.15.0:
+  version "0.15.6"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.15.6.tgz#c4e2f6ced2ac4de1ef6f737fe7c67d3026baa0e5"
+  integrity sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==
+  dependencies:
+    commander "^8.0.0"
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -13918,6 +13996,15 @@ mdast-util-gfm@^2.0.0:
     mdast-util-gfm-task-list-item "^1.0.0"
     mdast-util-to-markdown "^1.0.0"
 
+mdast-util-math@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/mdast-util-math/-/mdast-util-math-2.0.1.tgz#141b8e7e43731d2a7423c5eb8c0335c05d257ad2"
+  integrity sha512-ZZtjyRwobsiVg4bY0Q5CzAZztpbjRIA7ZlMMb0PNkwTXOnJTUoHvzBhVG95LIuek5Mlj1l2P+jBvWviqW7G+0A==
+  dependencies:
+    "@types/mdast" "^3.0.0"
+    longest-streak "^3.0.0"
+    mdast-util-to-markdown "^1.3.0"
+
 mdast-util-to-hast@^12.1.0:
   version "12.1.2"
   resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.2.tgz#5c793b04014746585254c7ce0bc2d117201a5d1d"
@@ -14267,6 +14354,19 @@ micromark-extension-gfm@^2.0.0:
     micromark-util-combine-extensions "^1.0.0"
     micromark-util-types "^1.0.0"
 
+micromark-extension-math@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/micromark-extension-math/-/micromark-extension-math-2.0.2.tgz#bb7d28b907b17f1813dd3d0df2a6df6bb1a4d0e1"
+  integrity sha512-cFv2B/E4pFPBBFuGgLHkkNiFAIQv08iDgPH2HCuR2z3AUgMLecES5Cq7AVtwOtZeRrbA80QgMUk8VVW0Z+D2FA==
+  dependencies:
+    "@types/katex" "^0.11.0"
+    katex "^0.13.0"
+    micromark-factory-space "^1.0.0"
+    micromark-util-character "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+    micromark-util-types "^1.0.0"
+    uvu "^0.5.0"
+
 micromark-factory-destination@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e"
@@ -15477,6 +15577,11 @@ normalize-url@^3.3.0:
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
   integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
 
+not@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/not/-/not-0.1.0.tgz#c9691c1746c55dcfbe54cbd8bd4ff041bc2b519d"
+  integrity sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==
+
 npm-bundled@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
@@ -15613,6 +15718,13 @@ npmlog@^5.0.1:
     gauge "^3.0.0"
     set-blocking "^2.0.0"
 
+nth-check@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+  dependencies:
+    boolbase "^1.0.0"
+
 num2fraction@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@@ -18023,6 +18135,20 @@ regx@^1.0.4:
   resolved "https://registry.yarnpkg.com/regx/-/regx-1.0.4.tgz#a0ee32c308910902019ca1117ed41b9ddd041b2f"
   integrity sha1-oO4ywwiRCQIBnKERftQbnd0EGy8=
 
+rehype-katex@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-6.0.2.tgz#20197bbc10bdf79f6b999bffa6689d7f17226c35"
+  integrity sha512-C4gDAlS1+l0hJqctyiU64f9CvT00S03qV1T6HiMzbSuLBgWUtcqydWHY9OpKrm0SpkK16FNd62CDKyWLwV2ppg==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    "@types/katex" "^0.11.0"
+    hast-util-to-text "^3.1.0"
+    katex "^0.15.0"
+    rehype-parse "^8.0.0"
+    unified "^10.0.0"
+    unist-util-remove-position "^4.0.0"
+    unist-util-visit "^4.0.0"
+
 rehype-parse@^6.0.1:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-6.0.2.tgz#aeb3fdd68085f9f796f1d3137ae2b85a98406964"
@@ -18032,6 +18158,16 @@ rehype-parse@^6.0.1:
     parse5 "^5.0.0"
     xtend "^4.0.0"
 
+rehype-parse@^8.0.0:
+  version "8.0.4"
+  resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-8.0.4.tgz#3d17c9ff16ddfef6bbcc8e6a25a99467b482d688"
+  integrity sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    hast-util-from-parse5 "^7.0.0"
+    parse5 "^6.0.0"
+    unified "^10.0.0"
+
 rehype-raw@^6.1.1:
   version "6.1.1"
   resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4"
@@ -18130,6 +18266,16 @@ remark-gfm@^3.0.1:
     micromark-extension-gfm "^2.0.0"
     unified "^10.0.0"
 
+remark-math@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/remark-math/-/remark-math-5.1.1.tgz#459e798d978d4ca032e745af0bac81ddcdf94964"
+  integrity sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==
+  dependencies:
+    "@types/mdast" "^3.0.0"
+    mdast-util-math "^2.0.0"
+    micromark-extension-math "^2.0.0"
+    unified "^10.0.0"
+
 remark-parse@^10.0.0:
   version "10.0.1"
   resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
@@ -21344,6 +21490,14 @@ unist-util-filter@^2.0.3:
   dependencies:
     unist-util-is "^4.0.0"
 
+unist-util-find-after@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-find-after/-/unist-util-find-after-4.0.0.tgz#1101cebf5fed88ae3c6f3fa676e86fd5772a4f32"
+  integrity sha512-gfpsxKQde7atVF30n5Gff2fQhAc4/HTOV4CvkXpTg9wRfQhZWdXitpyXHWB6YcYgnsxLx+4gGHeVjCTAAp9sjw==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-is "^5.0.0"
+
 unist-util-generated@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
@@ -21378,6 +21532,14 @@ unist-util-remove-position@^1.0.0:
   dependencies:
     unist-util-visit "^1.1.0"
 
+unist-util-remove-position@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.1.tgz#d5b46a7304ac114c8d91990ece085ca7c2c135c8"
+  integrity sha512-0yDkppiIhDlPrfHELgB+NLQD5mfjup3a8UYclHruTJWmY74je8g+CIFr79x5f6AkmzSwlvKLbs63hC0meOMowQ==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-visit "^4.0.0"
+
 unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6"