From 335ffdca1279a101d0783a8abd704ea83110317d Mon Sep 17 00:00:00 2001
From: chris <kokosias@yahoo.gr>
Date: Sun, 9 Aug 2020 01:57:46 +0300
Subject: [PATCH] allow table operations

---
 .../src/components/Button.js                  |  1 +
 .../src/components/HeadingsDropDown.js        | 21 ++---
 .../src/components/ImageUpload.js             |  5 +-
 .../src/components/TableDropDown.js           | 40 +++++-----
 .../src/components/comments/Comment.js        |  3 +-
 .../src/components/comments/CommentBox.js     |  3 +-
 .../comments/CommentBubbleComponent.js        | 10 +--
 .../src/components/link/LinkComponent.js      | 77 ++++++++++---------
 .../src/components/notes/NoteNumber.js        |  5 +-
 .../src/components/rightArea/BoxList.js       |  1 +
 .../src/components/rightArea/RightArea.js     |  9 +--
 .../components/trackChanges/TrackChangeBox.js | 38 +++++----
 .../trackChanges/TrackChangeEnable.js         |  1 +
 .../EditTableService/TableDropDownOptions.js  | 24 +++---
 .../TablesService/InsertTableService/Table.js | 10 +--
 .../track-changes/trackedTransaction.js       | 10 +++
 wax-prosemirror-themes/package.json           |  2 +-
 wax-prosemirror-utilities/package.json        |  2 +-
 18 files changed, 142 insertions(+), 120 deletions(-)

diff --git a/wax-prosemirror-components/src/components/Button.js b/wax-prosemirror-components/src/components/Button.js
index cb1049cca..85cf9df09 100644
--- a/wax-prosemirror-components/src/components/Button.js
+++ b/wax-prosemirror-components/src/components/Button.js
@@ -1,3 +1,4 @@
+/* eslint react/prop-types: 0 */
 import React, { useContext } from 'react';
 import styled from 'styled-components';
 import { ButtonStyles } from 'wax-prosemirror-themes';
diff --git a/wax-prosemirror-components/src/components/HeadingsDropDown.js b/wax-prosemirror-components/src/components/HeadingsDropDown.js
index d4fdcaea7..18d1a5712 100644
--- a/wax-prosemirror-components/src/components/HeadingsDropDown.js
+++ b/wax-prosemirror-components/src/components/HeadingsDropDown.js
@@ -1,20 +1,21 @@
-import React from "react";
-import styled from "styled-components";
-import { Commands } from "wax-prosemirror-utilities";
-import Dropdown from "react-dropdown";
-import "react-dropdown/style.css";
+/* eslint react/prop-types: 0 */
+import React from 'react';
+import styled from 'styled-components';
+import { Commands } from 'wax-prosemirror-utilities';
+import Dropdown from 'react-dropdown';
+import 'react-dropdown/style.css';
 
 const DropdownStyled = styled(Dropdown)`
-  display: ${props => (props.select ? "inline-flex" : "none")};
+  display: ${props => (props.select ? 'inline-flex' : 'none')};
   .Dropdown-control {
     border: none;
   }
 `;
 
 const dropDownOptions = [
-  { label: "Heading 1", value: "1" },
-  { label: "Heading  2", value: "2" },
-  { label: "Heading 3", value: "3" }
+  { label: 'Heading 1', value: '1' },
+  { label: 'Heading  2', value: '2' },
+  { label: 'Heading 3', value: '3' },
 ];
 
 const HeadingsDropDown = ({ dispatch, state, item }) => (
@@ -22,7 +23,7 @@ const HeadingsDropDown = ({ dispatch, state, item }) => (
     options={dropDownOptions}
     onChange={option => {
       Commands.setBlockType(state.config.schema.nodes.heading, {
-        level: option.value
+        level: option.value,
       })(state, dispatch);
     }}
     placeholder="Choose heading"
diff --git a/wax-prosemirror-components/src/components/ImageUpload.js b/wax-prosemirror-components/src/components/ImageUpload.js
index a82798f32..5858b097b 100644
--- a/wax-prosemirror-components/src/components/ImageUpload.js
+++ b/wax-prosemirror-components/src/components/ImageUpload.js
@@ -1,5 +1,6 @@
-import React from "react";
-import styled from "styled-components";
+/* eslint react/prop-types: 0 */
+import React from 'react';
+import styled from 'styled-components';
 
 const UploadImage = styled.div`
   color: #777;
diff --git a/wax-prosemirror-components/src/components/TableDropDown.js b/wax-prosemirror-components/src/components/TableDropDown.js
index 950c1a53e..72c1f5d69 100644
--- a/wax-prosemirror-components/src/components/TableDropDown.js
+++ b/wax-prosemirror-components/src/components/TableDropDown.js
@@ -1,14 +1,15 @@
-import React from "react";
-import styled from "styled-components";
-import * as tablesFn from "prosemirror-tables";
-import Dropdown from "react-dropdown";
-import "react-dropdown/style.css";
+/* eslint react/prop-types: 0 */
+import React from 'react';
+import styled from 'styled-components';
+import * as tablesFn from 'prosemirror-tables';
+import Dropdown from 'react-dropdown';
+import 'react-dropdown/style.css';
 
 const DropdownStyled = styled(Dropdown)`
   display: inline-flex;
   cursor: not-allowed;
   opacity: ${props => (props.select ? 1 : 0.4)};
-  pointer-events: ${props => (props.select ? "default" : "none")};
+  pointer-events: ${props => (props.select ? 'default' : 'none')};
   .Dropdown-control {
     border: none;
   }
@@ -28,28 +29,29 @@ const DropdownStyled = styled(Dropdown)`
 `;
 
 const dropDownOptions = [
-  { label: "add column before", value: "addColumnBefore" },
-  { label: "add column after", value: "addColumnAfter" },
-  { label: "Delete column", value: "deleteColumn" },
-  { label: "Insert row before", value: "addRowBefore" },
-  { label: "Insert row after", value: "addRowAfter" },
-  { label: "Delete row", value: "deleteRow" },
-  { label: "Delete table", value: "deleteTable" },
-  { label: "Merge cells", value: "mergeCells" },
-  { label: "Split cell", value: "splitCell" },
-  { label: "Toggle header column", value: "toggleHeaderColumn" },
-  { label: "Toggle header row", value: "toggleHeaderRow" },
-  { label: "Toggle header cells", value: "toggleHeaderCell" }
+  { label: 'add column before', value: 'addColumnBefore' },
+  { label: 'add column after', value: 'addColumnAfter' },
+  { label: 'Delete column', value: 'deleteColumn' },
+  { label: 'Insert row before', value: 'addRowBefore' },
+  { label: 'Insert row after', value: 'addRowAfter' },
+  { label: 'Delete row', value: 'deleteRow' },
+  { label: 'Delete table', value: 'deleteTable' },
+  { label: 'Merge cells', value: 'mergeCells' },
+  { label: 'Split cell', value: 'splitCell' },
+  { label: 'Toggle header column', value: 'toggleHeaderColumn' },
+  { label: 'Toggle header row', value: 'toggleHeaderRow' },
+  { label: 'Toggle header cells', value: 'toggleHeaderCell' },
 ];
 
 const TableDropDown = ({ view: { dispatch, state }, item }) => (
   <DropdownStyled
     options={dropDownOptions}
     onChange={option => {
-      tablesFn[option.value](state, dispatch);
+      item.run(state, dispatch, tablesFn[option.value]);
     }}
     placeholder="Table Options"
     select={item.select && item.select(state)}
   />
 );
+
 export default TableDropDown;
diff --git a/wax-prosemirror-components/src/components/comments/Comment.js b/wax-prosemirror-components/src/components/comments/Comment.js
index 6845cc58a..c215af8c8 100644
--- a/wax-prosemirror-components/src/components/comments/Comment.js
+++ b/wax-prosemirror-components/src/components/comments/Comment.js
@@ -1,3 +1,4 @@
+/* eslint react/prop-types: 0 */
 import React, { useState, useRef, useEffect } from 'react';
 import { v4 as uuidv4 } from 'uuid';
 import { last } from 'lodash';
@@ -43,7 +44,7 @@ export default ({ comment, activeView, user, active }) => {
     const {
       current: { value },
     } = commentInput;
-    const { tr, doc } = state;
+    const { tr } = state;
 
     const obj = {
       content: value,
diff --git a/wax-prosemirror-components/src/components/comments/CommentBox.js b/wax-prosemirror-components/src/components/comments/CommentBox.js
index d77ee8032..05b730cb2 100644
--- a/wax-prosemirror-components/src/components/comments/CommentBox.js
+++ b/wax-prosemirror-components/src/components/comments/CommentBox.js
@@ -1,3 +1,4 @@
+/* eslint react/prop-types: 0 */
 import React, { useState, useEffect, useContext, memo } from 'react';
 import { TextSelection } from 'prosemirror-state';
 import { last, maxBy } from 'lodash';
@@ -27,12 +28,12 @@ const CommentBoxStyled = styled.div`
         return 0.6;
       case 'entered':
         return 1;
+      default:
     }
   }};
 `;
 
 export default ({ comment, top, dataBox }) => {
-  console.log('rerender');
   const [animate, setAnimate] = useState(false);
   const {
     attrs: { id },
diff --git a/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js b/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js
index 1c8b2fcd0..79b2a117e 100644
--- a/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js
+++ b/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js
@@ -1,5 +1,5 @@
-import React, { useLayoutEffect, useState, useContext } from 'react';
-import styled from 'styled-components';
+/* eslint react/prop-types: 0 */
+import React, { useLayoutEffect, useContext } from 'react';
 import { Commands, DocumentHelpers } from 'wax-prosemirror-utilities';
 import { WaxContext } from 'wax-prosemirror-core';
 
@@ -32,12 +32,8 @@ const CommentBubbleComponent = ({
   const isSelectionComment = () => {
     const commentMark = activeView.state.schema.marks.comment;
     const mark = DocumentHelpers.findMark(state, commentMark, true);
-    const {
-      selection: { $from, $to },
-      doc,
-    } = state;
 
-    //TODO Overlapping comments . for now don't allow
+    // TODO Overlapping comments . for now don't allow
     if (mark.length >= 1) return true;
     return false;
   };
diff --git a/wax-prosemirror-components/src/components/link/LinkComponent.js b/wax-prosemirror-components/src/components/link/LinkComponent.js
index 7fc43a202..8c4b32a8d 100644
--- a/wax-prosemirror-components/src/components/link/LinkComponent.js
+++ b/wax-prosemirror-components/src/components/link/LinkComponent.js
@@ -1,7 +1,8 @@
-import React, { useRef, useEffect, useState, useContext } from "react";
-import styled from "styled-components";
-import { WaxContext } from "wax-prosemirror-core";
-import { DocumentHelpers } from "wax-prosemirror-utilities";
+/* eslint react/prop-types: 0 */
+import React, { useRef, useEffect, useState, useContext } from 'react';
+import styled from 'styled-components';
+import { WaxContext } from 'wax-prosemirror-core';
+import { DocumentHelpers } from 'wax-prosemirror-utilities';
 
 const LinkWrapper = styled.div`
   padding: 20px;
@@ -19,25 +20,23 @@ const Button = styled.button`
 `;
 
 const LinkComponent = ({ mark, setPosition, position }) => {
-  const href = mark ? mark.attrs.href : null,
-    linkMark = mark ? mark : null,
-    { view: { main }, activeView } = useContext(WaxContext),
-    { state, dispatch } = activeView,
-    ref = useRef(null),
-    linkInput = useRef(null),
-    [addButtonText, setButtonText] = useState("Create"),
-    [lastLinkMark, setLLastLinkMark] = useState(linkMark),
-    [linkHref, setLinkHref] = useState(href);
+  const href = mark ? mark.attrs.href : null;
+  const linkMark = mark ? mark : null;
+  const {
+    view: { main },
+    activeView,
+  } = useContext(WaxContext);
+  const { state, dispatch } = activeView;
+  const ref = useRef(null);
+  const linkInput = useRef(null);
+  const [addButtonText, setButtonText] = useState('Create');
+  const [lastLinkMark, setLLastLinkMark] = useState(linkMark);
+  const [linkHref, setLinkHref] = useState(href);
 
-  useEffect(
-    () => {
-      const width = ref.current ? ref.current.offsetWidth : 0;
-      const left = Math.abs(position.left - width / 2);
-      setLinkText();
-      removeMarkIfEmptyHref();
-    },
-    [ref.current, href]
-  );
+  useEffect(() => {
+    setLinkText();
+    removeMarkIfEmptyHref();
+  }, [ref.current, href]);
 
   const addLinkHref = () => {
     const href = linkHref;
@@ -50,9 +49,9 @@ const LinkComponent = ({ mark, setPosition, position }) => {
         mark.to,
         linkMark.create({
           ...((mark && mark.attrs) || {}),
-          href
-        })
-      )
+          href,
+        }),
+      ),
     );
     activeView.focus();
   };
@@ -63,45 +62,49 @@ const LinkComponent = ({ mark, setPosition, position }) => {
   };
 
   const handleKeyDown = event => {
-    if (event.key === "Enter" || event.which === 13) {
+    if (event.key === 'Enter' || event.which === 13) {
       addLinkHref();
     }
   };
 
   const updateLinkHref = () => {
-    const { current: { value } } = linkInput;
+    const {
+      current: { value },
+    } = linkInput;
     setLinkHref(value);
   };
 
   const setLinkText = () => {
-    if (mark && mark.attrs.href !== "") {
-      setButtonText("Update");
+    if (mark && mark.attrs.href !== '') {
+      setButtonText('Update');
       setLinkHref(mark.attrs.href);
     } else {
-      setButtonText("Create");
-      setLinkHref("");
+      setButtonText('Create');
+      setLinkHref('');
       if (linkInput.current) linkInput.current.focus();
     }
   };
 
   const removeMarkIfEmptyHref = () => {
-    const { selection: { $from, $to } } = state;
-    const PMLinkMark = state.schema.marks["link"];
+    const {
+      selection: { $from, $to },
+    } = state;
+    const PMLinkMark = state.schema.marks['link'];
     const actualMark = DocumentHelpers.getSelectionMark(state, PMLinkMark);
     setLLastLinkMark(actualMark);
 
     if (
-      lastLinkMark.attrs.href === "" &&
+      lastLinkMark.attrs.href === '' &&
       ($from.pos < lastLinkMark.from || $to.pos > lastLinkMark.to)
     ) {
       dispatch(
         state.tr
-          .setMeta("addToHistory", false)
+          .setMeta('addToHistory', false)
           .removeMark(
             lastLinkMark.from,
             lastLinkMark.to,
-            state.schema.marks.link
-          )
+            state.schema.marks.link,
+          ),
       );
     }
   };
diff --git a/wax-prosemirror-components/src/components/notes/NoteNumber.js b/wax-prosemirror-components/src/components/notes/NoteNumber.js
index aa79ea8c7..3341978ed 100644
--- a/wax-prosemirror-components/src/components/notes/NoteNumber.js
+++ b/wax-prosemirror-components/src/components/notes/NoteNumber.js
@@ -1,5 +1,6 @@
-import React from "react";
-import styled from "styled-components";
+/* eslint react/prop-types: 0 */
+import React from 'react';
+import styled from 'styled-components';
 
 const NoteNumberStyled = styled.div`
   display: flex;
diff --git a/wax-prosemirror-components/src/components/rightArea/BoxList.js b/wax-prosemirror-components/src/components/rightArea/BoxList.js
index 10d1d454d..23e2ab47a 100644
--- a/wax-prosemirror-components/src/components/rightArea/BoxList.js
+++ b/wax-prosemirror-components/src/components/rightArea/BoxList.js
@@ -1,3 +1,4 @@
+/* eslint react/prop-types: 0 */
 import { Mark } from 'prosemirror-model';
 import React from 'react';
 import CommentBox from '../comments/CommentBox';
diff --git a/wax-prosemirror-components/src/components/rightArea/RightArea.js b/wax-prosemirror-components/src/components/rightArea/RightArea.js
index a11ecbbb7..b66fa208e 100644
--- a/wax-prosemirror-components/src/components/rightArea/RightArea.js
+++ b/wax-prosemirror-components/src/components/rightArea/RightArea.js
@@ -1,11 +1,6 @@
+/* eslint react/prop-types: 0 */
 import { Mark } from 'prosemirror-model';
-import React, {
-  useContext,
-  useState,
-  useEffect,
-  useMemo,
-  useCallback,
-} from 'react';
+import React, { useContext, useState, useMemo, useCallback } from 'react';
 import useDeepCompareEffect from 'use-deep-compare-effect';
 import { each, uniqBy, sortBy } from 'lodash';
 import { WaxContext } from 'wax-prosemirror-core';
diff --git a/wax-prosemirror-components/src/components/trackChanges/TrackChangeBox.js b/wax-prosemirror-components/src/components/trackChanges/TrackChangeBox.js
index d2ec7235f..b81aa5ec4 100644
--- a/wax-prosemirror-components/src/components/trackChanges/TrackChangeBox.js
+++ b/wax-prosemirror-components/src/components/trackChanges/TrackChangeBox.js
@@ -1,8 +1,9 @@
-import { Mark } from "prosemirror-model";
-import React, { Fragment, useState, useEffect, useContext } from "react";
-import { Transition } from "react-transition-group";
-import styled from "styled-components";
-import { WaxContext } from "wax-prosemirror-core";
+/* eslint react/prop-types: 0 */
+import { Mark } from 'prosemirror-model';
+import React, { useState, useEffect, useContext } from 'react';
+import { Transition } from 'react-transition-group';
+import styled from 'styled-components';
+import { WaxContext } from 'wax-prosemirror-core';
 
 const TrackChangeBoxStyled = styled.div`
   display: flex;
@@ -10,30 +11,37 @@ const TrackChangeBoxStyled = styled.div`
   margin-top: 10px;
   border: 1px solid blue;
   position: absolute;
-  transition: ${({ state }) => "top 1s, opacity 1.5s, left 1s"};
+  transition: ${({ state }) => 'top 1s, opacity 1.5s, left 1s'};
   top: ${props => (props.top ? `${props.top}px` : 0)};
   left: ${props => (props.active ? `${63}%` : `${65}%`)};
   opacity: ${({ state }) => {
     switch (state) {
-      case "exited":
+      case 'exited':
         return 0.2;
-      case "exiting":
+      case 'exiting':
         return 0.4;
-      case "entering":
+      case 'entering':
         return 0.6;
-      case "entered":
+      case 'entered':
         return 1;
+      default:
     }
   }};
 `;
 
 export default ({ trackChange, view, top, dataBox }) => {
   const [animate, setAnimate] = useState(false);
-  const { view: { main }, app, activeView } = useContext(WaxContext);
+  const {
+    view: { main },
+    app,
+    activeView,
+  } = useContext(WaxContext);
   let action;
   if (trackChange instanceof Mark) {
-    if ((trackChange.type.name = "format_change")) {
-      const { attrs: { username, before, after } } = trackChange;
+    if ((trackChange.type.name = 'format_change')) {
+      const {
+        attrs: { username, before, after },
+      } = trackChange;
       action = `User ${username} added ${after[0]}`;
     }
   } else {
@@ -46,7 +54,7 @@ export default ({ trackChange, view, top, dataBox }) => {
   }, []);
 
   return (
-    <Fragment>
+    <>
       <Transition in={animate} timeout={1000}>
         {state => (
           <TrackChangeBoxStyled
@@ -63,6 +71,6 @@ export default ({ trackChange, view, top, dataBox }) => {
           </TrackChangeBoxStyled>
         )}
       </Transition>
-    </Fragment>
+    </>
   );
 };
diff --git a/wax-prosemirror-components/src/components/trackChanges/TrackChangeEnable.js b/wax-prosemirror-components/src/components/trackChanges/TrackChangeEnable.js
index 4966c9841..742f9a9b0 100644
--- a/wax-prosemirror-components/src/components/trackChanges/TrackChangeEnable.js
+++ b/wax-prosemirror-components/src/components/trackChanges/TrackChangeEnable.js
@@ -1,3 +1,4 @@
+/* eslint react/prop-types: 0 */
 import React, { useContext, useState } from 'react';
 import styled from 'styled-components';
 import { ButtonStyles } from 'wax-prosemirror-themes';
diff --git a/wax-prosemirror-services/src/TablesService/EditTableService/TableDropDownOptions.js b/wax-prosemirror-services/src/TablesService/EditTableService/TableDropDownOptions.js
index c08f229bb..d48701582 100644
--- a/wax-prosemirror-services/src/TablesService/EditTableService/TableDropDownOptions.js
+++ b/wax-prosemirror-services/src/TablesService/EditTableService/TableDropDownOptions.js
@@ -1,20 +1,20 @@
-import React from "react";
-import { v4 as uuidv4 } from "uuid";
-import { injectable } from "inversify";
-import { isEmpty } from "lodash";
-import { TableDropDown } from "wax-prosemirror-components";
-import { addColumnBefore } from "prosemirror-tables";
-import { Commands } from "wax-prosemirror-utilities";
-import Tools from "../../lib/Tools";
+import React from 'react';
+import { v4 as uuidv4 } from 'uuid';
+import { injectable } from 'inversify';
+import { isEmpty } from 'lodash';
+import { TableDropDown } from 'wax-prosemirror-components';
+import { addColumnBefore } from 'prosemirror-tables';
+import { Commands } from 'wax-prosemirror-utilities';
+import Tools from '../../lib/Tools';
 
 @injectable()
 export default class TableDropDownOptions extends Tools {
-  title = "Select Options";
-  content = "table";
+  title = 'Select Options';
+  content = 'table';
 
   get run() {
-    return () => {
-      return true;
+    return (state, dispatch, tableFn) => {
+      tableFn(state, dispatch);
     };
   }
 
diff --git a/wax-prosemirror-services/src/TablesService/InsertTableService/Table.js b/wax-prosemirror-services/src/TablesService/InsertTableService/Table.js
index cae1293ff..d475193db 100644
--- a/wax-prosemirror-services/src/TablesService/InsertTableService/Table.js
+++ b/wax-prosemirror-services/src/TablesService/InsertTableService/Table.js
@@ -1,11 +1,11 @@
-import Tools from "../../lib/Tools";
-import { Commands } from "wax-prosemirror-utilities";
-import { injectable } from "inversify";
-import { icons } from "wax-prosemirror-components";
+import Tools from '../../lib/Tools';
+import { Commands } from 'wax-prosemirror-utilities';
+import { injectable } from 'inversify';
+import { icons } from 'wax-prosemirror-components';
 
 @injectable()
 export default class Table extends Tools {
-  title = "Insert table";
+  title = 'Insert table';
   content = icons.table;
 
   get run() {
diff --git a/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js b/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js
index 43fc3a0fb..e452aaad1 100644
--- a/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js
+++ b/wax-prosemirror-services/src/TrackChangeService/track-changes/trackedTransaction.js
@@ -14,6 +14,16 @@ import addMarkStep from './helpers/addMarkStep';
 import removeMarkStep from './helpers/removeMarkStep';
 
 const trackedTransaction = (tr, state, user) => {
+  if (!tr.selectionSet) {
+    const $pos = state.selection.$anchor;
+    for (let { depth } = $pos; depth > 0; depth--) {
+      const node = $pos.node(depth);
+      if (node.type.spec.tableRole === 'table') {
+        return tr;
+      }
+    }
+  }
+
   if (
     !tr.steps.length ||
     (tr.meta &&
diff --git a/wax-prosemirror-themes/package.json b/wax-prosemirror-themes/package.json
index cb35a2db4..d63550ca5 100644
--- a/wax-prosemirror-themes/package.json
+++ b/wax-prosemirror-themes/package.json
@@ -1,6 +1,6 @@
 {
   "name": "wax-prosemirror-themes",
-  "author": "Collaborative Knowledge Foundation",
+  "author": "Christos Kokosias",
   "version": "0.0.13",
   "description": "Wax prosemirror themes",
   "license": "MIT",
diff --git a/wax-prosemirror-utilities/package.json b/wax-prosemirror-utilities/package.json
index 5161ee3a4..28ea30ba3 100644
--- a/wax-prosemirror-utilities/package.json
+++ b/wax-prosemirror-utilities/package.json
@@ -1,6 +1,6 @@
 {
   "name": "wax-prosemirror-utilities",
-  "author": "Collaborative Knowledge Foundation",
+  "author": "Christos Kokosias",
   "version": "0.0.13",
   "description": "Wax prosemirror utilities",
   "license": "MIT",
-- 
GitLab