소스 검색

LaTeX mode to text control via mathjax (#2221)

* add latex mode to text control via mathjax

* allow square brackets for displayed equations

* increase timeout for latex to render

* move config object outside component

* minor fixes

* resolve conflict in contributors

* Update contributors.txt

* fix classes and formatting

* remove contributors (back to normal)

* trying to come around markdown package

* already loaded

* latex mode

* Fab's comment

Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>

---------

Co-authored-by: Ryan <ryanyeojl@gmail.com>
Co-authored-by: Jean-Robin <jeanrobin.medori@avaiga.com>
Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>
Fred Lefévère-Laoide 6 달 전
부모
커밋
737b924f05

+ 36 - 0
doc/gui/examples/controls/text_latex.py

@@ -0,0 +1,36 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+from taipy.gui import Gui
+
+latex = """
+# Generated by *Taipy*
+
+You can insert *LaTeX* in a `text` control to
+display math equations.
+
+Displayed Equations:
+$$Attention(Q, K, V) = softmax(\\frac{QK^T}{\\sqrt{d_k}})V$$
+
+Inline Equations:
+Each head $h_i$ is the attention function of $\\textbf{Query}$, $\\textbf{Key}$ and $\\textbf{Value}$ with trainable parameters ($W_i^Q$, $W_i^K$, $W_i^V$)
+"""  # noqa W291
+
+page = """
+<|{latex}|text|mode=latex|>
+"""
+
+if __name__ == "__main__":
+    Gui(page).run(title="Text - Latex mode")

+ 2 - 2
frontend/taipy-gui/jest.config.js

@@ -27,7 +27,7 @@ module.exports = {
     ],
     coverageReporters: ["json", "html", "text"],
     modulePathIgnorePatterns: ["<rootDir>/packaging/"],
-    moduleNameMapper: {"react-markdown": "<rootDir>/node_modules/react-markdown/react-markdown.min.js"},
-    transformIgnorePatterns: ["<rootDir>/node_modules/(?!react-jsx-parser|react-markdown/)"],
+    moduleNameMapper: {"react-markdown": "<rootDir>/test-config/markdown.tsx"},
+    transformIgnorePatterns: ["<rootDir>/node_modules/(?!react-jsx-parser/)"],
     ...createJsWithTsPreset()
 };

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 171 - 544
frontend/taipy-gui/package-lock.json


+ 1 - 0
frontend/taipy-gui/package.json

@@ -11,6 +11,7 @@
     "@mui/x-tree-view": "^7.0.0",
     "apache-arrow": "^17.0.0",
     "axios": "^1.2.0",
+    "better-react-mathjax": "^2.0.3",
     "date-fns": "^3.6.0",
     "date-fns-tz": "^3.1.3",
     "lodash": "^4.17.21",

+ 23 - 5
frontend/taipy-gui/src/components/Taipy/Field.spec.tsx

@@ -61,13 +61,31 @@ describe("Field Component", () => {
         expect(elt).toHaveStyle("width: 500px");
     });
     it("can render markdown", async () => {
-        render(<Field value="titi" className="taipy-text" mode="md" />);
-        const elt = document.querySelector(".taipy-text");
-        await waitFor(() => expect(elt?.querySelector("p")).not.toBeNull());
+        const { container, getByText, findByText } = render(<Field value="titi" className="taipy-text" mode="md" />);
+        getByText(/markdown/i);
+        // https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
+        // expect(await findByText(/titi/i)).toBeInTheDocument();
     });
     it("can render pre", async () => {
-        render(<Field value="titi" className="taipy-text" mode="pre" />);
-        const elt = document.querySelector("pre.taipy-text");
+        const { container } = render(<Field value="titi" className="taipy-text" mode="pre" />);
+        const elt = container.querySelector("pre.taipy-text-pre");
         expect(elt).toBeInTheDocument();
     });
+    describe("latex mode", () => {
+        it("renders LaTeX as block math", async () => {
+            const { container, getByText } = render(
+                <Field value={"$$x = y + 1$$"} className="taipy-text" mode="latex" />
+            );
+            getByText(/latex/i);
+            await waitFor(() => expect(container.querySelector(".taipy-text-latex")).toBeInTheDocument());
+        });
+        it("renders LaTeX as inline math", async () => {
+            const { container, getByText, findByText } = render(
+                <Field value={"This is inline $x = y + 1$ math."} className="taipy-text" mode="latex" />
+            );
+            // getByText(/latex/i); // already loaded ?
+            await waitFor(() => expect(container.querySelector(".taipy-text-latex")).toBeInTheDocument());
+            expect(await findByText(/inline/i)).toBeInTheDocument();
+        });
+    });
 });

+ 60 - 5
frontend/taipy-gui/src/components/Taipy/Field.tsx

@@ -11,11 +11,12 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { lazy, useMemo } from "react";
+import React, { lazy, useMemo, Suspense } from "react";
 import Typography from "@mui/material/Typography";
 import Tooltip from "@mui/material/Tooltip";
 
 import { formatWSValue } from "../../utils";
+import { getSuffixedClassNames } from "./utils";
 import { useClassNames, useDynamicProperty, useFormatConfig } from "../../utils/hooks";
 import { TaipyBaseProps, TaipyHoverProps, getCssSize } from "./utils";
 import { getComponentClassName } from "./TaipyStyle";
@@ -33,6 +34,23 @@ interface TaipyFieldProps extends TaipyBaseProps, TaipyHoverProps {
 const unsetWeightSx = { fontWeight: "unset" };
 
 const Markdown = lazy(() => import("react-markdown"));
+const MathJax = lazy(() => import("better-react-mathjax").then((module) => ({ default: module.MathJax })));
+const MathJaxContext = lazy(() =>
+    import("better-react-mathjax").then((module) => ({ default: module.MathJaxContext }))
+);
+
+const mathJaxConfig = {
+    tex: {
+        inlineMath: [
+            ["$", "$"],
+            ["\\(", "\\)"],
+        ],
+        displayMath: [
+            ["$$", "$$"],
+            ["\\[", "\\]"],
+        ],
+    },
+};
 
 const Field = (props: TaipyFieldProps) => {
     const { id, dataType, format, defaultValue, raw } = props;
@@ -68,18 +86,55 @@ const Field = (props: TaipyFieldProps) => {
         <Tooltip title={hover || ""}>
             <>
                 {mode == "pre" ? (
-                    <pre className={`${className} ${getComponentClassName(props.children)}`} id={id} style={style}>
+                    <pre
+                        className={`${className} ${getSuffixedClassNames(className, "-pre")} ${getComponentClassName(
+                            props.children
+                        )}`}
+                        id={id}
+                        style={style}
+                    >
                         {value}
                     </pre>
                 ) : mode == "markdown" || mode == "md" ? (
-                    <Markdown className={`${className} ${getComponentClassName(props.children)}`}>{value}</Markdown>
+                    <Suspense fallback={<div>Loading Markdown...</div>}>
+                        <Markdown
+                            className={`${className} ${getSuffixedClassNames(
+                                className,
+                                "-markdown"
+                            )} ${getComponentClassName(props.children)}`}
+                        >
+                            {value}
+                        </Markdown>
+                    </Suspense>
                 ) : raw || mode == "raw" ? (
-                    <span className={className} id={id} style={style}>
+                    <span
+                        className={`${className} ${getSuffixedClassNames(className, "-raw")} ${getComponentClassName(
+                            props.children
+                        )}`}
+                        id={id}
+                        style={style}
+                    >
                         {value}
                     </span>
+                ) : mode == "latex" ? (
+                    <Suspense fallback={<div>Loading LaTex...</div>}>
+                        <MathJaxContext config={mathJaxConfig}>
+                            <MathJax
+                                className={`${className} ${getSuffixedClassNames(
+                                    className,
+                                    "-latex"
+                                )} ${getComponentClassName(props.children)}`}
+                                id={id}
+                            >
+                                {value}
+                            </MathJax>
+                        </MathJaxContext>
+                    </Suspense>
                 ) : (
                     <Typography
-                        className={`${className} ${getComponentClassName(props.children)}`}
+                        className={`${className} ${
+                            mode ? getSuffixedClassNames(className, "-" + mode) : ""
+                        } ${getComponentClassName(props.children)}`}
                         id={id}
                         component="span"
                         sx={typoSx}

+ 11 - 0
frontend/taipy-gui/test-config/markdown.tsx

@@ -0,0 +1,11 @@
+import React, { ReactNode } from "react";
+
+interface ChildrenProps {
+    children: ReactNode;
+}
+
+function ReactMarkdownMock({ children }: ChildrenProps) {
+    return <p>{children}</p>;
+}
+
+export default ReactMarkdownMock;

+ 1 - 1
taipy/gui/viselements.json

@@ -23,7 +23,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the text is processed:\n<ul><li>&quot;raw&quot;: synonym for setting the <i>raw</i> property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown."
+                        "doc": "Define the way the text is processed:\n<ul><li>&quot;raw&quot;: synonym for setting the <i>raw</i> property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown</li><li>&quot;latex&quot;: LaTe&chi; support</li>"
                     },
                     {
                         "name": "format",

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.