From 512229ec7415a19143cfba68c232a26fb89d4fd0 Mon Sep 17 00:00:00 2001
From: rlazarini <remi.lazarini@cea.fr>
Date: Thu, 12 Dec 2024 14:41:36 +0100
Subject: [PATCH] [Ivette] added Help component with test in sandbox and
 adapting the markdown component

---
 ivette/electron.vite.config.ts             |   1 +
 ivette/package.json                        |   1 +
 ivette/src/dome/renderer/dialogs.tsx       |   2 +-
 ivette/src/dome/renderer/help.tsx          | 122 ++++++++++++++++++++
 ivette/src/dome/renderer/style.css         |  81 +++++++++-----
 ivette/src/dome/renderer/text/markdown.tsx |  55 ++++++++-
 ivette/src/dome/renderer/text/style.css    |  15 +++
 ivette/src/sandbox/help.tsx                |  61 ++++++++++
 ivette/src/sandbox/panel.tsx               |  11 +-
 ivette/src/sandbox/sandbox.md              | 124 +++++++++++++++++++++
 10 files changed, 436 insertions(+), 37 deletions(-)
 create mode 100644 ivette/src/dome/renderer/help.tsx
 create mode 100644 ivette/src/sandbox/help.tsx
 create mode 100644 ivette/src/sandbox/sandbox.md

diff --git a/ivette/electron.vite.config.ts b/ivette/electron.vite.config.ts
index 58835c0b2ba..6637f2640d2 100644
--- a/ivette/electron.vite.config.ts
+++ b/ivette/electron.vite.config.ts
@@ -62,6 +62,7 @@ export default defineConfig({
         "dome/controls": path.resolve(DOME, "renderer", "controls"),
         "dome/data": path.resolve(DOME, "renderer", "data"),
         "dome/dialogs": path.resolve(DOME, "renderer", "dialogs"),
+        "dome/help": path.resolve(DOME, "renderer", "help"),
         "dome/dnd": path.resolve(DOME, "renderer", "dnd"),
         "dome/errors": path.resolve(DOME, "renderer", "errors"),
         "dome/frame": path.resolve(DOME, "renderer", "frame"),
diff --git a/ivette/package.json b/ivette/package.json
index 6978f96fcb8..3d54725bdb6 100644
--- a/ivette/package.json
+++ b/ivette/package.json
@@ -44,6 +44,7 @@
     "diff": "^5",
     "lodash": "^4",
     "react": "^18",
+    "react-code-blocks": "0.1.6",
     "react-cytoscapejs": "",
     "react-dom": "^18",
     "react-draggable": "^4.4.6",
diff --git a/ivette/src/dome/renderer/dialogs.tsx b/ivette/src/dome/renderer/dialogs.tsx
index d5b7dfb0b40..67bf1faf6f3 100644
--- a/ivette/src/dome/renderer/dialogs.tsx
+++ b/ivette/src/dome/renderer/dialogs.tsx
@@ -315,7 +315,7 @@ export async function showOpenDir(
 export function showModal(val: React.ReactNode): void { modal.setValue(val); }
 export function closeModal(): void { showModal(undefined); }
 
-interface ModalProps {
+export interface ModalProps {
   /** Text of the label. Prepend to other children elements. */
   label: string;
   /** Icon identifier. Displayed on the left side of the label. */
diff --git a/ivette/src/dome/renderer/help.tsx b/ivette/src/dome/renderer/help.tsx
new file mode 100644
index 00000000000..8af05746dd5
--- /dev/null
+++ b/ivette/src/dome/renderer/help.tsx
@@ -0,0 +1,122 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2024                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/**
+  @packageDocumentation
+  @module dome/help
+ */
+
+import React from 'react';
+import { classes } from 'dome/misc/utils';
+import { IconButton, IconButtonKind } from './controls/buttons';
+import { Modal, showModal, ModalProps } from './dialogs';
+import { iconTag, Markdown, Pattern } from './text/markdown';
+
+/* --------------------------------------------------------------------------*/
+/* --- Panel List                                                            */
+/* --------------------------------------------------------------------------*/
+interface HelpMarkdownProps {
+  /** classes for Doc component */
+  className?: string;
+  /** Tab of patterns */
+  patterns?: Pattern[];
+  /**
+   * scroll to title h1 or h2 when component is render.
+   * The value must be the id of the balise html.
+   * Id is calculate by title.toLowerCase().replaceAll(' ','-')
+   * where title is the content of h1 or h2 if it is a string
+  */
+  initialScrollTo?: string;
+  /** Markdown content. */
+  children?: string;
+}
+
+export function HelpMarkdown(props: HelpMarkdownProps): JSX.Element {
+  const { patterns = [iconTag], className, initialScrollTo, children } = props;
+  const classNames = classes('dome-xHelp', className);
+
+  const scrollableDivRef = React.useRef<HTMLDivElement>(null);
+  const anchorsRef = React.useRef<{
+    [key: string] : HTMLHeadingElement | null
+  }>({});
+
+  const scrollToAnchor = (id: string): void => {
+    const scrollableDiv = scrollableDivRef.current;
+    const anchor = anchorsRef.current[id];
+    const top = scrollableDiv?.offsetTop || 0;
+
+    if (scrollableDiv && anchor) {
+      const anchorPosition = anchor.offsetTop - top;
+      scrollableDiv.scrollTo({
+        top: anchorPosition,
+        behavior: 'smooth',
+      });
+    }
+  };
+
+  React.useEffect(() => {
+    if(initialScrollTo) scrollToAnchor(initialScrollTo);
+  }, [initialScrollTo]);
+
+  return (
+    <div ref={scrollableDivRef} className={classNames}>
+      <Markdown
+        patterns={patterns || [iconTag]}
+        anchorsRef={anchorsRef}
+      >{ children }</Markdown>
+    </div>
+  );
+}
+
+interface IconModalMdProps extends HelpMarkdownProps {
+  /** Icon props */
+  kind?: IconButtonKind;
+  title?: string;
+  size?: number;
+  /** Properties of Modal component */
+  modal: Omit<ModalProps, 'children'>;
+}
+
+export function IconHelpModalMd(props: IconModalMdProps): JSX.Element {
+  const { title, kind, size,
+    patterns, initialScrollTo,
+    modal, children
+  } = props;
+
+  return (
+    <IconButton
+      icon='HELP'
+      className='dome-xDoc-icon'
+      title={title}
+      kind={kind}
+      size={size}
+      onClick={() => showModal(
+        <Modal {...modal} >
+          <HelpMarkdown
+            patterns={patterns}
+            initialScrollTo={initialScrollTo}
+          >{ children }</HelpMarkdown>
+        </Modal>)
+      }
+    />
+  );
+}
diff --git a/ivette/src/dome/renderer/style.css b/ivette/src/dome/renderer/style.css
index f0d5e43fdb9..991a903e8b3 100644
--- a/ivette/src/dome/renderer/style.css
+++ b/ivette/src/dome/renderer/style.css
@@ -225,45 +225,55 @@ input[type="checkbox"]:checked {
   border-radius: 10px;
   background-color: var(--background);
   max-width: calc(100% - 100px);
-  max-height: calc(100% - 50px);
-  overflow: hidden;
+  max-height: calc(100vh - 55px);
+  overflow-y: hidden;
 
-  .dome-xModal-header {
+  .dome-xModal-content {
     display: flex;
-    justify-content: space-between;
-    background-color: var(--background-profound);
-    padding: 5px;
-    font-size: medium;
-    align-items: center;
-
-    .dome-xModal-title {
-      margin-right: 20px;
-
-      .dome-xIcon {
-        padding-right: .3em;
-
-        svg {
-          height: 16px;
+    flex-direction: column;
+    max-height: 100%;
+    overflow-y: hidden;
+
+    .dome-xModal-header {
+      display: flex;
+      justify-content: space-between;
+      background-color: var(--background-profound);
+      padding: 5px;
+      font-size: medium;
+      align-items: center;
+
+      .dome-xModal-title {
+        margin-right: 20px;
+
+        .dome-xIcon {
+          padding-right: .3em;
+
+          svg {
+            height: 16px;
+          }
         }
       }
-    }
 
-    &>.dome-xIcon:hover {
-      cursor: pointer;
+      &>.dome-xIcon:hover {
+        cursor: pointer;
+      }
     }
-  }
 
-  .dome-xModal-body {
-      background-color: var(--background);
+    .dome-xModal-body {
+        flex:1;
+        max-height: 100%;
+        overflow-y: hidden;
+        background-color: var(--background);
 
-      .dome-xModal-waiting {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        padding: 20px;
+        .dome-xModal-waiting {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          padding: 20px;
 
-        svg { bottom: 0 !important; }
-      }
+          svg { bottom: 0 !important; }
+        }
+    }
   }
 }
 
@@ -276,4 +286,15 @@ input[type="checkbox"]:checked {
   bottom: 0;
   background-color: rgba(50, 50, 50, .4);
 }
+
+/* -------------------------------------------------------------------------- */
+/* --- Doc                                                              --- */
+/* -------------------------------------------------------------------------- */
+
+.dome-xHelp {
+  max-height: 100%;
+  overflow-y: auto;
+  padding: 10px;
+}
+
 /* -------------------------------------------------------------------------- */
diff --git a/ivette/src/dome/renderer/text/markdown.tsx b/ivette/src/dome/renderer/text/markdown.tsx
index ca97fcc4159..3151d7cc919 100644
--- a/ivette/src/dome/renderer/text/markdown.tsx
+++ b/ivette/src/dome/renderer/text/markdown.tsx
@@ -23,18 +23,24 @@
 import React from 'react';
 import ReactMarkdown, { Options } from 'react-markdown';
 
+import * as Themes from 'dome/themes';
 import { classes } from 'dome/misc/utils';
 import { Icon } from 'dome/controls/icons';
-
+import {
+  CodeBlock, atomOneDark, atomOneLight
+} from "react-code-blocks";
 export interface Pattern {
   pattern: RegExp,
   replace: (key: number, match?: RegExpExecArray) => JSX.Element | null
 }
 
 export const iconTag: Pattern = {
-  pattern: /\[icon-([^\]]+)\]/g,
+  pattern: /(\[ex:\])?\[icon-([^\]]+)\]/g,
   replace: (key: number, match?: RegExpExecArray) => {
-    return match ? <Icon key={key} id={match[1]}/> : null;
+    if(match && match[1] === "[ex:]") {
+      return <span key={key}>{`[icon-${match[2]}]`}</span>;
+    }
+    return match ? <Icon key={key} id={match[2]}/> : null;
   }
 };
 
@@ -99,6 +105,9 @@ interface MarkdownProps {
   className?: string;
   /** Tab of patterns */
   patterns?: Pattern[];
+  /** Anchors ref */
+  anchorsRef: React.MutableRefObject<
+    {[key: string] : HTMLHeadingElement | null}>;
   /** Children */
   children?: string | null;
 }
@@ -106,15 +115,51 @@ interface MarkdownProps {
 export function Markdown(
   props: MarkdownProps
 ): JSX.Element {
-  const { className, patterns, children } = props;
+  const { className, patterns, anchorsRef, children } = props;
+  const theme = Themes.useColorTheme()[0];
   const markdownClasses = classes(
     "dome-xMarkdown", "dome-pages", className
   );
 
+  /**
+   * If children is a string this function return the
+   * heading element with an id and save ref in anchorsRef
+   */
+  const getHtmlTitle = (
+    children: React.ReactNode,
+    tag: "h1" | "h2" = 'h1'
+  ): JSX.Element => {
+    const Tag = tag;
+    const id = typeof children === "string" ?
+      children.toLowerCase().replaceAll(' ', '-') : undefined;
+
+    return id ?
+      <Tag id={id} ref={(el) => anchorsRef.current[id] = el}>{children}</Tag>:
+      <Tag>{children}</Tag>;
+  };
+
   const options: Options = { className: markdownClasses };
   if(patterns && patterns.length > 0) options.components = {
     p: ({ children }) => <div>{replaceTags(children, patterns)}</div>,
-    li: ({ children }) => <li>{replaceTags(children, patterns)}</li>
+    li: ({ children }) => <li>{replaceTags(children, patterns)}</li>,
+    h1: ({ children }) => getHtmlTitle(children),
+    h2: ({ children }) => getHtmlTitle(children, 'h2'),
+    /** Uses codeBlock if ```` is used in markdown with a language,
+     *  otherwise the code-inline class is added */
+    code: ({ className, children }) => {
+      if (className && className.includes("language-")
+        && typeof children === "string"
+      ) {
+        const language = className.split("language-")[1];
+        return <CodeBlock
+          text={children}
+          language={language}
+          showLineNumbers={false}
+          theme={theme === 'dark' ? atomOneDark : atomOneLight}
+        />;
+      }
+      return <code className='code-inline'>{children}</code>;
+    },
   };
 
   return <ReactMarkdown {...options}>{ children }</ReactMarkdown>;
diff --git a/ivette/src/dome/renderer/text/style.css b/ivette/src/dome/renderer/text/style.css
index b3a5e8e8d93..28aca401af6 100644
--- a/ivette/src/dome/renderer/text/style.css
+++ b/ivette/src/dome/renderer/text/style.css
@@ -75,6 +75,21 @@
 
 }
 
+.dome-pages a
+{
+  color: var(--text-highlighted);
+  font-weight: 600;
+  text-decoration: none;
+}
+
+.dome-pages code {
+  &.code-inline {
+    background-color: var(--background-interaction);
+    padding: 1px 5px;
+    border-radius: 5px;
+  }
+}
+
 /* -------------------------------------------------------------------------- */
 /* --- Styling CodeMirror 6 editor                                        --- */
 /* -------------------------------------------------------------------------- */
diff --git a/ivette/src/sandbox/help.tsx b/ivette/src/sandbox/help.tsx
new file mode 100644
index 00000000000..7109452f478
--- /dev/null
+++ b/ivette/src/sandbox/help.tsx
@@ -0,0 +1,61 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2024                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/* -------------------------------------------------------------------------- */
+/* --- Sandbox Testing for Force Graph component                          --- */
+/* -------------------------------------------------------------------------- */
+
+import React from 'react';
+import { registerSandbox, TitleBar } from 'ivette';
+import { HelpMarkdown, IconHelpModalMd } from 'dome/help';
+import docSandbox from './sandbox.md?raw';
+
+// --------------------------------------------------------------------------
+// --- Main force graph component
+// --------------------------------------------------------------------------
+
+function SandboxHelp(): JSX.Element {
+  return (
+    <>
+      <TitleBar>
+        <IconHelpModalMd
+          modal={{ label: 'docsandbox - Help' }}
+          initialScrollTo={'help'}
+        >{ docSandbox }</IconHelpModalMd>
+      </TitleBar>
+      <HelpMarkdown initialScrollTo={'help'}>{ docSandbox }</HelpMarkdown>
+    </>
+  );
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Sandbox                                                            --- */
+/* -------------------------------------------------------------------------- */
+
+registerSandbox({
+  id: 'sandbox.help',
+  label: 'Help',
+  preferredPosition: 'ABCD',
+  children: <SandboxHelp />,
+});
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/sandbox/panel.tsx b/ivette/src/sandbox/panel.tsx
index bbae94cf524..80dea7996be 100644
--- a/ivette/src/sandbox/panel.tsx
+++ b/ivette/src/sandbox/panel.tsx
@@ -37,6 +37,8 @@ import { Icon } from 'dome/controls/icons';
 import './style.css';
 import { Label } from 'dome/controls/labels';
 import { Modal, showModal } from 'dome/dialogs';
+import { IconHelpModalMd } from 'dome/help';
+import docSandbox from './sandbox.md?raw';
 
 /* -------------------------------------------------------------------------- */
 /* --- Use Panel                                                          --- */
@@ -75,12 +77,19 @@ function UsePanel(): JSX.Element {
             })
           }
         />
-
         <IconButton
           icon="SIDEBAR"
           title={"show or hide the panel"}
           onClick={flipVisible}
         />
+        <IconHelpModalMd
+          modal={{
+            label: 'docsandbox - Panel'
+          }}
+          initialScrollTo={'panel'}
+        >
+          { docSandbox }
+        </IconHelpModalMd>
       </TitleBar>
       <div style={{ position: 'relative', height: '100%' }}>
         <Panel visible={visible} position={position}>
diff --git a/ivette/src/sandbox/sandbox.md b/ivette/src/sandbox/sandbox.md
new file mode 100644
index 00000000000..64e853f4bf9
--- /dev/null
+++ b/ivette/src/sandbox/sandbox.md
@@ -0,0 +1,124 @@
+# Sandbox
+
+The sandbox part of Ivette is only available in development mode.
+It allows you to test new modules and discover a simplified form of the basic modules before using them.
+
+## Dot Diagram
+
+Documentation is not yet available for this module.
+
+## ForceGraph
+
+Documentation is not yet available for this module.
+
+## Icons
+
+Documentation is not yet available for this module.
+
+## Panel
+
+The Panel component allows the addition of a retractable panel to a positioned block.
+
+The panel can be displayed on any side of the block using the position prop, which defaults to the right. The visible prop allows hiding or showing the panel.
+
+### Props
+ ``` javascript
+ export type PanelPosition = 'top' | 'bottom' | 'left' | 'right';
+
+ interface PanelProps {
+  /** Additional class. */
+  className?: string;
+  /** Position to displayed the panel. Default 'tr' */
+  position?: PanelPosition;
+  /** Defaults to `true`. */
+  visible?: boolean;
+  /** Defaults to `true`. */
+  display?: boolean;
+  /** Panel children. */
+  children: JSX.Element[];
+}
+```
+
+
+## Qsplit
+
+Documentation is not yet available for this module.
+
+## Text
+
+Documentation is not yet available for this module.
+
+## UseDnd
+
+Documentation is not yet available for this module.
+
+## Help
+
+the documentation is written in [Markdown](#markdown). It must be in a `*.md` file, the raw content of which will be retrieved via an import.
+
+For example, for the documentation of a sandbox module
+``` javascript
+import docSandbox from './sandbox.md?raw';
+```
+Here, `?raw` is used to indicate that we want the raw content of the file.
+
+Typically, the documentation will be displayed in the application's modal.
+
+### help.tsx
+
+This file contains components that make it easier to display documentation in your components.
+
+#### HelpMarkdown
+
+This component is used to display the markdown help, it is used by `IconodalMd` and you can see an example of it out of modal in the `help` sandbox.
+It takes the following props:
+``` javascript
+interface DocMarkdownProps {
+  /** classes for Doc component */
+  className?: string;
+  /** Tab of patterns */
+  patterns?: Pattern[];
+  /**
+   * scroll to title h1 or h2 when component is render.
+   * The value must be the id of the balise html.
+   * Id is calculate by title.toLowerCase().replaceAll(' ','-')
+   * where title is the content of h1 or h2 if it is a string
+  */
+  initialScrollTo?: string;
+  /** Markdown content. */
+  children?: string;
+}
+```
+
+#### IconModalMd
+
+Allows you to add a `HELP` icon ([icon-HELP]) which will open a modal window with the chosen document when clicked.
+
+``` javascript
+interface IconModalMdProps extends DocMarkdownProps {
+  /** Icon props */
+  kind?: IconButtonKind;
+  title?: string;
+  size?: number;
+  /** Properties of Modal component */
+  modal: Omit<ModalProps, 'children'>;
+}
+```
+
+
+## Markdown
+
+TO BE COMPLETED
+
+### Pattern
+You can used patterns to replace parts of the text by JSX Element.
+
+#### Icons
+
+There is one basic pattern to replace tags by an `Icon`, it name `iconTag`  in markdown component .
+
+* [icon-TUNINGS] : [ex:][icon-TUNINGS]
+* [icon-TARGET] : [ex:][icon-TARGET]
+* [icon-PIN] : [ex:][icon-PIN]
+
+or inline [icon-TUNINGS], [icon-TARGET], [icon-PIN]
-- 
GitLab