content: {-# LANGUAGE BangPatterns               #-}
  {-# LANGUAGE FlexibleInstances          #-}
  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  
  {-# OPTIONS_GHC -Wno-missing-export-lists #-}
  
  -- TODO: try to make it more type-safe somehow?
  
  -- | Javascript methods used for the site.
  --
  -- Some Javascript can also be found in .widget files. Hopefully, in the
  -- future this whole module would be removed.
  module Guide.JS where
  
  -- Shared imports
  import Imports
  
  import NeatInterpolation
  
  import Guide.Uid
  
  import qualified Data.Text as T
  import qualified Data.Text.Lazy.Builder as B
  
  
  -- | Javascript code.
  newtype JS = JS {fromJS :: Text}
    deriving (Show, Buildable, Semigroup, Monoid)
  
  -- | A concatenation of all Javascript functions defined in this module.
  allJSFunctions :: JS
  allJSFunctions = JS . T.unlines . map fromJS $ [
    -- Utilities
    replaceWithData, prependData, appendData,
    moveNodeUp, moveNodeDown,
    switchSection, switchSectionsEverywhere,
    fadeIn, fadeOutAndRemove,
    focusOn,
    -- Misc
    createAjaxIndicator,
    autosizeTextarea,
    expandHash,
    expandItemNotes,
    showDiffPopup,
    -- Creating parts of interface
    makeItemNotesEditor,
    -- Add methods
    addCategoryAndRedirect, addItem,
    addPro, addCon,
    -- Set methods
    submitCategoryInfo, submitCategoryNotes,
    submitItemDescription,
    submitItemNotes, submitItemEcosystem,
    -- Other things
    deleteCategoryAndRedirect,
    -- Admin things
    acceptEdit, undoEdit,
    acceptBlock, undoBlock,
    createCheckpoint,
    saveToArchiveOrg]
  
  -- | A class for things that can be converted to Javascript syntax.
  class ToJS a where toJS :: a -> JS
  
  instance ToJS Bool where
    toJS True  = JS "true"
    toJS False = JS "false"
  instance ToJS JS where
    toJS = id
  instance ToJS Text where
    toJS = JS . escapeJSString
  instance ToJS Integer where
    toJS = JS . toText . show
  instance ToJS Int where
    toJS = JS . toText . show
  instance ToJS (Uid a) where
    toJS = toJS . uidToText
  
  -- | A helper class for calling Javascript functions.
  class JSParams a where
    jsParams :: a -> [JS]
  
  instance JSParams () where
    jsParams () = []
  instance ToJS a => JSParams [a] where
    jsParams = map toJS
  instance (ToJS a,ToJS b) => JSParams (a,b) where
    jsParams (a,b) = [toJS a, toJS b]
  instance (ToJS a,ToJS b,ToJS c) => JSParams (a,b,c) where
    jsParams (a,b,c) = [toJS a, toJS b, toJS c]
  instance (ToJS a,ToJS b,ToJS c,ToJS d) => JSParams (a,b,c,d) where
    jsParams (a,b,c,d) = [toJS a, toJS b, toJS c, toJS d]
  instance (ToJS a,ToJS b,ToJS c,ToJS d,ToJS e) => JSParams (a,b,c,d,e) where
    jsParams (a,b,c,d,e) = [toJS a, toJS b, toJS c, toJS d, toJS e]
  instance (ToJS a,ToJS b,ToJS c,ToJS d,ToJS e,ToJS f) => JSParams (a,b,c,d,e,f) where
    jsParams (a,b,c,d,e,f) = [toJS a, toJS b, toJS c, toJS d, toJS e, toJS f]
  
  -- | This hacky class lets you construct and use Javascript functions; you
  -- give 'makeJSFunction' function name, function parameters, and function
  -- body, and you get a polymorphic value of type @JSFunction a => a@, which
  -- you can use either as a complete function definition (if you set @a@ to
  -- be @JS@), or as a function that you can give some parameters and it would
  -- return a Javascript call:
  --
  -- > plus = makeJSFunction "plus" ["a", "b"] "return a+b;"
  --
  -- >>> plus :: JS
  -- JS "function plus(a,b) {\nreturn a+b;}\n"
  -- >>> plus (3, 5) :: JS
  -- JS "plus(3,5);"
  class JSFunction a where
    makeJSFunction
      :: Text          -- ^ Name
      -> [Text]        -- ^ Parameter names
      -> Text          -- ^ Definition
      -> a
  
  -- | Generates a function definition
  instance JSFunction JS where
    makeJSFunction fName fParams fDef =
      let paramList = T.intercalate "," fParams
      in JS $ format "function "+|fName|+"("+|paramList|+") {\n"
                  +|indentF 2 (build fDef)|+
              "}\n"
  
  -- | Generates a function that takes arguments and produces a Javascript
  -- function call
  instance JSParams a => JSFunction (a -> JS) where
    makeJSFunction fName _fParams _fDef = \args ->
      let paramList = T.intercalate "," (map fromJS (jsParams args))
      in  JS $ format "{}({});" fName paramList
  
  -- | Also produces a Javascript function call, but prefixes the function
  -- with @this.@; this is needed for event handlers in Vue for some reason
  newtype WithThis a = WithThis { withThis :: a }
  
  instance JSFunction a => JSFunction (WithThis a) where
    makeJSFunction fName fParams fDef = WithThis $
      makeJSFunction ("this." <> fName) fParams fDef
  
  -- | NB: this isn't a standalone function and so it doesn't have to be
  -- listed in 'allJSFunctions'.
  assign :: ToJS x => JS -> x -> JS
  assign v x = JS $ format "{} = {};" v (toJS x)
  
  -- TODO: all links here shouldn't be absolute [absolute-links]
  
  replaceWithData :: JSFunction a => a
  replaceWithData =
    makeJSFunction "replaceWithData" ["node"]
    [text|
      return function(data) {$(node).replaceWith(data);};
    |]
  
  prependData :: JSFunction a => a
  prependData =
    makeJSFunction "prependData" ["node"]
    [text|
      return function(data) {$(node).prepend(data);};
    |]
  
  appendData :: JSFunction a => a
  appendData =
    makeJSFunction "appendData" ["node"]
    [text|
      return function(data) {$(node).append(data);};
    |]
  
  -- | Move node up (in a list of sibling nodes), ignoring anchor elements
  -- inserted by 'thisNode'.
  moveNodeUp :: JSFunction a => a
  moveNodeUp =
    makeJSFunction "moveNodeUp" ["node"]
    [text|
      var el = $(node);
      while (el.prev().is(".dummy"))
        el.prev().before(el);
      if (el.not(':first-child'))
        el.prev().before(el);
    |]
  
  -- | Move node down (in a list of sibling nodes), ignoring anchor elements
  -- inserted by 'thisNode'.
  moveNodeDown :: JSFunction a => a
  moveNodeDown =
    makeJSFunction "moveNodeDown" ["node"]
    [text|
      var el = $(node);
      while (el.next().is(".dummy"))
        el.next().after(el);
      if (el.not(':last-child'))
        el.next().after(el);
    |]
  
  -- | Given something that contains section divs (or spans), show one and
  -- hide the rest. The div/span with the given @class@ will be chosen.
  --
  -- See Note [show-hide]
  switchSection :: JSFunction a => a
  switchSection =
    makeJSFunction "switchSection" ["node", "section"]
    [text|
      $(node).children(".section").removeClass("shown");
      $(node).children(".section."+section).addClass("shown");
      // See Note [autosize]
      autosize($('textarea'));
      autosize.update($('textarea'));
    |]
  
  -- | Switch sections /everywhere/ inside the container.
  --
  -- See Note [show-hide]
  switchSectionsEverywhere :: JSFunction a => a
  switchSectionsEverywhere =
    makeJSFunction "switchSectionsEverywhere" ["node", "section"]
    [text|
      $(node).find(".section").removeClass("shown");
      $(node).find(".section."+section).addClass("shown");
      // See Note [autosize]
      autosize($('textarea'));
      autosize.update($('textarea'));
    |]
  
  -- | This function makes the node half-transparent and then animates it to
  -- full opaqueness. It's useful when e.g. something has been moved and you
  -- want to “flash” the item to draw user's attention to it.
  fadeIn :: JSFunction a => a
  fadeIn =
    makeJSFunction "fadeIn" ["node"]
    [text|
      $(node).fadeTo(0,0.2).fadeTo(600,1);
    |]
  
  -- | This function animates the node to half-transparency and then removes it
  -- completely. It's useful when you're removing something and you want to
  -- draw user's attention to the fact that it's being removed.
  --
  -- The reason there isn't a simple @fadeOut@ utility function here is that
  -- removal has to be done by passing a callback to @fadeTo@. In jQuery you
  -- can't simply wait until the animation has stopped.
  fadeOutAndRemove :: JSFunction a => a
  fadeOutAndRemove =
    makeJSFunction "fadeOutAndRemove" ["node"]
    [text|
       $(node).fadeTo(400,0.2,function(){$(node).remove()});
    |]
  
  focusOn :: JSFunction a => a
  focusOn =
    makeJSFunction "focusOn" ["node"]
    [text|
       $(node).focus();
    |]
  
  createAjaxIndicator :: JSFunction a => a
  createAjaxIndicator =
    makeJSFunction "createAjaxIndicator" []
    [text|
      $("body").prepend('<div id="ajax-indicator"></div>');
      $(document).ajaxStart(function() {
        $("#ajax-indicator").show();
      });
      $(document).ajaxStop(function() {
        $("#ajax-indicator").hide();
      });
      $("#ajax-indicator").hide();
    |]
  
  autosizeTextarea :: JSFunction a => a
  autosizeTextarea =
    makeJSFunction "autosizeTextarea" ["textareaNode"]
    [text|
      autosize(textareaNode);
      autosize.update(textareaNode);
    |]
  
  -- | Read the anchor from the address bar (i.e. the thing after #) and use it
  -- to expand something (e.g. notes). It's needed to implement linking
  -- properly – e.g. notes are usually unexpanded, but when you're giving
  -- someone a direct link to notes, it makes sense to expand them. If you call
  -- 'expandHash' after the page has loaded, it will do just that.
  expandHash :: JSFunction a => a
  expandHash =
    makeJSFunction "expandHash" []
    [text|
      hash = $(location).attr('hash');
      if (hash.slice(0,12) == "#item-notes-") {
        if (hash.indexOf('-', 12) != (-1))
          // For links to sections of items' notes (from the TOC)
          itemId = hash.slice(12, hash.indexOf('-', 12))
        else
          // For links to items' notes
          itemId = hash.slice(12);
        expandItemNotes(itemId);
      } else
      if (hash.slice(0,6) == "#item-") {
        itemId = hash.slice(6);
        expandItemNotes(itemId);
      }
    |]
  
  expandItemNotes :: JSFunction a => a
  expandItemNotes =
    makeJSFunction "expandItemNotes" ["itemId"]
    [text|
      switchSection("#item-notes-"+itemId, "expanded");
    |]
  
  showDiffPopup :: JSFunction a => a
  showDiffPopup =
    makeJSFunction "showDiffPopup" ["ours", "modified", "merged", "send"]
    [text|
      dialog = $("<div>", {
        "class" : "diff-popup"
      })[0];
      choices = $("<div>", {
        "class" : "diff-choices"
      })[0];
  
      // our version
      choiceOurs = $("<div>", {
        "class" : "var-a" })[0];
      textOurs = $("<div>", {
        "class" : "text",
        "text"  : ours })[0];
      headerOurs = $("<strong>", {
        "text" : "Your version" })[0];
      buttonOurs = $("<button>", {
        "text" : "Submit this version, disregard changes on the server" })[0];
      $(buttonOurs).click(function() {
        send(ours); });
      $(choiceOurs).append(headerOurs, textOurs, buttonOurs);
  
      // modified version
      choiceMod = $("<div>", {
        "class" : "var-b" })[0];
      textMod = $("<div>", {
        "class" : "text",
        "text"  : modified })[0];
      headerMod = $("<strong>", {
        "text" : "Version on the server" })[0];
      buttonMod = $("<button>", {
        "text" : "Accept this version, disregard my changes" })[0];
      $(buttonMod).click(function() {
        send(modified); });
      $(choiceMod).append(headerMod, textMod, buttonMod);
  
      // building merged
      choiceMerged = $("<div>", {
        "class" : "var-merged" })[0];
      areaMerged = $("<textarea>", {
        "autocomplete" : "off",
        "text"         : merged })[0];
      headerMerged = $("<strong>", {
        "text" : "Merged version (edit if needed)" })[0];
      buttonMerged = $("<button>", {
        "text" : "Submit the merged version" })[0];
      $(buttonMerged).click(function () {
        send(areaMerged.value); });
      $(choiceMerged).append(headerMerged, areaMerged, buttonMerged);
  
      $(choices).append(choiceOurs, choiceMod, choiceMerged);
      $(dialog).append(choices);
  
      $.magnificPopup.open({
        modal: true,
        items: {
          src: dialog,
          type: 'inline' }
      });
  
      autosizeTextarea(areaMerged);
    |]
  
  
  {- Note [blurb diffing]
  ~~~~~~~~~~~~~~~~~~~~~~~
  
  A note on why we need the 'wasEmpty' parameter in 'makeItemNotesEditor'.
  
  Assume that the notes are empty. The text in the area, therefore, will be
  some default blurb (“# Links, #Imports, #Usage”, etc). Suppose the user edits
  it. What will be sent to the server?
  
    * original: blurb
    * our version: modified blurb
  
  What will happen next? The server will compare it to the value currently at
  the server (i.e. an empty string), and think that the blurb *was* on the
  server but got deleted while the client was doing editing. This is wrong, and
  will result in a diff popup comparing an edited blurb to an empty string. To
  prevent this, we pass 'wasEmpty' to 'makeItemNotesEditor' – if we're using a
  blurb, we'll pass an empty string as the original.
  
  -}
  
  -- | Dynamically creates a 'View.markdownEditor' (but specifically for item
  -- notes). See Note [dynamic interface].
  makeItemNotesEditor :: JSFunction a => a
  makeItemNotesEditor =
    -- See Note [blurb diffing]
    makeJSFunction "makeItemNotesEditor"
                   ["notesNode", "sectionNode", "textareaUid",
                    "wasEmpty", "content", "itemId"]
    [text|
      $(sectionNode).html("");
      area = $("<textarea>", {
        "autocomplete" : "off",
        "rows"         : "10",
        "id"           : textareaUid,
        "class"        : "big fullwidth",
        "text"         : content })[0];
      saveBtn = $("<input>", {
        "value" : "Save",
        "type"  : "button" })[0];
      save = function () {
        submitItemNotes(notesNode,
                        itemId,
                        wasEmpty ? "" : content,
                        area.value); };
      saveBtn.onclick = save;
      cancelBtn = $("<input>", {
        "value" : "Cancel",
        "type"  : "button" })[0];
      cancel = function () {
        $(sectionNode).html("");
        switchSection(notesNode, "expanded"); };
      cancelBtn.onclick = cancel;
      area.onkeydown = function (event) {
        if ((event.keyCode == 13 || event.keyCode == 10) &&
            (event.metaKey || event.ctrlKey)) {
          save();
          return false; }
        if (event.keyCode == 27) {
          cancel();
          return false; }
        };
      // Can't use $()-generation here because then the <span> would have
      // to be cloned (since we're inserting it multiple times) and I don't
      // know how to do that.
      space = "<span style='margin-left:6px'></span>";
      enter = $("<span>", {
        "class": "edit-field-instruction",
        "text" : "or press Ctrl+Enter to save" })[0];
      markdownSupported = $("<img>", {
        "src": "/markdown.svg",
        "class": "  markdown-supported "
        })[0];
      markdown = $("<a>", {
        "href"   : "/markdown",
        "target" : "_blank"})[0];
      $(sectionNode).append(
        area, saveBtn, $(space), cancelBtn, $(space), enter, $(markdown).append(markdownSupported));
    |]
  
  -- | Create a new category and redirect to it (or redirect to an old category
  -- if it exists already).
  addCategoryAndRedirect :: JSFunction a => a
  addCategoryAndRedirect =
    makeJSFunction "addCategoryAndRedirect" ["s"]
    [text|
      $.post("/haskell/add/category", {content: s})
       .done(function (url) {
          window.location.href = url;
        });
    |]
  
  -- | Add a new item to some category.
  addItem :: JSFunction a => a
  addItem =
    makeJSFunction "addItem" ["node", "catId", "s"]
    [text|
      $.post("/haskell/add/category/"+catId+"/item", {name: s})
       .done(appendData(node));
    |]
  
  submitCategoryInfo :: JSFunction a => a
  submitCategoryInfo =
    makeJSFunction "submitCategoryInfo" ["infoNode", "catId", "form"]
    [text|
      $.post("/haskell/set/category/"+catId+"/info", $(form).serialize())
       .done(function (data) {
          $(infoNode).replaceWith(data);
          // If pros-cons-enabled and other *enabled properties were changed, we
          // have to show/hide relevant sections in all items of the category.
          // See Note [enabled sections] for details.
          if ($(form)[0]["pros-cons-enabled"].checked)
                   $(".pros-cons-wrapper").show();
              else $(".pros-cons-wrapper").hide();
          if ($(form)[0]["ecosystem-enabled"].checked)
                   $(".ecosystem-wrapper").show();
              else $(".ecosystem-wrapper").hide();
          if ($(form)[0]["notes-enabled"].checked)
                   $(".notes-wrapper").show();
              else $(".notes-wrapper").hide();
       });
    |]
  
  submitCategoryNotes :: JSFunction a => a
  submitCategoryNotes =
    makeJSFunction "submitCategoryNotes"
                   ["node", "catId", "original, ours"]
    [text|
      $.post({
        url: "/haskell/set/category/"+catId+"/notes",
        data: {
          original: original,
          content: ours },
        success: function (data) {
          $.magnificPopup.close();
          $(node).replaceWith(data); },
        statusCode: {
          409: function (xhr, st, err) {
            modified = xhr.responseJSON["modified"];
            merged   = xhr.responseJSON["merged"];
            showDiffPopup(ours, modified, merged, function (x) {
              submitCategoryNotes(node, catId, modified, x) }); } }
        });
    |]
  
  submitItemDescription :: JSFunction a => a
  submitItemDescription =
    makeJSFunction "submitItemDescription"
                   ["node", "itemId", "original", "ours"]
    [text|
      $.post({
        url: "/haskell/set/item/"+itemId+"/description",
        data: {
          original: original,
          content: ours },
        success: function (data) {
          $.magnificPopup.close();
          $(node).replaceWith(data); },
        statusCode: {
          409: function (xhr, st, err) {
            modified = xhr.responseJSON["modified"];
            merged   = xhr.responseJSON["merged"];
            showDiffPopup(ours, modified, merged, function (x) {
              submitItemDescription(node, itemId, modified, x) }); } }
        });
    |]
  
  submitItemEcosystem :: JSFunction a => a
  submitItemEcosystem =
    makeJSFunction "submitItemEcosystem"
                   ["node", "itemId", "original", "ours"]
    [text|
      $.post({
        url: "/haskell/set/item/"+itemId+"/ecosystem",
        data: {
          original: original,
          content: ours },
        success: function (data) {
          $.magnificPopup.close();
          $(node).replaceWith(data); },
        statusCode: {
          409: function (xhr, st, err) {
            modified = xhr.responseJSON["modified"];
            merged   = xhr.responseJSON["merged"];
            showDiffPopup(ours, modified, merged, function (x) {
              submitItemEcosystem(node, itemId, modified, x) }); } }
        });
    |]
  
  submitItemNotes :: JSFunction a => a
  submitItemNotes =
    makeJSFunction "submitItemNotes"
                   ["node", "itemId", "original", "ours"]
    [text|
      $.post({
        url: "/haskell/set/item/"+itemId+"/notes",
        data: {
          original: original,
          content: ours },
        success: function (data) {
          $.magnificPopup.close();
          $(node).replaceWith(data);
          // Switching has to be done here and not in 'Main.renderItemNotes'
          // because $.post is asynchronous and will be done *after*
          // switchSection has worked.
          switchSection(node, "expanded"); },
        statusCode: {
          409: function (xhr, st, err) {
            modified = xhr.responseJSON["modified"];
            merged   = xhr.responseJSON["merged"];
            showDiffPopup(ours, modified, merged, function (x) {
              submitItemNotes(node, itemId, modified, x) }); } }
        });
    |]
  
  -- | Add a pro to some item.
  addPro :: JSFunction a => a
  addPro =
    makeJSFunction "addPro" ["node", "itemId", "s"]
    [text|
      $.post("/haskell/add/item/"+itemId+"/pro", {content: s})
       .done(function (data) {
          var jData = $(data);
          jData.appendTo(node);
          switchSection(jData, "editable");
        });
    |]
  
  -- | Add a con to some item.
  addCon :: JSFunction a => a
  addCon =
    makeJSFunction "addCon" ["node", "itemId", "s"]
    [text|
      $.post("/haskell/add/item/"+itemId+"/con", {content: s})
       .done(function (data) {
          var jData = $(data);
          jData.appendTo(node);
          switchSection(jData, "editable");
        });
    |]
  
  deleteCategoryAndRedirect :: JSFunction a => a
  deleteCategoryAndRedirect =
    makeJSFunction "deleteCategoryAndRedirect" ["catId"]
    [text|
      if (confirm("Confirm deletion?")) {
        $.post("/haskell/delete/category/"+catId)
         .done(function () {
            window.location.href = "/haskell";
         });
      }
    |]
  
  acceptEdit :: JSFunction a => a
  acceptEdit =
    makeJSFunction "acceptEdit" ["editId", "editNode"]
    [text|
      $.post("/admin/edit/"+editId+"/accept")
       .done(function () {
          fadeOutAndRemove(editNode);
       });
    |]
  
  undoEdit :: JSFunction a => a
  undoEdit =
    makeJSFunction "undoEdit" ["editId", "editNode"]
    [text|
      $.post("/admin/edit/"+editId+"/undo")
       .done(function (data) {
          if (data == "")
            fadeOutAndRemove(editNode);
          else
            alert("couldn't undo edit: " + data);
       });
    |]
  
  acceptBlock :: JSFunction a => a
  acceptBlock =
    makeJSFunction "acceptBlock" ["editLatest", "editEarliest", "blockNode"]
    [text|
      $.post("/admin/edits/"+editLatest+"/"+editEarliest+"/accept")
       .done(function () {
          fadeOutAndRemove(blockNode);
       });
    |]
  
  undoBlock :: JSFunction a => a
  undoBlock =
    makeJSFunction "undoBlock" ["editLatest", "editEarliest", "blockNode"]
    [text|
      $.post("/admin/edits/"+editLatest+"/"+editEarliest+"/undo")
       .done(function (data) {
          if (data == "")
            fadeOutAndRemove(blockNode);
          else
            $(blockNode).replaceWith(data);
       });
    |]
  
  createCheckpoint :: JSFunction a => a
  createCheckpoint =
    makeJSFunction "createCheckpoint" ["buttonNode"]
    [text|
      $.post("/admin/create-checkpoint")
       .done(function () {
          fadeIn(buttonNode);
       });
    |]
  
  saveToArchiveOrg :: JSFunction a => a
  saveToArchiveOrg =
    makeJSFunction "saveToArchiveOrg" ["link"]
    [text|
      $.post('http://web.archive.org/save/' + link)
        .done(function () {
          console.log(link + " saved to archive.org")
        });
    |]
  -- When adding a function, don't forget to add it to 'allJSFunctions'!
  
  escapeJSString :: Text -> Text
  escapeJSString s =
      toText $
      B.singleton '"' <> quote s <> B.singleton '"'
    where
      quote q = case T.uncons t of
        Nothing       -> toTextBuilder h
        Just (!c, t') -> toTextBuilder h <> escape c <> quote t'
        where
          (h, t) = T.break isEscape q
      -- 'isEscape' doesn't mention \n, \r and \t because they are handled by
      -- the “< '\x20'” case; yes, later 'escape' escapes them differently,
      -- but it's irrelevant
      isEscape c = c == '\"' || c == '\\' ||
                   c == '\x2028' || c == '\x2029' ||
                   c < '\x20'
      escape '\"' = "\\\""
      escape '\\' = "\\\\"
      escape '\n' = "\\n"
      escape '\r' = "\\r"
      escape '\t' = "\\t"
      escape c
        | c < '\x20' || c == '\x2028' || c == '\x2029' =
            "\\u" <> padLeftF 4 '0' (hexF (fromEnum c))
        | otherwise =
            B.singleton c
  
  newtype JQuerySelector = JQuerySelector Text
    deriving (ToJS, Buildable)
  
  selectId :: Text -> JQuerySelector
  selectId x = JQuerySelector $ format "#{}" x
  
  selectUid :: Uid a -> JQuerySelector
  selectUid x = JQuerySelector $ format "#{}" x
  
  selectClass :: Text -> JQuerySelector
  selectClass x = JQuerySelector $ format ".{}" x
  
  selectParent :: JQuerySelector -> JQuerySelector
  selectParent x = JQuerySelector $ format ":has(> {})" x
  
  selectChildren :: JQuerySelector -> JQuerySelector -> JQuerySelector
  selectChildren a b = JQuerySelector $ format "{} > {}" a b
  
  selectSection :: JQuerySelector -> Text -> JQuerySelector
  selectSection a b = JQuerySelector $ format "{} > .section.{}" a b
  