Search code examples
jsonhaskellobject-literalaesontemplate-haskell

How do I use Aeson to generate a literal (unquoted) javascript expression?


i'm using this function and need to pass it an Aeson Value:

{ logLevel : vega.Debug }

this is supposed to refer to an enum in a javascript package that the binding doesn't export.

afaict i'm supposed to use Data.Aeson.QQ.Simple for this, but everything i try that compiles puts quotes around "vega.Debug", which i can't have.

[aesonQQ| { logLevel : "vega.Debug" } |]

what am i missing? is there a way to use encode for this?


Solution

  • In general, Aeson Values represent JSON objects only, so they don’t support embedded JavaScript expressions, or any other extensions.

    If this API only accepts Values, you’re stuck. I think the best solution is to just duplicate the integer value of vega.Debug and serialise that.

    Otherwise, a straightforward solution is to make a modified version of toHtmlWith that accepts a more flexible input type, such as a string:

    toHtmlWith' :: Maybe Text -> VegaLite -> Text
    toHtmlWith' mopts vl =
      let spec = encodeToLazyText (fromVL vl)
          -- NB: Removed ‘encodeToLazyText’ call here.
          opts = maybe "" (\o -> "," <> o) mopts
    
      in TL.unlines
        [ "<!DOCTYPE html>"
        , "<html>"
        , "<head>"
          -- versions are fixed at vega 5, vega-lite 4
        , "  <script src=\"https://cdn.jsdelivr.net/npm/vega@5\"></script>"
        , "  <script src=\"https://cdn.jsdelivr.net/npm/vega-lite@4\"></script>"
        , "  <script src=\"https://cdn.jsdelivr.net/npm/vega-embed\"></script>"
        , "</head>"
        , "<body>"
        , "<div id=\"vis\"></div>"
        , "<script type=\"text/javascript\">"
        , "  var spec = " <> spec <> ";"
        , "  vegaEmbed(\'#vis\', spec" <> opts <> ").then(function(result) {"
        , "  // Access the Vega view instance (https://vega.github.io/vega/docs/api/view/) as result.view"
        , "  }).catch(console.error);"
        , "</script>"
        , "</body>"
        , "</html>"
        ]
    

    Then you can call encodeToLazyText on your own Aeson values, or include arbitrary Text strings as needed.

    If you really want to avoid duplicating the page contents, then you could also call the existing toHtmlWith with a Value containing a special delimiter that you control, such as String "<user1441998>vega.Debug</user1441998>", and then use that delimiter to postprocess the result:

    unquoteHackSplices = replace "\"<user1441998>" ""
      . replace "</user1441998>\"" ""
    

    is there a way to use encode for this?

    As yet another hack, you could make a ToJSON instance for your type that implements toEncoding but not toJSON, and have the encoded value be a JavaScript expression (i.e. invalid JSON). You would want to make toJSON raise an error so you don’t use it inadvertently.


    If you want to generate JavaScript code in general, I would have a look at language-javascript. Instead of producing a Value, produce a JSExpression and then use one of the pretty-printing functions like renderToText to render it. Here’s a sketch of the structure of a possible solution:

    -- Like ‘ToJSON’ but may produce arbitrary JavaScript expressions
    class ToJavaScript a where
      toJavaScript :: a -> JSExpression
    
    -- Helper function to convert from Aeson Value
    jsFromJson :: Value -> JSExpression
    jsFromJson v = case v of
      Object o -> JSObjectLiteral …
      Array a -> JSArrayLiteral …
      String s -> JSStringLiteral …
      …
    
    instance ToJavaScript YourType where
      toJavaScript = …
    
    rendered :: Text
    rendered = renderToText
      $ JSAstExpression (toJavaScript yourValue) JSNoAnnot
    

    Your expression would have the form:

    JSMemberDot
      (JSIdentifier JSNoAnnot "vega")
      JSNoAnnot
      (JSIdentifier JSNoAnnot "Debug")
    

    The JSAnnot type would also allow you to include comments in the generated result. Bear in mind that the language-javascript pretty-printing is likely less well optimised than Aeson’s JSON serialisation.