CodeEditor.purs 11.7 KB
Newer Older
1 2
module Gargantext.Components.CodeEditor where

arturo's avatar
arturo committed
3 4 5
import Gargantext.Prelude

import DOM.Simple.Types (Element)
6
import Data.Argonaut.Parser (jsonParser)
arturo's avatar
arturo committed
7
import Data.Either (Either(..))
8
import Data.Eq.Generic (genericEq)
arturo's avatar
arturo committed
9 10
import Data.Foldable (intercalate)
import Data.Generic.Rep (class Generic)
11 12
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable, null, toMaybe)
arturo's avatar
arturo committed
13
import Data.Show.Generic (genericShow)
14
import Data.String.Utils (endsWith)
15
import Effect (Effect)
Alexandre Delanoë's avatar
Alexandre Delanoë committed
16
import FFI.Simple ((.=))
arturo's avatar
arturo committed
17 18 19
import Gargantext.Components.Bootstrap as B
import Gargantext.Utils.HighlightJS as HLJS
import Gargantext.Utils.Reactix as R2
20
import MarkdownIt (renderString)
21 22
import Reactix as R
import Reactix.DOM.HTML as H
23
import Toestand as T
24

25 26
here :: R2.Here
here = R2.here "Gargantext.Components.CodeEditor"
27

28 29 30
type Code = String
type Html = String
type Error = String
31
type ElRef = R.Ref (Nullable Element)
32

33 34
data CodeType = Haskell | JSON | Markdown | Python

35 36
derive instance Generic CodeType _
instance Eq CodeType where
37
  eq = genericEq
38
instance Show CodeType where
39 40 41
  show = genericShow

data ViewType = Code | Preview | Both
42 43
derive instance Generic ViewType _
instance Eq ViewType where
44
  eq = genericEq
45
instance Show ViewType where
46
  show = genericShow
47 48

type Props =
arturo's avatar
arturo committed
49
  ( code            :: Code
50
  , defaultCodeType :: CodeType
arturo's avatar
arturo committed
51
  , onChange        :: CodeType -> Code -> Effect Unit
52 53
  )

54 55 56
-- Fixes newlines in code
-- This is useful eg for proper rendering of the textarea overlay
codeNlFix :: CodeType -> Code -> Code
57 58
codeNlFix _ "" = " "
codeNlFix _ c = if endsWith "\n" c then (c <> " ") else c
59

60 61 62 63
render :: CodeType -> Code -> Effect (Either Error Html)
render Haskell code = pure $ Right $ renderHaskell $ codeNlFix Haskell code
render Python  code = pure $ Right $ renderPython  $ codeNlFix Python code
render JSON code = pure result
64 65 66 67
  where
    parsedE = jsonParser code
    result = case parsedE of
      Left err -> Left err
68
      Right parsed -> Right $ R2.stringify parsed 2
69 70 71
render Markdown code = do
  r <- renderMd $ codeNlFix Markdown code
  pure $ Right r
72

73
previewPostProcess :: CodeType -> Element -> Effect Unit
74 75
previewPostProcess Haskell htmlEl = do
  HLJS.highlightBlock htmlEl
76 77 78 79

previewPostProcess Python htmlEl = do
  HLJS.highlightBlock htmlEl

80 81
previewPostProcess JSON htmlEl = do
  HLJS.highlightBlock htmlEl
82

83
previewPostProcess Markdown _ = pure unit
84

85 86
-- TODO Replace with markdown-it?
-- https://pursuit.purescript.org/packages/purescript-markdown-it
87 88 89 90
-- renderMd' :: forall e. MD.ToMarkupOptions e -> String -> String
-- renderMd' options input =
--   either identity (MD.toMarkup' options >>> Smolder.render)
--   (parseMd input :: Either String (SlamDownP String))
91

92 93 94
renderMd :: String -> Effect String
renderMd = renderString
--renderMd = renderMd' MD.defaultToMarkupOptions
95 96 97

renderHaskell :: String -> String
renderHaskell s = s
98

99 100 101 102
renderPython :: String -> String
renderPython s = s


arturo's avatar
arturo committed
103 104 105 106 107 108 109 110 111 112 113
-- | The code editor contains 3 components:
-- | - a hidden textarea
-- | - textarea code overlay
-- | - html preview
-- |
-- | The overlay is to provide seamless syntax highlighting on top of the
-- | textarea.
-- |
-- | I took the idea from: https://github.com/satya164/react-simple-code-editor
codeEditor :: R2.Leaf Props
codeEditor = R2.leaf codeEditorCpt
114
codeEditorCpt :: R.Component Props
arturo's avatar
arturo committed
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
codeEditorCpt = here.component "codeEditor" cpt where
  cpt {code, defaultCodeType, onChange} _ = do
    -- | States
    -- |
    controls <- initControls code defaultCodeType

    codeS' <- T.useLive T.unequal controls.codeS
    codeType' <- T.useLive T.unequal controls.codeType
    viewType' <- T.useLive T.unequal controls.viewType

    -- | Effects
    -- |
    R.useEffect2' codeS' codeType' $ do
      setCodeOverlay controls.codeOverlayElRef codeType' codeS'
      renderHtml codeS' codeType' controls.htmlElRef controls.error

    -- | Render
    -- |
    pure $

      H.div
      { className: "code-editor" }
      [
        toolbar
        { controls, onChange }
      ,
        H.div
        { className: "row no-gutters error" }
        [
          errorComponent
          {error: controls.error}
146
        ]
arturo's avatar
arturo committed
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
      ,
        H.div
        { className: "row no-gutters editor" }
        [
          H.div
          { className: "code-area " <> (codeHidden viewType') }
          [
            H.div
            { className: "code-container" }
            [
              H.textarea
              { defaultValue: codeS'
              , on: { change: onEditChange controls.codeS codeType' onChange }
              , placeholder: "Type some code..."
              , ref: controls.codeElRef
              } []
            ,
              H.pre
              { className: (langClass codeType')
                -- , contentEditable: "true"
              , ref: controls.codeOverlayElRef
              , rows: 30
                --, on: { input: onEditChange (fst codeType) codeElRef htmlRef codeRef error }
              } []
            ]
          ]
        ,
          H.div
          { className: "v-divider " <> (dividerHidden viewType') }
          [ H.text " " ]
        ,
          H.div
          { className: "html " <> (langClass codeType') <> (previewHidden viewType')
          , ref: controls.htmlElRef
          } []
        ]
      ]
  -- | Helpers
  -- |
  codeHidden :: ViewType -> String
  codeHidden Code = ""
  codeHidden Both = ""
  codeHidden _ = " d-none"

  dividerHidden :: ViewType -> String
  dividerHidden Both = ""
  dividerHidden _ = " d-none"

  langClass :: CodeType -> String
  langClass Haskell  = " language-haskell"
  langClass JSON     = " language-json"
  langClass Markdown = " language-md"
  langClass Python = " language-python"

  previewHidden :: ViewType -> String
  previewHidden Preview = ""
  previewHidden Both = ""
  previewHidden _ = " d-none"

  onEditChange :: forall e. T.Box Code -> CodeType -> OnChangeCodeType -> e -> Effect Unit
  onEditChange codeS codeType onChange e = do
    let code = R.unsafeEventValue e
    T.write_ code codeS
    onChange codeType code
211

212 213
setCodeOverlay :: ElRef -> CodeType -> Code -> Effect Unit
setCodeOverlay codeOverlayElRef codeType code = do
214 215 216 217 218 219 220 221 222
  let mCodeOverlayEl = toMaybe $ R.readRef codeOverlayElRef
  _ <- case mCodeOverlayEl of
    Nothing -> pure unit
    Just codeOverlayEl -> do
      _ <- pure $ (codeOverlayEl .= "innerText") $ codeNlFix codeType code
      HLJS.highlightBlock codeOverlayEl
      pure unit
  pure unit

223 224
renderHtml :: Code -> CodeType -> ElRef -> T.Box (Maybe Error) -> Effect Unit
renderHtml code codeType htmlElRef error =
225 226 227
  case (toMaybe $ R.readRef htmlElRef) of
    Nothing -> pure unit
    Just htmlEl -> do
228 229
      r <- render codeType code
      case r of
230
        Left err -> do
231
          T.write_ (Just err) error
232
        Right rendered -> do
233
          T.write_ Nothing error
234
          _ <- pure $ (htmlEl .= "innerHTML") rendered
235
          previewPostProcess codeType htmlEl
236
          pure unit
237

238 239
type OnChangeCodeType = CodeType -> Code -> Effect Unit

arturo's avatar
arturo committed
240 241
type ToolbarProps =
  ( controls :: Record Controls
242
  , onChange :: OnChangeCodeType
243 244
  )

arturo's avatar
arturo committed
245 246
toolbar :: R2.Leaf ToolbarProps
toolbar = R2.leaf toolbarCpt
247
toolbarCpt :: R.Component ToolbarProps
arturo's avatar
arturo committed
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
toolbarCpt = here.component "toolbar" cpt where
  cpt { controls: { codeS, codeType, viewType }
      , onChange
      } _ = do
    -- | States
    -- |
    codeS' <- T.useLive T.unequal codeS
    -- codeType' <- T.useLive T.unequal codeType

    -- | Render
    -- |
    pure $

      H.div
      { className: intercalate " "
        [ "code-editor__toolbar"
        , "row no-gutters align-items-center mb-3"
        ]
      }
      [
        H.div
        { className: "code-editor__toolbar__type" }
        [
          codeTypeSelector
          { codeType
          -- Handle rerendering of preview when viewType changed
          , onChange: \ct -> onChange ct codeS'
          }
        ]
      ,
        H.div
        {}
        [
          viewTypeSelector
          { state: viewType
          }
        ]
      ]
286

287 288 289

type ErrorComponentProps =
  (
290
    error :: T.Box (Maybe Error)
291 292 293 294 295
  )

errorComponent :: Record ErrorComponentProps -> R.Element
errorComponent p = R.createElement errorComponentCpt p []
errorComponentCpt :: R.Component ErrorComponentProps
296
errorComponentCpt = here.component "errorComponent" cpt
297
  where
298 299 300 301 302
    cpt { error } _ = do
      error' <- T.useLive T.unequal error

      pure $ case error' of
        Nothing -> H.div {} []
303
        Just err -> H.div { className: "text-danger mb-3" } [ H.text err ]
304 305 306


type CodeTypeSelectorProps =
arturo's avatar
arturo committed
307
  ( codeType :: T.Box CodeType
308
  , onChange :: CodeType -> Effect Unit
309 310
  )

arturo's avatar
arturo committed
311 312
codeTypeSelector :: R2.Leaf CodeTypeSelectorProps
codeTypeSelector = R2.leaf codeTypeSelectorCpt
313
codeTypeSelectorCpt :: R.Component CodeTypeSelectorProps
arturo's avatar
arturo committed
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
codeTypeSelectorCpt = here.component "codeTypeSelector" cpt where
  cpt { codeType, onChange } _ = do
    -- | States
    -- |
    codeType' <- T.useLive T.unequal codeType

    -- | Render
    -- |
    pure $

      H.div
      { className: "input-group input-group-sm" }
      [
        H.div
        { className: "input-group-prepend" }
        [
          B.icon
          { name: "code"
          , className: "input-group-text"
          }
        ]
      ,
        R2.select
        { className: "form-control"
        , defaultValue: show codeType'
        , on: { change: onSelectChange codeType onChange }
        , style: { width: "150px" }
        }
342
        (option <$> [JSON, Markdown, Haskell, Python])
arturo's avatar
arturo committed
343
      ]
344

arturo's avatar
arturo committed
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
  -- | Helpers
  -- |
  option :: CodeType -> R.Element
  option value = H.option { value: show value } [ H.text $ show value ]

  onSelectChange :: forall e. T.Box CodeType -> (CodeType -> Effect Unit) -> e -> Effect Unit
  onSelectChange codeType onChange e = do
    let ct = case value of
          "Haskell"  -> Haskell
          "JSON"     -> JSON
          "Markdown" -> Markdown
          "Python"   -> Python
          _          -> Markdown
    T.write_ ct codeType
    onChange ct
    where
      value = R.unsafeEventValue e
362 363


364
type ViewTypeSelectorProps =
arturo's avatar
arturo committed
365
  ( state :: T.Box ViewType
366 367
  )

arturo's avatar
arturo committed
368 369
viewTypeSelector :: R2.Leaf ViewTypeSelectorProps
viewTypeSelector = R2.leaf viewTypeSelectorCpt
370

371
viewTypeSelectorCpt :: R.Component ViewTypeSelectorProps
arturo's avatar
arturo committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
viewTypeSelectorCpt = here.component "viewTypeSelector" cpt where
  cpt { state } _ = do
    -- | States
    -- |
    state' <- T.useLive T.unequal state

    -- | Render
    -- |
    pure $

      H.div
      { className: "btn-group btn-group-sm"
      , role: "group"
      }
      [ viewTypeButton Code state' state
      , viewTypeButton Both state' state
      , viewTypeButton Preview state' state
389
      ]
390

arturo's avatar
arturo committed
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
  -- | Helpers
  -- |
  viewTypeButton viewType state' state =
    H.button
    { className: "btn btn-light" <> active
    , on: { click: \_ -> T.write viewType state }
    , type: "button"
    }
    [
      H.i { className: "fa " <> (icon viewType) } []
    ]
    where
      active = if viewType == state' then " active" else ""

  icon Preview = "fa-eye"
  icon Both = "fa-columns"
  icon Code = "fa-pencil"
408 409 410 411

type Controls =
  (
      codeElRef :: R.Ref (Nullable Element)
412 413
    , codeS :: T.Box Code
    , codeType :: T.Box CodeType
414
    , codeOverlayElRef :: R.Ref (Nullable Element)
415
    , error :: T.Box (Maybe Error)
416
    , htmlElRef :: R.Ref (Nullable Element)
417
    , viewType :: T.Box ViewType
418 419 420 421 422
  )

initControls :: Code -> CodeType -> R.Hooks (Record Controls)
initControls code defaultCodeType = do
  htmlElRef <- R.useRef null
423
  codeS <- T.useBox code
424 425
  codeElRef <- R.useRef null
  codeOverlayElRef <- R.useRef null
426 427 428
  codeType <- T.useBox defaultCodeType
  error <- T.useBox Nothing
  viewType <- T.useBox Preview
429 430 431

  pure $ {
      codeElRef
432
    , codeS
433 434 435 436 437 438
    , codeType
    , codeOverlayElRef
    , error
    , htmlElRef
    , viewType
    }
439 440

reinitControls :: Record Controls -> Code -> CodeType -> Effect Unit
441
reinitControls { codeType, codeS, error } code defaultCodeType = do
442 443 444
  T.write_ defaultCodeType codeType
  T.write_ code codeS
  T.write_ Nothing error