You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by oy...@apache.org on 2021/03/05 08:21:29 UTC

[tinkerpop] branch TINKERPOP-2530 updated (15e269d -> a8b30b8)

This is an automated email from the ASF dual-hosted git repository.

oyvindsabo pushed a change to branch TINKERPOP-2530
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git.


 discard 15e269d  TINKERPOP-2530: Transfer OyvindSabo/gremlint and OyvindSabo/gremlint.com to apache/tinkerpop/gremlint
     new a8b30b8  TINKERPOP-2530: Transfer OyvindSabo/gremlint and OyvindSabo/gremlint.com to apache/tinkerpop/gremlint

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (15e269d)
            \
             N -- N -- N   refs/heads/TINKERPOP-2530 (a8b30b8)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


[tinkerpop] 01/01: TINKERPOP-2530: Transfer OyvindSabo/gremlint and OyvindSabo/gremlint.com to apache/tinkerpop/gremlint

Posted by oy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

oyvindsabo pushed a commit to branch TINKERPOP-2530
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit a8b30b84cd1105d75b5808e5e792a3d014a2c873
Author: oyvindsabo <oy...@gmail.com>
AuthorDate: Thu Mar 4 23:14:58 2021 +0100

    TINKERPOP-2530: Transfer OyvindSabo/gremlint and OyvindSabo/gremlint.com to apache/tinkerpop/gremlint
    
    Transfers OyvindSabo/gremlint and OyvindSabo/gremlint.com into a
    gremlint directory at the root of the apache/tinkerpop repository.
    This is the first step to bring the Gremlint code recently donated by
    Ardoq into TinkerPop. Some files, like the README.md and package.json
    files have links to OyvindSabo/gremlint and OyvindSabo/gremlint.com.
    Also, gremlint.com still installs gremlint from
    github:OyvindSabo/gremlint#master.
---
 gremlint/gremlint.com/.gitignore                   |   2 +
 gremlint/gremlint.com/.prettierrc                  |   5 +
 gremlint/gremlint.com/LICENSE                      | 202 +++++
 gremlint/gremlint.com/README.md                    |  50 ++
 gremlint/gremlint.com/package.json                 |  58 ++
 gremlint/gremlint.com/public/CNAME                 |   1 +
 gremlint/gremlint.com/public/favicon.ico           | Bin 0 -> 82144 bytes
 gremlint/gremlint.com/public/index.html            |  41 +
 gremlint/gremlint.com/public/logo512.png           | Bin 0 -> 9664 bytes
 gremlint/gremlint.com/public/manifest.json         |  15 +
 gremlint/gremlint.com/public/robots.txt            |   3 +
 gremlint/gremlint.com/src/App.css                  |  38 +
 gremlint/gremlint.com/src/App.test.tsx             |   9 +
 gremlint/gremlint.com/src/App.tsx                  |  54 ++
 .../gremlint.com/src/components/CodePreview.tsx    |  73 ++
 gremlint/gremlint.com/src/components/FadeIn.tsx    |  41 +
 .../src/components/LoadingAnimation.tsx            | 108 +++
 .../src/components/NavigationButton.tsx            |  60 ++
 gremlint/gremlint.com/src/components/Navigator.tsx |  60 ++
 gremlint/gremlint.com/src/components/Paragraph.tsx |  45 ++
 .../gremlint.com/src/components/QueryInput.tsx     |  55 ++
 gremlint/gremlint.com/src/components/Spacer.ts     |  26 +
 .../gremlint.com/src/components/StyleGuideRule.tsx |  41 +
 .../gremlint.com/src/components/TextButton.tsx     |  56 ++
 gremlint/gremlint.com/src/components/Title.tsx     |  45 ++
 gremlint/gremlint.com/src/components/Toggle.tsx    |  93 +++
 .../src/gremlint-loading-logo-colored.png          | Bin 0 -> 315251 bytes
 .../src/gremlint-loading-logo-grayscale.png        | Bin 0 -> 220873 bytes
 gremlint/gremlint.com/src/index.css                |  13 +
 gremlint/gremlint.com/src/index.tsx                |  17 +
 .../src/libs/reduced-state/dispatch.ts             |  22 +
 .../gremlint.com/src/libs/reduced-state/index.ts   |  21 +
 .../src/libs/reduced-state/reducedState.ts         |  51 ++
 .../gremlint.com/src/libs/reduced-state/types.ts   |  36 +
 .../src/libs/reduced-state/useReducedState.ts      |  35 +
 gremlint/gremlint.com/src/react-app-env.d.ts       |   1 +
 gremlint/gremlint.com/src/reportWebVitals.ts       |  15 +
 gremlint/gremlint.com/src/router.ts                |  27 +
 gremlint/gremlint.com/src/setupTests.ts            |   5 +
 gremlint/gremlint.com/src/store/actions.ts         |  25 +
 gremlint/gremlint.com/src/store/index.ts           |  27 +
 gremlint/gremlint.com/src/store/initialState.ts    |  29 +
 gremlint/gremlint.com/src/store/reducers.ts        |  85 +++
 gremlint/gremlint.com/src/store/routines.ts        |  41 +
 gremlint/gremlint.com/src/store/types.ts           |  27 +
 gremlint/gremlint.com/src/styleVariables.ts        |  26 +
 .../src/views/QueryFormatter/AdvancedOptions.tsx   | 102 +++
 .../src/views/QueryFormatter/index.tsx             |  56 ++
 .../gremlint.com/src/views/StyleGuide/index.tsx    |  32 +
 .../gremlint.com/src/views/StyleGuide/rules.ts     | 304 ++++++++
 gremlint/gremlint.com/tsconfig.json                |  26 +
 gremlint/gremlint.com/tslint.json                  |   3 +
 gremlint/gremlint/.gitignore                       |   2 +
 gremlint/gremlint/.prettierrc                      |   5 +
 gremlint/gremlint/LICENSE                          | 202 +++++
 gremlint/gremlint/README.md                        | 122 +++
 gremlint/gremlint/jestconfig.json                  |   7 +
 gremlint/gremlint/package.json                     |  48 ++
 .../__tests__/closureIndentation.test.ts           | 349 +++++++++
 .../curlyBracketMultilineWrapping.test.ts          | 171 +++++
 .../__tests__/curlyBracketWrapping.test.ts         | 111 +++
 .../formatQuery/__tests__/defaultConfig.test.ts    |  79 ++
 .../determineWhatPartsOfCodeAreGremlin.test.ts     | 125 +++
 .../__tests__/dotsAfterLineBreaks.test.ts          |  61 ++
 .../invalidIndentationAndMaxLineLength.test.ts     |  31 +
 .../formatQuery/__tests__/maxLineLength.test.ts    |  70 ++
 .../__tests__/modulatorIndentation.test.ts         | 841 +++++++++++++++++++++
 .../__tests__/modulatorWrapping.test.ts            | 128 ++++
 .../__tests__/multipleQueriesAtOnce.test.ts        |  68 ++
 .../__tests__/nonGremlinIndentation.test.ts        |  79 ++
 .../__tests__/nonMethodIndentation.test.ts         |  40 +
 gremlint/gremlint/src/formatQuery/consts.ts        |  35 +
 .../formatQuery/formatSyntaxTrees/formatClosure.ts |  89 +++
 .../formatQuery/formatSyntaxTrees/formatMethod.ts  | 102 +++
 .../formatSyntaxTrees/formatNonGremlin.ts          |  30 +
 .../formatQuery/formatSyntaxTrees/formatString.ts  |  31 +
 .../formatTraversal/getStepGroups/index.ts         |  58 ++
 .../getStepGroups/reduceFirstStepInStepGroup.ts    |  70 ++
 .../getStepGroups/reduceLastStepInStepGroup.ts     |  76 ++
 .../getStepGroups/reduceMiddleStepInStepGroup.ts   |  58 ++
 .../getStepGroups/reduceSingleStepInStepGroup.ts   |  81 ++
 .../formatTraversal/getStepGroups/utils.ts         | 124 +++
 .../formatSyntaxTrees/formatTraversal/index.ts     |  87 +++
 .../formatQuery/formatSyntaxTrees/formatWord.ts    |  33 +
 .../src/formatQuery/formatSyntaxTrees/index.ts     |  49 ++
 .../src/formatQuery/formatSyntaxTrees/utils.ts     |  73 ++
 gremlint/gremlint/src/formatQuery/index.ts         |  54 ++
 .../__tests__/extractGremlinQueries.test.ts        | 564 ++++++++++++++
 .../parseToSyntaxTrees/extractGremlinQueries.ts    | 173 +++++
 .../src/formatQuery/parseToSyntaxTrees/index.ts    | 391 ++++++++++
 .../recreateQueryOnelinerFromSyntaxTree.ts         |  75 ++
 .../recreateQueryStringFromFormattedSyntaxTrees.ts |  97 +++
 gremlint/gremlint/src/formatQuery/types.ts         | 169 +++++
 gremlint/gremlint/src/formatQuery/utils.ts         |  40 +
 gremlint/gremlint/src/index.ts                     |  20 +
 gremlint/gremlint/tsconfig.json                    |  12 +
 gremlint/gremlint/tslint.json                      |   6 +
 97 files changed, 7343 insertions(+)

diff --git a/gremlint/gremlint.com/.gitignore b/gremlint/gremlint.com/.gitignore
new file mode 100644
index 0000000..b7dab5e
--- /dev/null
+++ b/gremlint/gremlint.com/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+build
\ No newline at end of file
diff --git a/gremlint/gremlint.com/.prettierrc b/gremlint/gremlint.com/.prettierrc
new file mode 100644
index 0000000..a0d1c9a
--- /dev/null
+++ b/gremlint/gremlint.com/.prettierrc
@@ -0,0 +1,5 @@
+{
+  "printWidth": 120,
+  "trailingComma": "all",
+  "singleQuote": true
+}
diff --git a/gremlint/gremlint.com/LICENSE b/gremlint/gremlint.com/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/gremlint/gremlint.com/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/gremlint/gremlint.com/README.md b/gremlint/gremlint.com/README.md
new file mode 100644
index 0000000..1322c3b
--- /dev/null
+++ b/gremlint/gremlint.com/README.md
@@ -0,0 +1,50 @@
+![Gremlint Github Header 1920x1024](https://user-images.githubusercontent.com/25663729/88488788-d5a73700-cf8f-11ea-9adb-03d62c77c1b7.png)
+
+### What is [gremlint.com](https://gremlint.com)?
+
+[Gremlint](https://github.com/OyvindSabo/gremlint) is a code formatting library which parses Gremlin queries and rewrites them to adhere to certain styling rules.
+
+[Gremlint.com](https://gremlint.com) is a website which utilizes the Gremlint library to give users an online "living" style guide for Gremlin queries. It also serves as a platform for showcasing the features of Gremlint.
+
+### Give it a try!
+
+https://gremlint.com
+
+## Available Scripts
+
+In the project directory, you can run:
+
+```
+npm install
+```
+
+Installs dependencies.
+
+```
+npm start
+```
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+```
+npm test
+```
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+```
+npm run build
+```
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
diff --git a/gremlint/gremlint.com/package.json b/gremlint/gremlint.com/package.json
new file mode 100644
index 0000000..8d839eb
--- /dev/null
+++ b/gremlint/gremlint.com/package.json
@@ -0,0 +1,58 @@
+{
+  "homepage": "https://gremlint.com",
+  "name": "gremlint.com",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@testing-library/jest-dom": "^5.11.4",
+    "@testing-library/react": "^11.1.0",
+    "@testing-library/user-event": "^12.1.10",
+    "@types/jest": "^26.0.15",
+    "@types/node": "^12.0.0",
+    "@types/react": "^16.9.53",
+    "@types/react-dom": "^16.9.8",
+    "gremlint": "github:OyvindSabo/gremlint#master",
+    "react": "^17.0.1",
+    "react-dom": "^17.0.1",
+    "react-scripts": "^4.0.3",
+    "sharp-router": "^4.1.5",
+    "styled-components": "^5.2.1",
+    "typescript": "^4.0.3",
+    "web-vitals": "^0.2.4"
+  },
+  "scripts": {
+    "predeploy": "npm run build",
+    "deploy": "gh-pages -d build",
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "author": "Øyvind Sæbø",
+  "license": "Apache-2.0",
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "@types/styled-components": "^5.1.4",
+    "gh-pages": "^3.1.0",
+    "prettier": "^2.1.2",
+    "tslint": "^6.1.3",
+    "tslint-config-prettier": "^1.18.0"
+  }
+}
diff --git a/gremlint/gremlint.com/public/CNAME b/gremlint/gremlint.com/public/CNAME
new file mode 100644
index 0000000..755baaf
--- /dev/null
+++ b/gremlint/gremlint.com/public/CNAME
@@ -0,0 +1 @@
+gremlint.com
\ No newline at end of file
diff --git a/gremlint/gremlint.com/public/favicon.ico b/gremlint/gremlint.com/public/favicon.ico
new file mode 100644
index 0000000..1c73e71
Binary files /dev/null and b/gremlint/gremlint.com/public/favicon.ico differ
diff --git a/gremlint/gremlint.com/public/index.html b/gremlint/gremlint.com/public/index.html
new file mode 100644
index 0000000..672f571
--- /dev/null
+++ b/gremlint/gremlint.com/public/index.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta name="description" content="Web site created using create-react-app" />
+    <meta name="google-site-verification" content="8rkkiQkZaBwVUAUBxSY6Nj_EBHqCGPEYnEJmlyXuLnw" />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>
diff --git a/gremlint/gremlint.com/public/logo512.png b/gremlint/gremlint.com/public/logo512.png
new file mode 100644
index 0000000..a4e47a6
Binary files /dev/null and b/gremlint/gremlint.com/public/logo512.png differ
diff --git a/gremlint/gremlint.com/public/manifest.json b/gremlint/gremlint.com/public/manifest.json
new file mode 100644
index 0000000..3292390
--- /dev/null
+++ b/gremlint/gremlint.com/public/manifest.json
@@ -0,0 +1,15 @@
+{
+  "short_name": "Gremlint",
+  "name": "Gremlint - Gremlin query formatter",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "512x512",
+      "type": "image/png"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}
diff --git a/gremlint/gremlint.com/public/robots.txt b/gremlint/gremlint.com/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/gremlint/gremlint.com/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/gremlint/gremlint.com/src/App.css b/gremlint/gremlint.com/src/App.css
new file mode 100644
index 0000000..74b5e05
--- /dev/null
+++ b/gremlint/gremlint.com/src/App.css
@@ -0,0 +1,38 @@
+.App {
+  text-align: center;
+}
+
+.App-logo {
+  height: 40vmin;
+  pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  .App-logo {
+    animation: App-logo-spin infinite 20s linear;
+  }
+}
+
+.App-header {
+  background-color: #282c34;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: calc(10px + 2vmin);
+  color: white;
+}
+
+.App-link {
+  color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
diff --git a/gremlint/gremlint.com/src/App.test.tsx b/gremlint/gremlint.com/src/App.test.tsx
new file mode 100644
index 0000000..2a68616
--- /dev/null
+++ b/gremlint/gremlint.com/src/App.test.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+  render(<App />);
+  const linkElement = screen.getByText(/learn react/i);
+  expect(linkElement).toBeInTheDocument();
+});
diff --git a/gremlint/gremlint.com/src/App.tsx b/gremlint/gremlint.com/src/App.tsx
new file mode 100644
index 0000000..d1bd9ef
--- /dev/null
+++ b/gremlint/gremlint.com/src/App.tsx
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState } from 'react';
+import './App.css';
+import styled from 'styled-components';
+import { useRouter } from 'sharp-router';
+import router from './router';
+import FadeIn from './components/FadeIn';
+import Navigator from './components/Navigator';
+import QueryFormatter from './views/QueryFormatter';
+import StyleGuide from './views/StyleGuide';
+import LoadingAnimation from './components/LoadingAnimation';
+
+const ViewWrapper = styled.div`
+  width: min(800px, 100vw);
+  margin-left: calc(50vw - min(400px, 50vw));
+`;
+
+const App = () => {
+  const { matchedRoute } = useRouter(router);
+  const [loadingComplete, setLoadingComplete] = useState(false);
+  if (!loadingComplete) return <LoadingAnimation onLoadingComplete={() => setLoadingComplete(true)} />;
+  return (
+    <FadeIn>
+      <div>
+        <Navigator matchedRoute={matchedRoute} />
+        <div>
+          <ViewWrapper>
+            {matchedRoute === '/' ? <QueryFormatter /> : matchedRoute === '/style-guide' ? <StyleGuide /> : null}
+          </ViewWrapper>
+        </div>
+      </div>
+    </FadeIn>
+  );
+};
+
+export default App;
diff --git a/gremlint/gremlint.com/src/components/CodePreview.tsx b/gremlint/gremlint.com/src/components/CodePreview.tsx
new file mode 100644
index 0000000..1179dea
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/CodePreview.tsx
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { HTMLAttributes } from 'react';
+import { disabledTextColor, textColor } from '../styleVariables';
+
+const CodePreviewWrapper = styled.div`
+  padding: 10px;
+`;
+
+const CodePreviewBox = styled.div`
+  border-radius: 5px;
+  font-family: 'Courier New', Courier, monospace;
+  background: rgba(0, 0, 0, 0.05);
+  outline: none;
+  font-size: 15px;
+  padding: 10px;
+  border: none;
+  resize: none;
+  box-shadow: inset rgba(0, 0, 0, 0.5) 0 0 10px -5px;
+  white-space: pre-wrap;
+  overflow: auto;
+  position: relative;
+`;
+
+const Code = styled.div`
+  color: ${textColor};
+  line-height: 20px;
+  font-size: 15px;
+`;
+
+const CodeRuler = styled.div<{ $maxLineLength: number }>`
+  top: 0;
+  left: 0;
+  width: calc(10px + ${({ $maxLineLength }) => $maxLineLength}ch);
+  border-right: 1px solid ${disabledTextColor};
+  position: absolute;
+  height: 100%;
+  pointer-events: none;
+`;
+
+type CodePreviewProps = {
+  maxLineLength?: number;
+} & HTMLAttributes<HTMLSpanElement>;
+
+const CodePreview = ({ maxLineLength, children }: CodePreviewProps) => (
+  <CodePreviewWrapper>
+    <CodePreviewBox>
+      <Code>{children}</Code>
+      {maxLineLength ? <CodeRuler $maxLineLength={maxLineLength} /> : null}
+    </CodePreviewBox>
+  </CodePreviewWrapper>
+);
+
+export default CodePreview;
diff --git a/gremlint/gremlint.com/src/components/FadeIn.tsx b/gremlint/gremlint.com/src/components/FadeIn.tsx
new file mode 100644
index 0000000..621fb74
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/FadeIn.tsx
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { HTMLAttributes, useEffect, useState } from 'react';
+import styled from 'styled-components';
+
+const FadeInWrapper = styled.div<{ $opacity: number }>`
+  opacity: ${({ $opacity }) => $opacity};
+`;
+
+const FadeIn = ({ children, ...props }: HTMLAttributes<HTMLDivElement>) => {
+  const [opacity, setOpacity] = useState(0);
+
+  useEffect(() => {
+    setTimeout(() => setOpacity(1));
+  }, []);
+
+  return (
+    <FadeInWrapper $opacity={opacity} {...props}>
+      {children}
+    </FadeInWrapper>
+  );
+};
+
+export default FadeIn;
diff --git a/gremlint/gremlint.com/src/components/LoadingAnimation.tsx b/gremlint/gremlint.com/src/components/LoadingAnimation.tsx
new file mode 100644
index 0000000..0fa89a6
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/LoadingAnimation.tsx
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { white } from '../styleVariables';
+import gremlintLoadingLogoColored from '../gremlint-loading-logo-colored.png';
+import gremlintLoadingLogoGrayscale from '../gremlint-loading-logo-grayscale.png';
+
+const LoadingAnimationWrapper = styled.div`
+  position: fixed;
+  background: ${white};
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+`;
+
+const GrayscaleImageWrapper = styled.div`
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  bottom: calc(50vh - 25vmin);
+`;
+
+const ColoredImageWrapper = styled.div<{ $loadingCompletion: number }>`
+  overflow: hidden;
+  height: ${({ $loadingCompletion }) => $loadingCompletion / 2}vmin;
+  width: 100%;
+  position: absolute;
+  bottom: calc(50vh - 25vmin);
+`;
+
+const Image = styled.img<{ $opacity: number }>`
+  opacity: ${({ $opacity }) => $opacity};
+  transition: 0.25s;
+  height: 50vmin;
+  width: 50vmin;
+  display: block;
+  margin: auto;
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translate(-50%, 0);
+`;
+
+type LoadingAnimationProps = {
+  onLoadingComplete: VoidFunction;
+};
+
+const LoadingAnimation = ({ onLoadingComplete }: LoadingAnimationProps) => {
+  const [loadingCompletion, setLoadingCompletion] = useState(0);
+  const [coloredImageHasLoaded, setColoredImageHasLoaded] = useState(false);
+  const [grayscaleImageHasLoaded, setGrayscaleImageHasLoaded] = useState(false);
+
+  useEffect(() => {
+    setTimeout(
+      () => {
+        if (loadingCompletion < 100) {
+          if (coloredImageHasLoaded && grayscaleImageHasLoaded) {
+            setLoadingCompletion(loadingCompletion + 1);
+          }
+        } else {
+          setTimeout(onLoadingComplete, 250);
+        }
+      },
+      loadingCompletion === 0 ? 250 : 10,
+    );
+  }, [loadingCompletion, coloredImageHasLoaded, grayscaleImageHasLoaded, onLoadingComplete]);
+
+  return (
+    <LoadingAnimationWrapper>
+      <GrayscaleImageWrapper>
+        <Image
+          src={gremlintLoadingLogoGrayscale}
+          $opacity={grayscaleImageHasLoaded && loadingCompletion !== 100 ? 1 : 0}
+          onLoad={() => setGrayscaleImageHasLoaded(true)}
+        />
+      </GrayscaleImageWrapper>
+      <ColoredImageWrapper $loadingCompletion={loadingCompletion}>
+        <Image
+          src={gremlintLoadingLogoColored}
+          $opacity={loadingCompletion !== 100 ? 1 : 0}
+          onLoad={() => setColoredImageHasLoaded(true)}
+        />
+      </ColoredImageWrapper>
+    </LoadingAnimationWrapper>
+  );
+};
+
+export default LoadingAnimation;
diff --git a/gremlint/gremlint.com/src/components/NavigationButton.tsx b/gremlint/gremlint.com/src/components/NavigationButton.tsx
new file mode 100644
index 0000000..55aebbe
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/NavigationButton.tsx
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { highlightColor, highlightedTextColor, textColor } from '../styleVariables';
+
+const NavigationButtonWrapper = styled.span`
+  display: inline-block;
+  vertical-align: bottom;
+  padding: 10px;
+  box-sizing: border-box;
+  height: 40px;
+  width: 160px;
+`;
+
+const NavigationButtonLink = styled.a<{ $isSelected: boolean }>`
+  text-decoration: none;
+  display: inline-block;
+  height: 20px;
+  line-height: 20px;
+  font-size: 15px;
+  color: ${({ $isSelected }) => ($isSelected ? highlightedTextColor : textColor)};
+  border-bottom: ${({ $isSelected }) => ($isSelected ? `2px solid ${highlightColor}` : 'none')};
+  &:hover {
+    color: ${highlightedTextColor};
+  }
+`;
+
+type NavigationButtonProps = {
+  isSelected: boolean;
+  href: string;
+  label: string;
+};
+
+const NavigationButton = ({ isSelected, href, label }: NavigationButtonProps) => (
+  <NavigationButtonWrapper>
+    <NavigationButtonLink href={href} $isSelected={isSelected}>
+      {label}
+    </NavigationButtonLink>
+  </NavigationButtonWrapper>
+);
+
+export default NavigationButton;
diff --git a/gremlint/gremlint.com/src/components/Navigator.tsx b/gremlint/gremlint.com/src/components/Navigator.tsx
new file mode 100644
index 0000000..fe62af5
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/Navigator.tsx
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import NavigationButton from './NavigationButton';
+import { white } from '../styleVariables';
+
+const NavigatorWrapper = styled.div`
+  background: ${white};
+  box-shadow: ${white} 0 0 10px;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1;
+`;
+
+const NavigatorCenterContainer = styled.div`
+  width: min(800px, 100vw);
+  margin-left: calc(50vw - min(400px, 50vw));
+`;
+
+const Spacer = styled.div`
+  height: 40px;
+`;
+
+type NavigatorProps = {
+  matchedRoute: string;
+};
+
+const Navigator = ({ matchedRoute }: NavigatorProps) => (
+  <div>
+    <NavigatorWrapper>
+      <NavigatorCenterContainer>
+        <NavigationButton isSelected={matchedRoute === '/'} label="Query formatter" href="#/" />
+        <NavigationButton isSelected={matchedRoute === '/style-guide'} label="Style guide" href="#/style-guide" />
+      </NavigatorCenterContainer>
+    </NavigatorWrapper>
+    <Spacer />
+  </div>
+);
+
+export default Navigator;
diff --git a/gremlint/gremlint.com/src/components/Paragraph.tsx b/gremlint/gremlint.com/src/components/Paragraph.tsx
new file mode 100644
index 0000000..665aef7
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/Paragraph.tsx
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled, { css } from 'styled-components';
+import { HTMLAttributes } from 'react';
+import { textColor } from '../styleVariables';
+
+const ParagraphWrapper = styled.div<{ $hasContent: boolean }>`
+  ${({ $hasContent }) =>
+    $hasContent &&
+    css`
+      padding: 10px;
+    `}
+`;
+
+const ParagraphContent = styled.span`
+  color: ${textColor};
+  line-height: 20px;
+  font-size: 15px;
+`;
+
+const Paragraph = ({ children }: HTMLAttributes<HTMLSpanElement>) => (
+  <ParagraphWrapper $hasContent={Boolean(children)}>
+    <ParagraphContent>{children}</ParagraphContent>
+  </ParagraphWrapper>
+);
+
+export default Paragraph;
diff --git a/gremlint/gremlint.com/src/components/QueryInput.tsx b/gremlint/gremlint.com/src/components/QueryInput.tsx
new file mode 100644
index 0000000..2f95281
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/QueryInput.tsx
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { inputTextColor } from '../styleVariables';
+
+const QueryInputWrapper = styled.div`
+  padding: 10px;
+`;
+
+const QueryInputTextArea = styled.textarea`
+  height: calc(100vh / 4);
+  border-radius: 5px;
+  font-family: 'Courier New', Courier, monospace;
+  background: rgba(0, 0, 0, 0.05);
+  outline: none;
+  font-size: 16px;
+  padding: 10px;
+  border: none;
+  resize: none;
+  width: 100%;
+  box-shadow: inset rgba(0, 0, 0, 0.5) 0 0 10px -5px;
+  color: ${inputTextColor};
+  box-sizing: border-box;
+`;
+
+type QueryInputProps = {
+  onChange?: ((event: React.ChangeEvent<HTMLTextAreaElement>) => void) | undefined;
+  value: string;
+};
+
+const QueryInput = ({ onChange, value }: QueryInputProps) => (
+  <QueryInputWrapper>
+    <QueryInputTextArea onChange={onChange} value={value} rows={25} />
+  </QueryInputWrapper>
+);
+
+export default QueryInput;
diff --git a/gremlint/gremlint.com/src/components/Spacer.ts b/gremlint/gremlint.com/src/components/Spacer.ts
new file mode 100644
index 0000000..a2f12f1
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/Spacer.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import styled from 'styled-components';
+
+const Spacer = styled.div`
+  height: 20px;
+`;
+
+export default Spacer;
diff --git a/gremlint/gremlint.com/src/components/StyleGuideRule.tsx b/gremlint/gremlint.com/src/components/StyleGuideRule.tsx
new file mode 100644
index 0000000..825bcb7
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/StyleGuideRule.tsx
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import Paragraph from './Paragraph';
+import Title from './Title';
+import CodePreview from './CodePreview';
+import Spacer from './Spacer';
+
+type StyleGuideRuleProps = {
+  title: string;
+  explanation: string;
+  example: string;
+};
+
+const StyleGuideRule = ({ title, explanation, example }: StyleGuideRuleProps) => (
+  <div>
+    <Title>{title}</Title>
+    <Paragraph>{explanation}</Paragraph>
+    <CodePreview>{example}</CodePreview>
+    <Spacer />
+  </div>
+);
+
+export default StyleGuideRule;
diff --git a/gremlint/gremlint.com/src/components/TextButton.tsx b/gremlint/gremlint.com/src/components/TextButton.tsx
new file mode 100644
index 0000000..6a4ec6a
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/TextButton.tsx
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { highlightedTextColor, textColor } from '../styleVariables';
+
+const TextButtonWrapper = styled.span`
+  display: inline-block;
+  padding: 10px;
+  box-sizing: border-box;
+`;
+
+const TextButtonButton = styled.button`
+  height: 20px;
+  line-height: 20px;
+  font-size: 15px;
+  color: ${textColor};
+  &: {
+    color: ${highlightedTextColor};
+  }
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 0;
+  outline: none;
+`;
+
+type TextButtonProps = {
+  label: string;
+  onClick: VoidFunction;
+};
+
+const TextButton = ({ label, onClick }: TextButtonProps) => (
+  <TextButtonWrapper>
+    <TextButtonButton onClick={onClick}>{label}</TextButtonButton>
+  </TextButtonWrapper>
+);
+
+export default TextButton;
diff --git a/gremlint/gremlint.com/src/components/Title.tsx b/gremlint/gremlint.com/src/components/Title.tsx
new file mode 100644
index 0000000..4a8594a
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/Title.tsx
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled, { css } from 'styled-components';
+import { HTMLAttributes } from 'react';
+import { textColor } from '../styleVariables';
+
+const TitleWrapper = styled.div<{ $hasContent: boolean }>`
+  ${({ $hasContent }) =>
+    $hasContent &&
+    css`
+      padding: 10px;
+    `}
+`;
+
+const TitleContent = styled.div`
+  color: ${textColor};
+  line-height: 30px;
+  font-size: 25px;
+`;
+
+const Title = ({ children }: HTMLAttributes<HTMLDivElement>) => (
+  <TitleWrapper $hasContent={Boolean(children)}>
+    <TitleContent>{children}</TitleContent>
+  </TitleWrapper>
+);
+
+export default Title;
diff --git a/gremlint/gremlint.com/src/components/Toggle.tsx b/gremlint/gremlint.com/src/components/Toggle.tsx
new file mode 100644
index 0000000..09adb36
--- /dev/null
+++ b/gremlint/gremlint.com/src/components/Toggle.tsx
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { borderColor, highlightedTextColor, textColor, white } from '../styleVariables';
+
+const ToggleContainer = styled.span<{ $width: string; $height: string }>`
+  display: inline-block;
+  height: ${({ $height }) => $height};
+  width: ${({ $width }) => $width};
+  border-radius: 5px;
+  background: rgba(0, 0, 0, 0.05);
+  box-shadow: inset rgba(0, 0, 0, 0.5) 0 0 10px -5px;
+  position: relative;
+`;
+
+const Option = styled.span<{ $width: string; $height: string }>`
+  cursor: pointer;
+  display: inline-block;
+  height: ${({ $height }) => $height};
+  width: calc(${({ $width }) => $width} / 2);
+  box-sizing: border-box;
+  padding: 10px;
+  line-height: 20px;
+  font-size: 16px;
+  color: ${textColor};
+  text-align: center;
+`;
+
+const SelectedOption = styled.span<{ $checked: boolean }>`
+  background: ${white};
+  cursor: pointer;
+  display: inline-block;
+  position: absolute;
+  top: 0;
+  left: ${({ $checked }) => ($checked ? '160px' : '0')};
+  height: 40px;
+  width: 160px;
+  border-radius: 5px;
+  box-sizing: border-box;
+  padding: 10px;
+  line-height: 20px;
+  font-size: 16px;
+  color: ${highlightedTextColor};
+  text-align: center;
+  border: 1px solid ${borderColor};
+  transition: 0.5s;
+`;
+
+type ToggleProps = {
+  width: string;
+  height: string;
+  checked: boolean;
+  labels: { checked: string; unchecked: string };
+  onChange: (checked: boolean) => void;
+};
+
+const Toggle = ({
+  width = '320px',
+  height = '40px',
+  checked = false,
+  labels = { checked: 'Checked', unchecked: 'Unchecked' },
+  onChange,
+}: ToggleProps) => (
+  <ToggleContainer $width={width} $height={height}>
+    <Option $width={width} $height={height} onClick={() => onChange(false)}>
+      {labels.unchecked}
+    </Option>
+    <Option $width={width} $height={height} onClick={() => onChange(true)}>
+      {labels.checked}
+    </Option>
+    <SelectedOption $checked={checked}>{checked ? labels.checked : labels.unchecked}</SelectedOption>
+  </ToggleContainer>
+);
+
+export default Toggle;
diff --git a/gremlint/gremlint.com/src/gremlint-loading-logo-colored.png b/gremlint/gremlint.com/src/gremlint-loading-logo-colored.png
new file mode 100644
index 0000000..9439d45
Binary files /dev/null and b/gremlint/gremlint.com/src/gremlint-loading-logo-colored.png differ
diff --git a/gremlint/gremlint.com/src/gremlint-loading-logo-grayscale.png b/gremlint/gremlint.com/src/gremlint-loading-logo-grayscale.png
new file mode 100644
index 0000000..4799023
Binary files /dev/null and b/gremlint/gremlint.com/src/gremlint-loading-logo-grayscale.png differ
diff --git a/gremlint/gremlint.com/src/index.css b/gremlint/gremlint.com/src/index.css
new file mode 100644
index 0000000..ec2585e
--- /dev/null
+++ b/gremlint/gremlint.com/src/index.css
@@ -0,0 +1,13 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}
diff --git a/gremlint/gremlint.com/src/index.tsx b/gremlint/gremlint.com/src/index.tsx
new file mode 100644
index 0000000..b0a768f
--- /dev/null
+++ b/gremlint/gremlint.com/src/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  document.getElementById('root'),
+);
+
+// If you want to start measuring performance in your app, pass a function
+// to log results (for example: reportWebVitals(console.log))
+// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+reportWebVitals();
diff --git a/gremlint/gremlint.com/src/libs/reduced-state/dispatch.ts b/gremlint/gremlint.com/src/libs/reduced-state/dispatch.ts
new file mode 100644
index 0000000..554473e
--- /dev/null
+++ b/gremlint/gremlint.com/src/libs/reduced-state/dispatch.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const dispatch = (action: string, payload?: any) => {
+  window.dispatchEvent(new CustomEvent(action, { detail: payload }));
+};
diff --git a/gremlint/gremlint.com/src/libs/reduced-state/index.ts b/gremlint/gremlint.com/src/libs/reduced-state/index.ts
new file mode 100644
index 0000000..f16ecb5
--- /dev/null
+++ b/gremlint/gremlint.com/src/libs/reduced-state/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { dispatch } from './dispatch';
+export { default } from './reducedState';
diff --git a/gremlint/gremlint.com/src/libs/reduced-state/reducedState.ts b/gremlint/gremlint.com/src/libs/reduced-state/reducedState.ts
new file mode 100644
index 0000000..c57e8dd
--- /dev/null
+++ b/gremlint/gremlint.com/src/libs/reduced-state/reducedState.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ChangeListener, CreateReducedStateProps } from './types';
+
+const createReducedState = <T>({ initialState, reducers, routines }: CreateReducedStateProps<T>) => {
+  let state = initialState;
+  let changeListeners: ChangeListener<T>[] = [];
+
+  Object.entries(reducers).forEach(([action, reducer]) => {
+    window.addEventListener(action, ((event: CustomEvent) => {
+      const nextState = reducer(state, event.detail);
+      state = nextState;
+      changeListeners.forEach((changeListener) => changeListener(state));
+    }) as EventListener);
+  });
+
+  Object.entries(routines).forEach(([action, routine]) => {
+    window.addEventListener(action, ((event: CustomEvent) => {
+      routine(state, event.detail);
+    }) as EventListener);
+  });
+
+  const addChangeListener = (changeListenerToBeAdded: ChangeListener<T>) => {
+    changeListeners = [...changeListeners, changeListenerToBeAdded];
+  };
+
+  const removeChangeListener = (changeListenerToBeRemoved: ChangeListener<T>) => {
+    changeListeners = changeListeners.filter((changeListener) => changeListener !== changeListenerToBeRemoved);
+  };
+
+  return { state, addChangeListener, removeChangeListener };
+};
+
+export default createReducedState;
diff --git a/gremlint/gremlint.com/src/libs/reduced-state/types.ts b/gremlint/gremlint.com/src/libs/reduced-state/types.ts
new file mode 100644
index 0000000..b6f9b4a
--- /dev/null
+++ b/gremlint/gremlint.com/src/libs/reduced-state/types.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type Reducer<T> = (state: T, payload: any) => T;
+
+export type Routine<T> = (state: T, payload: any) => void;
+
+export type CreateReducedStateProps<T> = {
+  initialState: T;
+  reducers: Record<string, Reducer<T>>;
+  routines: Record<string, Routine<T>>;
+};
+
+export type ChangeListener<T> = (state: T) => void;
+
+export type ReducedState<T> = {
+  state: T;
+  addChangeListener: (changeListenerToBeAdded: ChangeListener<T>) => void;
+  removeChangeListener: (changeListenerToBeRemoved: ChangeListener<T>) => void;
+};
diff --git a/gremlint/gremlint.com/src/libs/reduced-state/useReducedState.ts b/gremlint/gremlint.com/src/libs/reduced-state/useReducedState.ts
new file mode 100644
index 0000000..b41f927
--- /dev/null
+++ b/gremlint/gremlint.com/src/libs/reduced-state/useReducedState.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useState } from 'react';
+import { ReducedState } from './types';
+
+export const useReducedState = <T>(reducedState: ReducedState<T>) => {
+  const [state, setState] = useState<T>(reducedState.state);
+
+  useEffect(() => {
+    const changeListener = (state: T) => {
+      setState(state);
+    };
+    reducedState.addChangeListener(changeListener);
+    return () => reducedState.removeChangeListener(changeListener);
+  }, [reducedState]);
+
+  return state;
+};
diff --git a/gremlint/gremlint.com/src/react-app-env.d.ts b/gremlint/gremlint.com/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/gremlint/gremlint.com/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/gremlint/gremlint.com/src/reportWebVitals.ts b/gremlint/gremlint.com/src/reportWebVitals.ts
new file mode 100644
index 0000000..49a2a16
--- /dev/null
+++ b/gremlint/gremlint.com/src/reportWebVitals.ts
@@ -0,0 +1,15 @@
+import { ReportHandler } from 'web-vitals';
+
+const reportWebVitals = (onPerfEntry?: ReportHandler) => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;
diff --git a/gremlint/gremlint.com/src/router.ts b/gremlint/gremlint.com/src/router.ts
new file mode 100644
index 0000000..29e47d5
--- /dev/null
+++ b/gremlint/gremlint.com/src/router.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import createRouter from 'sharp-router';
+
+const router = createRouter({
+  '/': 'Gremlint - Query formatter',
+  '/style-guide': 'Gremlint - Style guide',
+});
+
+export default router;
diff --git a/gremlint/gremlint.com/src/setupTests.ts b/gremlint/gremlint.com/src/setupTests.ts
new file mode 100644
index 0000000..8f2609b
--- /dev/null
+++ b/gremlint/gremlint.com/src/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';
diff --git a/gremlint/gremlint.com/src/store/actions.ts b/gremlint/gremlint.com/src/store/actions.ts
new file mode 100644
index 0000000..c0979a6
--- /dev/null
+++ b/gremlint/gremlint.com/src/store/actions.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const SET_QUERY_INPUT = 'SET_QUERY_INPUT';
+export const FORMAT_QUERY = 'FORMAT_QUERY';
+export const TOGGLE_SHOULD_SHOW_ADVANCED_OPTIONS = 'TOGGLE_SHOULD_SHOW_ADVANCED_OPTIONS';
+export const SET_INDENTATION = 'SET_INDENTATION';
+export const SET_MAX_LINE_LENGTH = 'SET_MAX_LINE_LENGTH';
+export const SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS = 'SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS';
diff --git a/gremlint/gremlint.com/src/store/index.ts b/gremlint/gremlint.com/src/store/index.ts
new file mode 100644
index 0000000..b63ccfb
--- /dev/null
+++ b/gremlint/gremlint.com/src/store/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import createReducedState from '../libs/reduced-state';
+import initialState from './initialState';
+import reducers from './reducers';
+import routines from './routines';
+
+const store = createReducedState({ initialState, reducers, routines });
+
+export default store;
diff --git a/gremlint/gremlint.com/src/store/initialState.ts b/gremlint/gremlint.com/src/store/initialState.ts
new file mode 100644
index 0000000..30bbfa1
--- /dev/null
+++ b/gremlint/gremlint.com/src/store/initialState.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+const initialState = {
+  queryInput: '',
+  queryOutput: '',
+  shouldShowAdvancedOptions: false,
+  indentation: 0,
+  maxLineLength: 72,
+  shouldPlaceDotsAfterLineBreaks: false,
+};
+
+export default initialState;
diff --git a/gremlint/gremlint.com/src/store/reducers.ts b/gremlint/gremlint.com/src/store/reducers.ts
new file mode 100644
index 0000000..01471bf
--- /dev/null
+++ b/gremlint/gremlint.com/src/store/reducers.ts
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from 'gremlint';
+import {
+  FORMAT_QUERY,
+  SET_INDENTATION,
+  SET_MAX_LINE_LENGTH,
+  SET_QUERY_INPUT,
+  SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS,
+  TOGGLE_SHOULD_SHOW_ADVANCED_OPTIONS,
+} from './actions';
+import { State } from './types';
+
+const handleSetQueryInput = (state: State, queryInput: string) => ({
+  ...state,
+  queryInput,
+});
+
+const handleFormatQuery = (state: State) => ({
+  ...state,
+  queryOutput: formatQuery(state.queryInput, {
+    indentation: state.indentation,
+    maxLineLength: state.maxLineLength,
+    shouldPlaceDotsAfterLineBreaks: state.shouldPlaceDotsAfterLineBreaks,
+  }),
+});
+
+const handleToggleShouldShowAdvancedOptions = (state: State) => ({
+  ...state,
+  shouldShowAdvancedOptions: !state.shouldShowAdvancedOptions,
+});
+
+const handleSetIndentation = (state: State, unparsedIndentation: string) => {
+  const indentation = parseInt(unparsedIndentation);
+  if (isNaN(indentation)) return { ...state };
+  if (indentation < 0) return { ...state, indentation: 0 };
+  const { maxLineLength } = state;
+  if (indentation > maxLineLength) {
+    return { ...state, indentation: maxLineLength };
+  }
+  return { ...state, indentation };
+};
+
+const handleSetMaxLineLength = (state: State, unparsedMaxLineLength: string) => {
+  const maxLineLength = parseInt(unparsedMaxLineLength);
+  if (isNaN(maxLineLength)) return { ...state };
+  const { indentation } = state;
+  if (maxLineLength < indentation) {
+    return { ...state, maxLineLength: indentation };
+  }
+  return { ...state, maxLineLength };
+};
+
+const handleSetShouldPlaceDotsAfterLineBreaks = (state: State, shouldPlaceDotsAfterLineBreaks: boolean) => ({
+  ...state,
+  shouldPlaceDotsAfterLineBreaks,
+});
+
+const reducers = {
+  [SET_QUERY_INPUT]: handleSetQueryInput,
+  [FORMAT_QUERY]: handleFormatQuery,
+  [TOGGLE_SHOULD_SHOW_ADVANCED_OPTIONS]: handleToggleShouldShowAdvancedOptions,
+  [SET_INDENTATION]: handleSetIndentation,
+  [SET_MAX_LINE_LENGTH]: handleSetMaxLineLength,
+  [SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS]: handleSetShouldPlaceDotsAfterLineBreaks,
+};
+
+export default reducers;
diff --git a/gremlint/gremlint.com/src/store/routines.ts b/gremlint/gremlint.com/src/store/routines.ts
new file mode 100644
index 0000000..d0bca5c
--- /dev/null
+++ b/gremlint/gremlint.com/src/store/routines.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { dispatch } from '../libs/reduced-state';
+import {
+  SET_QUERY_INPUT,
+  FORMAT_QUERY,
+  SET_INDENTATION,
+  SET_MAX_LINE_LENGTH,
+  SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS,
+} from './actions';
+
+const handleSetQueryInput = () => dispatch(FORMAT_QUERY);
+const handleSetIndentation = () => dispatch(FORMAT_QUERY);
+const handleSetMaxLineLength = () => dispatch(FORMAT_QUERY);
+const handleSetShouldPlaceDotsAfterLineBreaks = () => dispatch(FORMAT_QUERY);
+
+const routines = {
+  [SET_QUERY_INPUT]: handleSetQueryInput,
+  [SET_INDENTATION]: handleSetIndentation,
+  [SET_MAX_LINE_LENGTH]: handleSetMaxLineLength,
+  [SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS]: handleSetShouldPlaceDotsAfterLineBreaks,
+};
+
+export default routines;
diff --git a/gremlint/gremlint.com/src/store/types.ts b/gremlint/gremlint.com/src/store/types.ts
new file mode 100644
index 0000000..dd5f8b1
--- /dev/null
+++ b/gremlint/gremlint.com/src/store/types.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type State = {
+  queryInput: string;
+  queryOutput: string;
+  shouldShowAdvancedOptions: boolean;
+  indentation: number;
+  maxLineLength: number;
+  shouldPlaceDotsAfterLineBreaks: boolean;
+};
diff --git a/gremlint/gremlint.com/src/styleVariables.ts b/gremlint/gremlint.com/src/styleVariables.ts
new file mode 100644
index 0000000..55c2f28
--- /dev/null
+++ b/gremlint/gremlint.com/src/styleVariables.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const borderColor = 'lightgray';
+export const textColor = 'slategray';
+export const inputTextColor = 'darkslategray';
+export const highlightedTextColor = 'darkslategray';
+export const disabledTextColor = 'lightgray';
+export const highlightColor = 'yellowgreen';
+export const white = 'white';
diff --git a/gremlint/gremlint.com/src/views/QueryFormatter/AdvancedOptions.tsx b/gremlint/gremlint.com/src/views/QueryFormatter/AdvancedOptions.tsx
new file mode 100644
index 0000000..529e459
--- /dev/null
+++ b/gremlint/gremlint.com/src/views/QueryFormatter/AdvancedOptions.tsx
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { dispatch } from '../../libs/reduced-state';
+import { useReducedState } from '../../libs/reduced-state/useReducedState';
+import store from '../../store';
+import { SET_INDENTATION, SET_MAX_LINE_LENGTH, SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS } from '../../store/actions';
+import { inputTextColor, textColor } from '../../styleVariables';
+import Toggle from '../../components/Toggle';
+
+const AdvancedOptionRowWrapper = styled.div`
+  padding: 10px;
+`;
+
+const AdvancedOptionLabel = styled.div`
+  height: 20px;
+  line-height: 20px;
+  font-size: 15px;
+  color: ${textColor};
+`;
+
+const AdvancedOptionInput = styled.input`
+  border-radius: 5px;
+  background: rgba(0, 0, 0, 0.05);
+  outline: none;
+  font-size: 16px;
+  padding: 10px;
+  border: none;
+  box-shadow: inset rgba(0, 0, 0, 0.5) 0 0 10px -5px;
+  color: ${inputTextColor};
+  display: inline-block;
+  vertical-align: bottom;
+  box-sizing: border-box;
+  height: 40px;
+  width: 320px;
+`;
+
+const AdvancedOptions = () => {
+  const state = useReducedState(store);
+  return (
+    <div>
+      <AdvancedOptionRowWrapper>
+        <AdvancedOptionLabel>Indentation</AdvancedOptionLabel>
+        <AdvancedOptionInput
+          type="number"
+          min={0}
+          max={state.maxLineLength}
+          value={state.indentation}
+          onChange={({ target }) => {
+            dispatch(SET_INDENTATION, target.value);
+          }}
+        />
+      </AdvancedOptionRowWrapper>
+      <AdvancedOptionRowWrapper>
+        <AdvancedOptionLabel>Max line length</AdvancedOptionLabel>
+        <AdvancedOptionInput
+          type="number"
+          min={state.indentation}
+          value={state.maxLineLength}
+          onChange={({ target }) => {
+            dispatch(SET_MAX_LINE_LENGTH, target.value);
+          }}
+        />
+      </AdvancedOptionRowWrapper>
+      <AdvancedOptionRowWrapper>
+        <AdvancedOptionLabel>Dot placement</AdvancedOptionLabel>
+        <Toggle
+          height="40px"
+          width="320px"
+          checked={state.shouldPlaceDotsAfterLineBreaks}
+          labels={{
+            checked: 'After line break',
+            unchecked: 'Before line break',
+          }}
+          onChange={(shouldPlaceDotsAfterLineBreaks) => {
+            dispatch(SET_SHOULD_PLACE_DOTS_AFTER_LINE_BREAKS, shouldPlaceDotsAfterLineBreaks);
+          }}
+        />
+      </AdvancedOptionRowWrapper>
+    </div>
+  );
+};
+
+export default AdvancedOptions;
diff --git a/gremlint/gremlint.com/src/views/QueryFormatter/index.tsx b/gremlint/gremlint.com/src/views/QueryFormatter/index.tsx
new file mode 100644
index 0000000..c2214f1
--- /dev/null
+++ b/gremlint/gremlint.com/src/views/QueryFormatter/index.tsx
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import store from '../../store';
+import QueryInput from '../../components/QueryInput';
+import TextButton from '../../components/TextButton';
+import CodePreview from '../../components/CodePreview';
+import AdvancedOptions from './AdvancedOptions';
+import { State } from '../../store/types';
+import { dispatch } from '../../libs/reduced-state';
+import { useReducedState } from '../../libs/reduced-state/useReducedState';
+import { SET_QUERY_INPUT, TOGGLE_SHOULD_SHOW_ADVANCED_OPTIONS } from '../../store/actions';
+
+const ExpandableAdvancedOptionsWrapper = styled.div<{ $isExpanded: boolean }>`
+  max-height: ${({ $isExpanded }) => ($isExpanded ? '240px' : '0')};
+  box-shadow: inset white 0 0 10px 0;
+  overflow: hidden;
+  transition: 0.5s;
+`;
+
+const QueryFormatter = () => {
+  const state = useReducedState<State>(store);
+  return (
+    <div>
+      <QueryInput value={state.queryInput} onChange={({ target }) => dispatch(SET_QUERY_INPUT, target.value)} />
+      <TextButton
+        label={state.shouldShowAdvancedOptions ? 'Hide advanced options' : 'Show advanced options'}
+        onClick={() => dispatch(TOGGLE_SHOULD_SHOW_ADVANCED_OPTIONS)}
+      />
+      <ExpandableAdvancedOptionsWrapper $isExpanded={state.shouldShowAdvancedOptions}>
+        <AdvancedOptions />
+      </ExpandableAdvancedOptionsWrapper>
+      {state.queryOutput ? <CodePreview maxLineLength={state.maxLineLength}>{state.queryOutput}</CodePreview> : null}
+    </div>
+  );
+};
+
+export default QueryFormatter;
diff --git a/gremlint/gremlint.com/src/views/StyleGuide/index.tsx b/gremlint/gremlint.com/src/views/StyleGuide/index.tsx
new file mode 100644
index 0000000..b83574c
--- /dev/null
+++ b/gremlint/gremlint.com/src/views/StyleGuide/index.tsx
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import StyleGuideRule from '../../components/StyleGuideRule';
+import { rules } from './rules';
+
+const StyleGuide = () => (
+  <div>
+    {rules.map(({ title, explanation, example }) => (
+      <StyleGuideRule key={title} title={title} explanation={explanation} example={example} />
+    ))}
+  </div>
+);
+
+export default StyleGuide;
diff --git a/gremlint/gremlint.com/src/views/StyleGuide/rules.ts b/gremlint/gremlint.com/src/views/StyleGuide/rules.ts
new file mode 100644
index 0000000..a7220d6
--- /dev/null
+++ b/gremlint/gremlint.com/src/views/StyleGuide/rules.ts
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const rules = [
+  {
+    title: 'Break long queries into multiple lines',
+    explanation: `What is considered too long depends on your application.
+When breaking the query, not all parts of the traversal have to be broken up. First, divide the query into logical groups, based on which steps belong naturally together. For instance, every set of steps which end with an as()-step often belong together, as they together form a new essential step in the query.
+    
+If anoymous traversals are passed as arguments to another step, like a filter()-step, and it's causing the line to be too long, first split the line at the commas. Only if the traversal arguments are still too long, consider splitting them further.`,
+    example: `// Good (80 characters max width)
+g.V().hasLabel('person').where(outE("created").count().is(P.gte(2))).count()
+    
+// Good (50 characters max width)
+g.V().
+  hasLabel('person').
+  where(outE("created").count().is(P.gte(2))).
+  count()
+    
+// Good (30 characters max width)
+g.V().
+  hasLabel('person').
+  where(
+    outE("created").
+    count().
+    is(P.gte(2))).
+  count()`,
+  },
+  {
+    title: 'Use soft tabs (spaces) for indentation',
+    explanation: 'This ensures that your code looks the same for anyone, regardless of their text editor settings.',
+    example: `// Bad - indented using hard tabs
+g.V().
+  hasLabel('person').as('person').
+  properties('location').as('location').
+  select('person','location').
+    by('name').
+    by(valueMap())
+    
+// Good - indented using spaces
+g.V().
+∙∙hasLabel('person').as('person').
+∙∙properties('location').as('location').
+∙∙select('person','location').
+∙∙∙∙by('name').
+∙∙∙∙by(valueMap())`,
+  },
+  {
+    title: 'Use two spaces for indentation',
+    explanation:
+      'Two spaces makes the intent of the indent clear, but does not waste too much space. Of course, more spaces are allowed when indenting from an already indented block of code.',
+    example: `// Bad - Indented using four spaces
+g.V().
+    hasLabel('person').as('person').
+    properties('location').as('location').
+    select('person','location').
+        by('name').
+        by(valueMap())
+// Good - Indented using two spaces
+g.V().
+  hasLabel('person').as('person').
+  properties('location').as('location').
+  select('person','location').
+    by('name').
+    by(valueMap())`,
+  },
+  {
+    title: 'Use indents wisely',
+    explanation: `No newline should ever have the same indent as the line starting with the traversal source g.
+Use indents when the step in the new line is a modulator of a previous line.
+Use indents when the content in the new line is an argument of a previous step.
+If multiple anonymous traversals are passed as arguments to a function, each newline which is not the first step of the traversal should be indented to make it more clear where the distinction between each argument goes. If this is the case, but the newline would already be indented because the step in the content in the new line is the argument of a previous step, there is no need to double-indent.
+Don't be tempted to add extra indentation to vertically align a step with a step in a previous line.`,
+    example: `// Bad - No newline should have the same indent as the line starting with the traversal source g
+g.V().
+group().
+by().
+by(bothE().count())
+// Bad - Modulators of a step on a previous line should be indented
+g.V().
+  group().
+  by().
+  by(bothE().count())
+// Good
+g.V().
+  group().
+    by().
+    by(bothE().count())
+// Bad - You have ignored the indent rules to achieve the temporary satisfaction of vertical alignment
+g.V().local(union(identity(),
+                  bothE().count()).
+            fold())
+// Good
+g.V().
+  local(
+    union(
+      identity(),
+      bothE().count()).
+    fold())
+// Bad - When multiple anonymous traversals are passed as arguments to a function, each newline which is not the first of line of the step should be indented to make it more clear where the distinction between each argument goes.
+g.V().
+  has('person','name','marko').
+  fold().
+  coalesce(
+    unfold(),
+    addV('person').
+    property('name','marko').
+    property('age',29))
+// Good - We make it clear that the coalesce step takes two traversals as arguments
+g.V().
+  has('person','name','marko').
+  fold().
+  coalesce(
+    unfold(),
+    addV('person').
+      property('name','marko').
+      property('age',29))`,
+  },
+  {
+    title: 'Keep as()-steps at the end of each line',
+    explanation: `The end of the line is a natural place to assign a label to a step. It's okay if the as()-step is in the middle of the line if there are multiple consecutive label assignments, or if the line is so short that a newline doesn't make sense. Maybe a better way to put it is to not start a line with an as()-step, unless you're using it inside a match()-step of course.`,
+    example: `// Bad
+g.V().
+  as('a').
+  out('created').
+  as('b').
+  select('a','b')
+// Good
+g.V().as('a').
+  out('created').as('b').
+  select('a','b')
+// Good
+g.V().as('a').out('created').as('b').select('a','b')`,
+  },
+  {
+    title: 'Add linebreak after punctuation, not before',
+    explanation: `While adding the linebreak before the punctuation looks good in most cases, it introduces alignment problems when not all lines start with a punctuation. You never know if the next line should be indented relative to the punctuation of the previous line or the method of the previous line. Switching between having the punctuation at the start or the end of the line depending on whether it works in a particular case requires much brainpower (which we don't have), so it's  [...]
+    example: `// Bad - Looks okay, though
+g.V().has('name','marko')
+     .out('knows')
+     .has('age', gt(29))
+     .values('name')
+// Good
+g.V().
+  has('name','marko').
+  out('knows').
+  has('age', gt(29)).
+  values('name')
+// Bad - Punctuation at the start of the line makes the transition from filter to select to count too smooth
+g.V()
+  .hasLabel("person")
+  .group()
+    .by(values("name", "age").fold())
+  .unfold()
+  .filter(
+    select(values)
+    .count(local)
+    .is(gt(1)))
+// Good - Keeping punctuation at the end of each line, more clearly shows the query structure
+g.V().
+  hasLabel("person").
+  group().
+    by(values("name", "age").fold()).
+  unfold().
+  filter(
+    select(values).
+    count(local).
+    is(gt(1)))`,
+  },
+  {
+    title: 'Add linebreak and indentation for nested traversals which are long enough to span multiple lines',
+    explanation: '',
+    example: `// Bad - Not newlining the first argument of a function whose arguments span over multipe lines causes the arguments to not align.
+g.V().
+  hasLabel("person").
+  groupCount().
+    by(values("age").
+      choose(is(lt(28)),
+        constant("young"),
+        choose(is(lt(30)),
+          constant("old"),
+          constant("very old"))))
+// Bad - We talked about this in the indentation section, didn't we?
+g.V().
+  hasLabel("person").
+  groupCount().
+    by(values("age").
+       choose(is(lt(28)),
+              constant("young"),
+              choose(is(lt(30)),
+                     constant("old"),
+                     constant("very old"))))
+// Good
+g.V().
+  hasLabel("person").
+  groupCount().
+    by(
+      values("age").
+      choose(
+        is(lt(28)),
+        constant("young"),
+        choose(
+          is(lt(30)),
+          constant("old"),
+          constant("very old"))))`,
+  },
+  {
+    title: 'Place all trailing parentheses on a single line instead of distinct lines',
+    explanation:
+      'Aligning the end parenthesis with the step to which the start parenthesis belongs might make it easier to check that the number of parentheses is correct, but looks ugly and wastes a lot of space.',
+    example: `// Bad
+g.V().
+  hasLabel("person").
+  groupCount().
+    by(
+      values("age").
+      choose(
+        is(lt(28)),
+        constant("young"),
+        choose(
+          is(lt(30)),
+          constant("old"),
+          constant("very old")
+        )
+      )
+    )
+// Good
+g.V().
+  hasLabel("person").
+  groupCount().
+    by(
+      values("age").
+      choose(
+        is(lt(28)),
+        constant("young"),
+        choose(
+          is(lt(30)),
+          constant("old"),
+          constant("very old"))))`,
+  },
+  {
+    title: 'Use // for single line comments. Place single line comments on a newline above the subject of the comment.',
+    explanation: '',
+    example: `// Bad
+g.V().
+  has('name','alice').out('bought'). // Find everything that Alice has bought
+  in('bought').dedup().values('name') // Find everyone who have bought some of the same things as Alice
+// Good
+g.V().
+  // Find everything that Alice has bought
+  has('name','alice').out('bought').
+  // Find everyone who have bought some of the same things as Alice
+  in('bought').dedup().values('name')`,
+  },
+  {
+    title: 'Use single quotes for strings',
+    explanation:
+      'Use single quotes for literal string values. If the string contains double quotes or single quotes, surround the string with the type of quote which creates the fewest escaped characters.',
+    example: `// Bad - Use single quotes where possible
+g.V().has("Movie", "name", "It's a wonderful life")
+// Bad - Escaped single quotes are even worse than double quotes
+g.V().has('Movie', 'name', 'It\\'s a wonderful life')
+// Good
+g.V().has('Movie', 'name', "It's a wonderful life")`,
+  },
+  {
+    title: 'Write idiomatic Gremlin code',
+    explanation: `If there is a simpler way, do it the simpler way. Use the Gremlin methods for what they're worth.`,
+    example: `// Bad
+g.V().outE().inV()
+// Good
+g.V().out()
+// Bad
+g.V().
+  has('name', 'alice').
+  outE().hasLabel('bought').inV().
+  values('name')
+// Good
+g.V().
+  has('name','alice').
+  out('bought').
+  values('name')
+// Bad
+g.V().hasLabel('person').has('name', 'alice')
+// Good
+g.V().has('person', 'name', 'alice')`,
+  },
+];
diff --git a/gremlint/gremlint.com/tsconfig.json b/gremlint/gremlint.com/tsconfig.json
new file mode 100644
index 0000000..a273b0c
--- /dev/null
+++ b/gremlint/gremlint.com/tsconfig.json
@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx"
+  },
+  "include": [
+    "src"
+  ]
+}
diff --git a/gremlint/gremlint.com/tslint.json b/gremlint/gremlint.com/tslint.json
new file mode 100644
index 0000000..267f369
--- /dev/null
+++ b/gremlint/gremlint.com/tslint.json
@@ -0,0 +1,3 @@
+{
+  "extends": ["tslint:recommended", "tslint-config-prettier"]
+}
diff --git a/gremlint/gremlint/.gitignore b/gremlint/gremlint/.gitignore
new file mode 100644
index 0000000..69a671c
--- /dev/null
+++ b/gremlint/gremlint/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+/lib
\ No newline at end of file
diff --git a/gremlint/gremlint/.prettierrc b/gremlint/gremlint/.prettierrc
new file mode 100644
index 0000000..a0d1c9a
--- /dev/null
+++ b/gremlint/gremlint/.prettierrc
@@ -0,0 +1,5 @@
+{
+  "printWidth": 120,
+  "trailingComma": "all",
+  "singleQuote": true
+}
diff --git a/gremlint/gremlint/LICENSE b/gremlint/gremlint/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/gremlint/gremlint/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/gremlint/gremlint/README.md b/gremlint/gremlint/README.md
new file mode 100644
index 0000000..0831d19
--- /dev/null
+++ b/gremlint/gremlint/README.md
@@ -0,0 +1,122 @@
+![Gremlint Github Header 1920x1024](https://user-images.githubusercontent.com/25663729/88488788-d5a73700-cf8f-11ea-9adb-03d62c77c1b7.png)
+
+### What is Gremlint?
+
+Gremlint is a code formatter which parses Gremlin queries and rewrites them to adhere to certain styling rules. It does so by parsing the query to an abstract syntax tree, and reprinting it from scratch.
+
+### But why?
+
+- To make Gremlin queries more readable
+- To make your queries more beautiful
+- To act as a "living" style guide
+
+### Install Gremlint as a JavaScript / TypeScript package
+
+Since Gremlint is not yet "published", it has to be installed from its GitHub repo:
+
+```bash
+npm install OyvindSabo/gremlint#master
+```
+
+### Basic example
+
+```typescript
+import { formatQuery } from 'gremlint';
+
+const unformattedQuery = `g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')`;
+
+const formattedQuery = formatQuery(unformattedQuery);
+
+console.log(formattedQuery);
+```
+
+```
+g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh')).
+    with(ShortestPath.distance, 'weight')
+```
+
+### Override default max line length
+
+The default max line length is 80, but it can easily be overridden.
+
+```typescript
+import { formatQuery } from 'gremlint';
+
+const unformattedQuery = `g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')`;
+
+const formattedQuery = formatQuery(unformattedQuery, { maxLineLength: 50 });
+
+console.log(formattedQuery);
+```
+
+```
+g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(
+      ShortestPath.target,
+      __.has('name', 'josh')).
+    with(ShortestPath.distance, 'weight')
+```
+
+### Other formatting options
+
+```typescript
+import { formatQuery } from 'gremlint';
+
+const unformattedQuery = `g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')`;
+
+const formattedQuery = formatQuery(unformattedQuery, {
+  indentation: 4, // default: 0
+  maxLineLength: 40, // default: 80
+  shouldPlaceDotsAfterLineBreaks: true, // default: false
+});
+
+console.log(formattedQuery);
+```
+
+```
+    g.V()
+      .has('person', 'name', 'marko')
+      .shortestPath()
+        .with(
+          ShortestPath.target,
+          __.has('name', 'josh'))
+        .with(
+          ShortestPath.distance,
+          'weight')
+```
+
+### Just looking for an online Gremlin query formatter?
+
+https://gremlint.com is a website which utilizes the Gremlint library to give users an online "living" style guide for Gremlin queries. It also serves as a platform for showcasing the features of Gremlint. Its source code is available [here](https://github.com/OyvindSabo/gremlint.com).
+![Gremlint V2 Screenshot](https://user-images.githubusercontent.com/25663729/88488518-f078ac00-cf8d-11ea-9e1c-01edec285751.png)
+
+### For contributors
+
+**Install dependencies**
+
+`npm install`
+
+**Lint source files**
+
+`npm run lint`
+
+**Format source files**
+
+`npm run format`
+
+**Run tests**
+
+`npm test`
+
+**Compile the TypeScript source code**
+
+`npm run build`
+
+**Bump version**
+
+`npm version [major | minor | patch]`
diff --git a/gremlint/gremlint/jestconfig.json b/gremlint/gremlint/jestconfig.json
new file mode 100644
index 0000000..20c25c0
--- /dev/null
+++ b/gremlint/gremlint/jestconfig.json
@@ -0,0 +1,7 @@
+{
+  "transform": {
+    "^.+\\.(t|j)sx?$": "ts-jest"
+  },
+  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
+  "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
+}
diff --git a/gremlint/gremlint/package.json b/gremlint/gremlint/package.json
new file mode 100644
index 0000000..b8f5280
--- /dev/null
+++ b/gremlint/gremlint/package.json
@@ -0,0 +1,48 @@
+{
+  "name": "gremlint",
+  "version": "1.0.0",
+  "description": "Linter/code formatter for Gremlin",
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "scripts": {
+    "build": "tsc",
+    "format": "prettier --write \"src/**/*.ts\"",
+    "lint": "tslint -p tsconfig.json",
+    "test": "jest --config jestconfig.json",
+    "prepare": "npm run build",
+    "prepublishOnly": "npm test && npm run lint",
+    "preversion": "npm run lint",
+    "version": "npm run format && git add -A src",
+    "postversion": "git push && git push --tags"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/OyvindSabo/gremlint.git"
+  },
+  "keywords": [
+    "code",
+    "formatter",
+    "graph",
+    "gremlin",
+    "query"
+  ],
+  "author": "Øyvind Sæbø",
+  "license": "Apache-2.0",
+  "bugs": {
+    "url": "https://github.com/OyvindSabo/gremlint/issues"
+  },
+  "homepage": "https://github.com/OyvindSabo/gremlint#readme",
+  "devDependencies": {
+    "@types/jest": "^26.0.15",
+    "jest": "^26.6.1",
+    "prettier": "^2.1.2",
+    "ts-jest": "^26.4.2",
+    "tslint": "^6.1.3",
+    "tslint-config-prettier": "^1.18.0",
+    "typescript": "^4.0.3"
+  },
+  "files": [
+    "lib/**/*"
+  ],
+  "dependencies": {}
+}
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts
new file mode 100644
index 0000000..f0aaadf
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts
@@ -0,0 +1,349 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('When specifying indentation for a query, the relative indentation within closures should be preserved', () => {
+  // Test that relative indentation is preserved between all the lines within a closure when indentation is 0
+  expect(
+    formatQuery(
+      `g.V().
+has('sell_price').
+has('buy_price').
+project('product', 'profit').
+by('name').
+by{ it.get().value('sell_price') -
+    it.get().value('buy_price') };`,
+      {
+        indentation: 0,
+        maxLineLength: 70,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  has('sell_price').
+  has('buy_price').
+  project('product', 'profit').
+    by('name').
+    by{ it.get().value('sell_price') -
+        it.get().value('buy_price') };`);
+
+  // Test that relative indentation is preserved between all the lines within a closure when indentation is 20
+  expect(
+    formatQuery(
+      `g.V().
+has('sell_price').
+has('buy_price').
+project('product', 'profit').
+by('name').
+by{ it.get().value('sell_price') -
+    it.get().value('buy_price') };`,
+      {
+        indentation: 20,
+        maxLineLength: 70,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`                    g.V().
+                      has('sell_price').
+                      has('buy_price').
+                      project('product', 'profit').
+                        by('name').
+                        by{ it.get().value('sell_price') -
+                            it.get().value('buy_price') };`);
+
+  // Test that relative indentation is preserved in closures which are nested
+  expect(
+    formatQuery(
+      `g.V().filter(out('Sells').
+             map{ it.get('sell_price') -
+                  it.get('buy_price') }.
+             where(gt(50)))`,
+      { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(
+    `g.V().
+  filter(
+    out('Sells').
+    map{ it.get('sell_price') -
+         it.get('buy_price') }.
+    where(gt(50)))`,
+  );
+
+  expect(
+    formatQuery(
+      `g.V().filter(map{ one   = 1
+                  two   = 2
+                  three = 3 }))`,
+      { indentation: 0, maxLineLength: 35, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().filter(map{ one   = 1
+                  two   = 2
+                  three = 3 }))`);
+  expect(
+    formatQuery(
+      `g.V().filter(map{ one   = 1
+                  two   = 2
+                  three = 3 }))`,
+      { indentation: 0, maxLineLength: 28, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  filter(map{ one   = 1
+              two   = 2
+              three = 3 }))`);
+  expect(
+    formatQuery(
+      `g.V().filter(map{ one   = 1
+                  two   = 2
+                  three = 3 }))`,
+      { indentation: 0, maxLineLength: 22, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  filter(
+    map{ one   = 1
+         two   = 2
+         three = 3 }))`);
+
+  expect(
+    formatQuery(
+      `g.V().where(map{ buyPrice  = it.get().value('buy_price');
+                 sellPrice = it.get().value('sell_price');
+                 sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 60, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().where(map{ buyPrice  = it.get().value('buy_price');
+                 sellPrice = it.get().value('sell_price');
+                 sellPrice - buyPrice; }.is(gt(50)))`);
+  expect(
+    formatQuery(
+      `g.V().where(map{ buyPrice  = it.get().value('buy_price');
+                 sellPrice = it.get().value('sell_price');
+                 sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 50, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  where(map{ buyPrice  = it.get().value('buy_price');
+             sellPrice = it.get().value('sell_price');
+             sellPrice - buyPrice; }.is(gt(50)))`);
+  expect(
+    formatQuery(
+      `g.V().where(map{ buyPrice  = it.get().value('buy_price');
+                 sellPrice = it.get().value('sell_price');
+                 sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  where(
+    map{ buyPrice  = it.get().value('buy_price');
+         sellPrice = it.get().value('sell_price');
+         sellPrice - buyPrice; }.is(gt(50)))`);
+
+  expect(
+    formatQuery(
+      `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 60, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; }.is(gt(50)))`);
+  expect(
+    formatQuery(
+      `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 55, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  where(out().map{ buyPrice  = it.get().value('buy_price');
+                   sellPrice = it.get().value('sell_price');
+                   sellPrice - buyPrice; }.is(gt(50)))`);
+  expect(
+    formatQuery(
+      `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 50, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  where(
+    out().map{ buyPrice  = it.get().value('buy_price');
+               sellPrice = it.get().value('sell_price');
+               sellPrice - buyPrice; }.is(gt(50)))`);
+  expect(
+    formatQuery(
+      `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V().
+  where(
+    out().
+    map{ buyPrice  = it.get().value('buy_price');
+         sellPrice = it.get().value('sell_price');
+         sellPrice - buyPrice; }.
+    is(gt(50)))`);
+
+  // Test that relative indentation is preserved between all the lines within a closure when not all tokens in a stepGroup are methods (for instance, g in g.V() adds to the width of the stepGroup even if it is not a method)
+  expect(
+    formatQuery(
+      `g.V().map({ it.get('sell_price') -
+            it.get('buy_price') }))`,
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().map({ it.get('sell_price') -
+            it.get('buy_price') }))`);
+
+  // Test that relative indentation is preserved between all the lines within a closure when the first line is indented because the query doesn't start at the beginning of the line
+  expect(
+    formatQuery(
+      `profit = g.V().map({ it.get('sell_price') -
+                     it.get('buy_price') }))`,
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`profit = g.V().map({ it.get('sell_price') -
+                     it.get('buy_price') }))`);
+
+  // Test that relative indentation is preserved between all lines within a closure when the method to which the closure is an argument is wrapped
+  expect(
+    formatQuery(
+      `g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map{ it.get().value('factor_a') *
+            it.get().value('factor_b') })`,
+      { indentation: 0, maxLineLength: 40, shouldPlaceDotsAfterLineBreaks: false },
+    ),
+  ).toBe(`g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project(
+    'Factor A',
+    'Factor B',
+    'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(
+      map{ it.get().value('factor_a') *
+           it.get().value('factor_b') })`);
+
+  // Test that relative indentation is preserved between all lines within a closure when dots are placed after line breaks
+  // When the whole query is long enough to wrap
+  expect(
+    formatQuery(
+      `g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map{ it.get().value('factor_a') *
+            it.get().value('factor_b') })`,
+      { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: true },
+    ),
+  ).toBe(`g.V(ids)
+  .has('factor_a')
+  .has('factor_b')
+  .project('Factor A', 'Factor B', 'Product')
+    .by(values('factor_a'))
+    .by(values('factor_b'))
+    .by(map{ it.get().value('factor_a') *
+             it.get().value('factor_b') })`);
+
+  // When the query is long enough to wrap, but the traversal containing the closure is not the first step in its step group and not long enough to wrap
+  expect(
+    formatQuery(
+      `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; })`,
+      { indentation: 0, maxLineLength: 50, shouldPlaceDotsAfterLineBreaks: true },
+    ),
+  ).toBe(`g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; })`);
+
+  // When the query is long enough to wrap, but the traversal containing the closure is the first step in its step group and not long enough to wrap
+  expect(
+    formatQuery(
+      `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
+                       sellPrice = it.get().value('sell_price');
+                       sellPrice - buyPrice; }.is(gt(50)))`,
+      { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: true },
+    ),
+  ).toBe(`g.V()
+  .where(
+    out()
+    .map{ buyPrice  = it.get().value('buy_price');
+          sellPrice = it.get().value('sell_price');
+          sellPrice - buyPrice; }
+    .is(gt(50)))`);
+
+  // When the query is long enough to wrap, but the traversal containing the closure is the first step in its traversal and not long enough to wrap
+  expect(
+    formatQuery(
+      `g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map{ it.get().value('factor_a') *
+            it.get().value('factor_b') })`,
+      { indentation: 0, maxLineLength: 40, shouldPlaceDotsAfterLineBreaks: true },
+    ),
+  ).toBe(`g.V(ids)
+  .has('factor_a')
+  .has('factor_b')
+  .project(
+    'Factor A',
+    'Factor B',
+    'Product')
+    .by(values('factor_a'))
+    .by(values('factor_b'))
+    .by(
+      map{ it.get().value('factor_a') *
+           it.get().value('factor_b') })`);
+
+  // When the whole query is short enough to not wrap
+  expect(
+    formatQuery(
+      `g.V().map({ it.get('sell_price') -
+            it.get('buy_price') }))`,
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: true,
+      },
+    ),
+  ).toBe(`g.V().map({ it.get('sell_price') -
+            it.get('buy_price') }))`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/curlyBracketMultilineWrapping.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/curlyBracketMultilineWrapping.test.ts
new file mode 100644
index 0000000..d131748
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/curlyBracketMultilineWrapping.test.ts
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('When determining if multiline curly bracket closures should cause wrapping, look only at the longest line of the code block', () => {
+  // Test that when moving multiline code blocks, we move all the lines of the code block, not just the first
+  expect(
+    formatQuery(
+      `g.V(1).out().map{ it.get()
+                    .value('name') }`,
+      {
+        indentation: 0,
+        maxLineLength: 25,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V(1).
+  out().
+  map{ it.get()
+         .value('name') }`);
+
+  expect(
+    formatQuery(
+      `g.V().filter{ it.get()
+                .label() == 'person' }`,
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  filter{ it.get()
+            .label() == 'person' }`);
+
+  expect(
+    formatQuery(
+      `g.V().
+branch{ it.get()
+          .value('name') }.
+option('marko', values('age')).
+option(none, values('name'))`,
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  branch{ it.get()
+            .value('name') }.
+    option('marko', values('age')).
+    option(none, values('name'))`);
+
+  expect(
+    formatQuery(
+      `g.V(4).
+out().
+values('name').
+inject('daniel').
+map{ it.get()
+       .length() }.
+path()`,
+      {
+        indentation: 0,
+        maxLineLength: 25,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V(4).
+  out().
+  values('name').
+  inject('daniel').
+  map{ it.get()
+         .length() }.
+  path()`);
+
+  expect(
+    formatQuery(
+      `g.V().
+filter{ it.get()
+          .value('name') == 'marko' }.
+flatMap{ it.get()
+           .vertices(OUT,'created') }.
+map{ it.get()
+       .value('name') }`,
+      {
+        indentation: 0,
+        maxLineLength: 40,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  filter{ it.get()
+            .value('name') == 'marko' }.
+  flatMap{ it.get()
+             .vertices(OUT,'created') }.
+  map{ it.get()
+         .value('name') }`);
+
+  expect(
+    formatQuery(
+      `g.V().
+out().
+out().
+path().
+by{ it.value('name') }.
+by{ it.value('name') }.
+by{ g.V(it).
+      in('created').
+      values('name').
+      fold().next() }`,
+      {
+        indentation: 0,
+        maxLineLength: 30,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  out().
+  out().
+  path().
+    by{ it.value('name') }.
+    by{ it.value('name') }.
+    by{ g.V(it).
+          in('created').
+          values('name').
+          fold().next() }`);
+
+  expect(
+    formatQuery(
+      `g.V(ids).
+has('factor_a').
+has('factor_b').
+project('Factor A', 'Factor B', 'Product').
+by(values('factor_a')).
+by(values('factor_b')).
+by(map{ it.get().value('factor_a') * 
+        it.get().value('factor_b') })`,
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map{ it.get().value('factor_a') * 
+            it.get().value('factor_b') })`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/curlyBracketWrapping.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/curlyBracketWrapping.test.ts
new file mode 100644
index 0000000..49f7f6b
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/curlyBracketWrapping.test.ts
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('It should be possible to call curly bracket closures which are not wrapped in parentheses', () => {
+  // Test calling closure whose curly brackets are wrapped in parentheses
+  expect(
+    formatQuery(
+      "g.V().branch({ it.get().value('name') }).option('marko', values('age')).option(none, values('name'))",
+      {
+        indentation: 0,
+        maxLineLength: 40,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  branch({ it.get().value('name') }).
+    option('marko', values('age')).
+    option(none, values('name'))`);
+
+  // Test calling closure which is not the last step
+  expect(
+    formatQuery("g.V().branch{ it.get().value('name') }.option('marko', values('age')).option(none, values('name'))", {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  branch{ it.get().value('name') }.
+    option('marko', values('age')).
+    option(none, values('name'))`);
+  expect(
+    formatQuery("g.V().hasLabel('person').outE('created').count().map{ it.get() * 10 }.path()", {
+      indentation: 0,
+      maxLineLength: 25,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  hasLabel('person').
+  outE('created').
+  count().
+  map{ it.get() * 10 }.
+  path()`);
+
+  // Test calling closure which is the last step
+  expect(
+    formatQuery("g.V(4).out().values('name').inject('daniel').map{ it.get().length() }", {
+      indentation: 0,
+      maxLineLength: 30,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V(4).
+  out().
+  values('name').
+  inject('daniel').
+  map{ it.get().length() }`);
+
+  // Test calling closure whose subsequent step should not wrap
+  expect(
+    formatQuery("g.V(4).out().values('name').inject('daniel').map{ it.get().length() }.path()", {
+      indentation: 0,
+      maxLineLength: 80,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V(4).out().values('name').inject('daniel').map{ it.get().length() }.path()`);
+
+  // Test calling consecutive closures
+  expect(
+    formatQuery(
+      "g.V().filter{ it.get().value('name') == 'marko' }.flatMap{ it.get().vertices(OUT, 'created') }.map{ it.get().value('name') }",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  filter{ it.get().value('name') == 'marko' }.
+  flatMap{ it.get().vertices(OUT, 'created') }.
+  map{ it.get().value('name') }`);
+
+  // Test calling closure by()-modulator
+  expect(
+    formatQuery("g.V().group().by{ it.value('name')[1] }.by('name').next()", {
+      indentation: 0,
+      maxLineLength: 30,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  group().
+    by{ it.value('name')[1] }.
+    by('name').
+  next()`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts
new file mode 100644
index 0000000..c6ace66
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('It should be possible ot use formatQuery with a config, with a partial config and no config', () => {
+  // Test using formatQuery with a default config
+  expect(
+    formatQuery(
+      "g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')",
+      {
+        indentation: 0,
+        maxLineLength: 80,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh')).
+    with(ShortestPath.distance, 'weight')`);
+
+  // Test using formatQuery with a non-default confi
+  expect(
+    formatQuery(
+      "g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')",
+      {
+        indentation: 8,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: true,
+      },
+    ),
+  ).toBe(`        g.V()
+          .has('person', 'name', 'marko')
+          .shortestPath()
+            .with(
+              ShortestPath.target,
+              __.has('name', 'josh'))
+            .with(ShortestPath.distance, 'weight')`);
+
+  // Test using formatQuery with an empty config
+  expect(
+    formatQuery(
+      "g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')",
+      {},
+    ),
+  ).toBe(`g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh')).
+    with(ShortestPath.distance, 'weight')`);
+
+  // Test using formatQuery without a config
+  expect(
+    formatQuery(
+      "g.V().has('person', 'name', 'marko').shortestPath().with(ShortestPath.target, __.has('name', 'josh')).with(ShortestPath.distance, 'weight')",
+    ),
+  ).toBe(`g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh')).
+    with(ShortestPath.distance, 'weight')`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts
new file mode 100644
index 0000000..3f1a2e6
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('Extract the parts of the code that can be parsed as Gremlin, format those separately, and leave the rest of the code alone', () => {
+  expect(
+    formatQuery(
+      `contains = {
+  value -> it.get().contains(value)
+}
+
+g.V().filter(values('name').filter(contains('Gremlint')))`,
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`contains = {
+  value -> it.get().contains(value)
+}
+
+g.V().
+  filter(
+    values('name').
+    filter(contains('Gremlint')))`);
+
+  expect(
+    formatQuery(
+      `      g.V(ids).
+     has('factor_a').
+    has('factor_b').
+   project('Factor A', 'Factor B', 'Product').
+  by(values('factor_a')).
+ by(values('factor_b')).
+by(map({ it.get().value('factor_a') *
+         it.get().value('factor_b') }))`,
+      {
+        indentation: 0,
+        maxLineLength: 72,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`      g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map({ it.get().value('factor_a') *
+             it.get().value('factor_b') }))`);
+
+  expect(
+    formatQuery(
+      `a = 4.5;
+b = 4.5;
+
+g.V(ids).
+has('factor_a').
+has('factor_b').
+project('Factor A', 'Factor B', 'Product').
+by(values('factor_a')).
+by(values('factor_b')).
+by(map{ it.get().value('factor_a') *
+        it.get().value('factor_b') })`,
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`a = 4.5;
+b = 4.5;
+
+g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map{ it.get().value('factor_a') *
+            it.get().value('factor_b') })`);
+
+  expect(
+    formatQuery(
+      `g.V(ids).
+has('factor_a').
+has('factor_b').
+project('Factor A', 'Factor B', 'Product').
+by(values('factor_a')).
+by(values('factor_b')).
+by(map{ it.get().value('factor_a') *
+        it.get().value('factor_b') });`,
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(map{ it.get().value('factor_a') *
+            it.get().value('factor_b') });`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts
new file mode 100644
index 0000000..831e1b8
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('If dots are configured to be placed after line breaks, make sure they are correctly placed, and neither missing nor duplicated', () => {
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').group().by(values('name', 'age').fold()).unfold().filter(select(values).count(local).is(gt(1)))",
+      {
+        indentation: 0,
+        maxLineLength: 40,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').
+  group().
+    by(values('name', 'age').fold()).
+  unfold().
+  filter(
+    select(values).
+    count(local).
+    is(gt(1)))`);
+
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').group().by(values('name', 'age').fold()).unfold().filter(select(values).count(local).is(gt(1)))",
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: true,
+      },
+    ),
+  ).toBe(`g.V()
+  .hasLabel('person')
+  .group()
+    .by(
+      values('name', 'age').fold())
+  .unfold()
+  .filter(
+    select(values)
+    .count(local)
+    .is(gt(1)))`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts
new file mode 100644
index 0000000..e743a11
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('The formatter should not crash when indentation is equal to maxLineLength', () => {
+  expect(
+    formatQuery(`g.V()`, {
+      indentation: 0,
+      maxLineLength: 0,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V(
+)`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/maxLineLength.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/maxLineLength.test.ts
new file mode 100644
index 0000000..4206067
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/maxLineLength.test.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('No line in the query should exceed the maximum line length', () => {
+  // When the maximum line length is equal to the length of the query, no line wrapping should occur
+  expect(
+    formatQuery("g.V().hasLabel('person').where(outE('created').count().is(P.gte(2))).count()", {
+      indentation: 0,
+      maxLineLength: 76,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe("g.V().hasLabel('person').where(outE('created').count().is(P.gte(2))).count()");
+
+  // A query of length 77 should be wrapped when the maximum line length is set to 76
+  expect(
+    formatQuery("g.V().hasLabel('person').where(outE('created').count().is(P.gte(2))).count()", {
+      indentation: 0,
+      maxLineLength: 75,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  hasLabel('person').
+  where(outE('created').count().is(P.gte(2))).
+  count()`);
+
+  // When wrapping occurs, the parentheses, punctuations or commas after the wrapped tokens should be included when
+  // considering whether to further wrap the query. This doesn't currently work, as the following test shows
+  // https://github.com/OyvindSabo/gremlint/issues/44
+  /*expect(
+    formatQuery("g.V().hasLabel('person').where(outE('created').count().is(P.gte(2))).count()", {
+      indentation: 0,
+      maxLineLength: 45,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  hasLabel('person').
+  where(
+    outE('created').count().is(P.gte(2))).
+  count()`);*/
+
+  // Test that if the query is wrapped before exceeding the max line length, even if it does not start at the beginning
+  // of the line
+  expect(
+    formatQuery("List<Vertex> people = g.V().hasLabel('person').toList();", {
+      indentation: 0,
+      maxLineLength: 40,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`List<Vertex> people = g.V().
+  hasLabel('person').
+  toList();`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts
new file mode 100644
index 0000000..4ebb342
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts
@@ -0,0 +1,841 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+// If modulators have to be wrapped, they should be indented with two additional spaces, but consecutive steps should
+// not be indented with two additional spaces. Check that as-steps are indented as modulators.
+test('Wrapped modulators should be indented with two spaces', () => {
+  // Test as()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().has('name', within('marko', 'vadas', 'josh')).as('person').V().has('name', within('lop', 'ripple')).addE('uses').from('person')",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  has('name', within('marko', 'vadas', 'josh')).
+    as('person').
+  V().
+  has('name', within('lop', 'ripple')).
+  addE('uses').from('person')`);
+
+  // Test as_()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().has('name', within('marko', 'vadas', 'josh')).as_('person').V().has('name', within('lop', 'ripple')).addE('uses').from('person')",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  has('name', within('marko', 'vadas', 'josh')).
+    as_('person').
+  V().
+  has('name', within('lop', 'ripple')).
+  addE('uses').from('person')`);
+
+  // Test by()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').group().by(values('name', 'age').fold()).unfold().filter(select(values).count(local).is(gt(1)))",
+      {
+        indentation: 0,
+        maxLineLength: 40,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').
+  group().
+    by(values('name', 'age').fold()).
+  unfold().
+  filter(
+    select(values).
+    count(local).
+    is(gt(1)))`);
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').groupCount().by(values('age').choose(is(lt(28)),constant('young'),choose(is(lt(30)), constant('old'), constant('very old'))))",
+      {
+        indentation: 0,
+        maxLineLength: 80,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').
+  groupCount().
+    by(
+      values('age').
+      choose(
+        is(lt(28)),
+        constant('young'),
+        choose(is(lt(30)), constant('old'), constant('very old'))))`);
+
+  // Test emit()-modulator indentation
+  expect(
+    formatQuery("g.V(1).repeat(bothE('created').dedup().otherV()).emit().path()", {
+      indentation: 0,
+      maxLineLength: 45,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V(1).
+  repeat(bothE('created').dedup().otherV()).
+    emit().
+  path()`,
+  );
+  expect(
+    formatQuery('g.V().repeat(both()).times(1000000).emit().range(6,10)', {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  repeat(both()).
+    times(1000000).
+    emit().
+  range(6, 10)`,
+  );
+  expect(
+    formatQuery("g.V(1).repeat(out()).times(2).emit().path().by('name')", {
+      indentation: 0,
+      maxLineLength: 30,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V(1).
+  repeat(out()).
+    times(2).
+    emit().
+  path().by('name')`,
+  );
+  expect(
+    formatQuery("g.withSack(1).V(1).repeat(sack(sum).by(constant(1))).times(10).emit().sack().math('sin _')", {
+      indentation: 0,
+      maxLineLength: 40,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.withSack(1).
+  V(1).
+  repeat(sack(sum).by(constant(1))).
+    times(10).
+    emit().
+  sack().
+  math('sin _')`,
+  );
+
+  // Test from()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().has('person','name','vadas').as('e').in('knows').as('m').out('knows').where(neq('e')).path().from('m').by('name')",
+      {
+        indentation: 0,
+        maxLineLength: 20,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  has(
+    'person',
+    'name',
+    'vadas').
+    as('e').
+  in('knows').
+    as('m').
+  out('knows').
+  where(neq('e')).
+  path().
+    from('m').
+    by('name')`,
+  );
+
+  // Test from()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().has('person','name','vadas').as_('e').in('knows').as_('m').out('knows').where(neq('e')).path().from_('m').by('name')",
+      {
+        indentation: 0,
+        maxLineLength: 20,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  has(
+    'person',
+    'name',
+    'vadas').
+    as_('e').
+  in('knows').
+    as_('m').
+  out('knows').
+  where(neq('e')).
+  path().
+    from_('m').
+    by('name')`,
+  );
+
+  // Test option()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').choose(values('name')).option('marko', values('age')).option('josh', values('name')).option('vadas', elementMap()).option('peter', label())",
+      {
+        indentation: 0,
+        maxLineLength: 80,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  choose(values('name')).
+    option('marko', values('age')).
+    option('josh', values('name')).
+    option('vadas', elementMap()).
+    option('peter', label())`,
+  );
+
+  // Test read()-modulator indentation
+  expect(
+    formatQuery('g.io(someInputFile).read().iterate()', {
+      indentation: 0,
+      maxLineLength: 20,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.io(someInputFile).
+    read().
+  iterate()`,
+  );
+
+  // Test times()-modulator indentation
+  expect(
+    formatQuery("g.V().repeat(both()).times(3).values('age').max()", {
+      indentation: 0,
+      maxLineLength: 20,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  repeat(both()).
+    times(3).
+  values('age').
+  max()`,
+  );
+
+  // Test to()-modulator indentation
+  expect(
+    formatQuery("g.V(v1).addE('knows').to(v2).property('weight',0.75).iterate()", {
+      indentation: 0,
+      maxLineLength: 20,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V(v1).
+  addE('knows').
+    to(v2).
+  property(
+    'weight',
+    0.75).
+  iterate()`,
+  );
+
+  // Test until()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V(6).repeat('a', both('created').simplePath()).emit(repeat('b', both('knows')).until(loops('b').as('b').where(loops('a').as('b'))).hasId(2)).dedup()",
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V(6).
+  repeat('a', both('created').simplePath()).
+    emit(
+      repeat('b', both('knows')).
+        until(
+          loops('b').as('b').
+          where(loops('a').as('b'))).
+      hasId(2)).
+  dedup()`,
+  );
+
+  // Test with()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().connectedComponent().with(ConnectedComponent.propertyName, 'component').project('name','component').by('name').by('component')",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  connectedComponent().
+    with(ConnectedComponent.propertyName, 'component').
+  project('name', 'component').
+    by('name').
+    by('component')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').connectedComponent().with(ConnectedComponent.propertyName, 'component').with(ConnectedComponent.edges, outE('knows')).project('name','component').by('name').by('component')",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  connectedComponent().
+    with(ConnectedComponent.propertyName, 'component').
+    with(ConnectedComponent.edges, outE('knows')).
+  project('name', 'component').
+    by('name').
+    by('component')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('software').values('name').fold().order(Scope.local).index().with(WithOptions.indexer, WithOptions.list).unfold().order().by(__.tail(Scope.local, 1))",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('software').
+  values('name').
+  fold().
+  order(Scope.local).
+  index().
+    with(WithOptions.indexer, WithOptions.list).
+  unfold().
+  order().by(__.tail(Scope.local, 1))`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').values('name').fold().order(Scope.local).index().with(WithOptions.indexer, WithOptions.map)",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  values('name').
+  fold().
+  order(Scope.local).
+  index().
+    with(WithOptions.indexer, WithOptions.map)`,
+  );
+  expect(
+    formatQuery('g.io(someInputFile).with(IO.reader, IO.graphson).read().iterate()', {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.io(someInputFile).
+    with(IO.reader, IO.graphson).
+    read().
+  iterate()`,
+  );
+  expect(
+    formatQuery('g.io(someOutputFile).with(IO.writer,IO.graphml).write().iterate()', {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.io(someOutputFile).
+    with(IO.writer, IO.graphml).
+    write().
+  iterate()`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').pageRank().with(PageRank.edges, __.outE('knows')).with(PageRank.propertyName, 'friendRank').order().by('friendRank',desc).elementMap('name','friendRank')",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  pageRank().
+    with(PageRank.edges, __.outE('knows')).
+    with(PageRank.propertyName, 'friendRank').
+  order().by('friendRank', desc).
+  elementMap('name', 'friendRank')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').peerPressure().with(PeerPressure.propertyName, 'cluster').group().by('cluster').by('name')",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  peerPressure().
+    with(PeerPressure.propertyName, 'cluster').
+  group().by('cluster').by('name')`,
+  );
+  expect(
+    formatQuery("g.V().shortestPath().with(ShortestPath.target, __.has('name','peter'))", {
+      indentation: 0,
+      maxLineLength: 55,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'peter'))`,
+  );
+  expect(
+    formatQuery(
+      "g.V().shortestPath().with(ShortestPath.edges, Direction.IN).with(ShortestPath.target, __.has('name','josh'))",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  shortestPath().
+    with(ShortestPath.edges, Direction.IN).
+    with(ShortestPath.target, __.has('name', 'josh'))`,
+  );
+  expect(
+    formatQuery("g.V().has('person','name','marko').shortestPath().with(ShortestPath.target,__.has('name','josh'))", {
+      indentation: 0,
+      maxLineLength: 55,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh'))`,
+  );
+  expect(
+    formatQuery(
+      "g.V().has('person','name','marko').shortestPath().with(ShortestPath.target, __.has('name','josh')).with(ShortestPath.distance, 'weight')",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh')).
+    with(ShortestPath.distance, 'weight')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().has('person','name','marko').shortestPath().with(ShortestPath.target, __.has('name','josh')).with(ShortestPath.includeEdges, true)",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with(ShortestPath.target, __.has('name', 'josh')).
+    with(ShortestPath.includeEdges, true)`,
+  );
+  expect(
+    formatQuery(
+      "g.inject(g.withComputer().V().shortestPath().with(ShortestPath.distance, 'weight').with(ShortestPath.includeEdges, true).with(ShortestPath.maxDistance, 1).toList().toArray()).map(unfold().values('name','weight').fold())",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.inject(
+  g.withComputer().
+    V().
+    shortestPath().
+      with(ShortestPath.distance, 'weight').
+      with(ShortestPath.includeEdges, true).
+      with(ShortestPath.maxDistance, 1).
+    toList().
+    toArray()).
+  map(unfold().values('name', 'weight').fold())`,
+  );
+  expect(
+    formatQuery("g.V().hasLabel('person').valueMap().with(WithOptions.tokens)", {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  valueMap().
+    with(WithOptions.tokens)`,
+  );
+  expect(
+    formatQuery("g.V().hasLabel('person').valueMap('name').with(WithOptions.tokens,WithOptions.labels)", {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  valueMap('name').
+    with(
+      WithOptions.tokens,
+      WithOptions.labels)`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').properties('location').valueMap().with(WithOptions.tokens, WithOptions.values)",
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  properties('location').
+  valueMap().
+    with(
+      WithOptions.tokens,
+      WithOptions.values)`,
+  );
+
+  // Test with_()-modulator indentation
+  expect(
+    formatQuery(
+      "g.V().connectedComponent().with_(ConnectedComponent.propertyName, 'component').project('name','component').by('name').by('component')",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  connectedComponent().
+    with_(ConnectedComponent.propertyName, 'component').
+  project('name', 'component').
+    by('name').
+    by('component')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').connectedComponent().with_(ConnectedComponent.propertyName, 'component').with_(ConnectedComponent.edges, outE('knows')).project('name','component').by('name').by('component')",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  connectedComponent().
+    with_(ConnectedComponent.propertyName, 'component').
+    with_(ConnectedComponent.edges, outE('knows')).
+  project('name', 'component').
+    by('name').
+    by('component')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('software').values('name').fold().order(Scope.local).index().with_(WithOptions.indexer, WithOptions.list).unfold().order().by(__.tail(Scope.local, 1))",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('software').
+  values('name').
+  fold().
+  order(Scope.local).
+  index().
+    with_(WithOptions.indexer, WithOptions.list).
+  unfold().
+  order().by(__.tail(Scope.local, 1))`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').values('name').fold().order(Scope.local).index().with_(WithOptions.indexer, WithOptions.map)",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  values('name').
+  fold().
+  order(Scope.local).
+  index().
+    with_(WithOptions.indexer, WithOptions.map)`,
+  );
+  expect(
+    formatQuery('g.io(someInputFile).with_(IO.reader, IO.graphson).read().iterate()', {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.io(someInputFile).
+    with_(IO.reader, IO.graphson).
+    read().
+  iterate()`,
+  );
+  expect(
+    formatQuery('g.io(someOutputFile).with_(IO.writer,IO.graphml).write().iterate()', {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.io(someOutputFile).
+    with_(IO.writer, IO.graphml).
+    write().
+  iterate()`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').pageRank().with_(PageRank.edges, __.outE('knows')).with_(PageRank.propertyName, 'friendRank').order().by('friendRank',desc).elementMap('name','friendRank')",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  pageRank().
+    with_(PageRank.edges, __.outE('knows')).
+    with_(PageRank.propertyName, 'friendRank').
+  order().by('friendRank', desc).
+  elementMap('name', 'friendRank')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').peerPressure().with_(PeerPressure.propertyName, 'cluster').group().by('cluster').by('name')",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  peerPressure().
+    with_(PeerPressure.propertyName, 'cluster').
+  group().by('cluster').by('name')`,
+  );
+  expect(
+    formatQuery("g.V().shortestPath().with_(ShortestPath.target, __.has('name','peter'))", {
+      indentation: 0,
+      maxLineLength: 55,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  shortestPath().
+    with_(ShortestPath.target, __.has('name', 'peter'))`,
+  );
+  expect(
+    formatQuery(
+      "g.V().shortestPath().with_(ShortestPath.edges, Direction.IN).with_(ShortestPath.target, __.has('name','josh'))",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  shortestPath().
+    with_(ShortestPath.edges, Direction.IN).
+    with_(ShortestPath.target, __.has('name', 'josh'))`,
+  );
+  expect(
+    formatQuery("g.V().has('person','name','marko').shortestPath().with_(ShortestPath.target,__.has('name','josh'))", {
+      indentation: 0,
+      maxLineLength: 55,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with_(ShortestPath.target, __.has('name', 'josh'))`,
+  );
+  expect(
+    formatQuery(
+      "g.V().has('person','name','marko').shortestPath().with_(ShortestPath.target, __.has('name','josh')).with_(ShortestPath.distance, 'weight')",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with_(ShortestPath.target, __.has('name', 'josh')).
+    with_(ShortestPath.distance, 'weight')`,
+  );
+  expect(
+    formatQuery(
+      "g.V().has('person','name','marko').shortestPath().with_(ShortestPath.target, __.has('name','josh')).with_(ShortestPath.includeEdges, true)",
+      {
+        indentation: 0,
+        maxLineLength: 55,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  has('person', 'name', 'marko').
+  shortestPath().
+    with_(ShortestPath.target, __.has('name', 'josh')).
+    with_(ShortestPath.includeEdges, true)`,
+  );
+  expect(
+    formatQuery(
+      "g.inject(g.withComputer().V().shortestPath().with_(ShortestPath.distance, 'weight').with_(ShortestPath.includeEdges, true).with_(ShortestPath.maxDistance, 1).toList().toArray()).map(unfold().values('name','weight').fold())",
+      {
+        indentation: 0,
+        maxLineLength: 50,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.inject(
+  g.withComputer().
+    V().
+    shortestPath().
+      with_(ShortestPath.distance, 'weight').
+      with_(ShortestPath.includeEdges, true).
+      with_(ShortestPath.maxDistance, 1).
+    toList().
+    toArray()).
+  map(unfold().values('name', 'weight').fold())`,
+  );
+  expect(
+    formatQuery("g.V().hasLabel('person').valueMap().with_(WithOptions.tokens)", {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  valueMap().
+    with_(WithOptions.tokens)`,
+  );
+  expect(
+    formatQuery("g.V().hasLabel('person').valueMap('name').with_(WithOptions.tokens,WithOptions.labels)", {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  valueMap('name').
+    with_(
+      WithOptions.tokens,
+      WithOptions.labels)`,
+  );
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').properties('location').valueMap().with_(WithOptions.tokens, WithOptions.values)",
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.V().
+  hasLabel('person').
+  properties('location').
+  valueMap().
+    with_(
+      WithOptions.tokens,
+      WithOptions.values)`,
+  );
+
+  // Test write()-modulator indentation
+  expect(
+    formatQuery('g.io(someOutputFile).write().iterate()', {
+      indentation: 0,
+      maxLineLength: 25,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(
+    `g.io(someOutputFile).
+    write().
+  iterate()`,
+  );
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts
new file mode 100644
index 0000000..8859eac
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test("Modulators should not be line-wrapped if they can fit on the line of the step they're modulating", () => {
+  // Test as()-modulator wrapping
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').as('person').properties('location').as('location').select('person','location').by('name').by(valueMap())",
+      {
+        indentation: 0,
+        maxLineLength: 80,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').as('person').
+  properties('location').as('location').
+  select('person', 'location').by('name').by(valueMap())`);
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').as('person').properties('location').as('location').select('person','location').by('name').by(valueMap())",
+      {
+        indentation: 0,
+        maxLineLength: 40,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').as('person').
+  properties('location').as('location').
+  select('person', 'location').
+    by('name').
+    by(valueMap())`);
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').as('person').properties('location').as('location').select('person','location').by('name').by(valueMap())",
+      {
+        indentation: 0,
+        maxLineLength: 35,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').as('person').
+  properties('location').
+    as('location').
+  select('person', 'location').
+    by('name').
+    by(valueMap())`);
+
+  // Test by()-modulator wrapping
+  expect(
+    formatQuery('g.V().group().by().by(bothE().count())', {
+      indentation: 0,
+      maxLineLength: 40,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe('g.V().group().by().by(bothE().count())');
+  expect(
+    formatQuery('g.V().group().by().by(bothE().count())', {
+      indentation: 0,
+      maxLineLength: 35,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  group().by().by(bothE().count())`);
+  expect(
+    formatQuery('g.V().group().by().by(bothE().count())', {
+      indentation: 0,
+      maxLineLength: 30,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  group().
+    by().
+    by(bothE().count())`);
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').group().by(values('name', 'age').fold()).unfold().filter(select(values).count(local).is(gt(1)))",
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').
+  group().by(values('name', 'age').fold()).
+  unfold().
+  filter(
+    select(values).count(local).is(gt(1)))`);
+  expect(
+    formatQuery(
+      "g.V().hasLabel('person').group().by(values('name', 'age').fold()).unfold().filter(select(values).count(local).is(gt(1)))",
+      {
+        indentation: 0,
+        maxLineLength: 40,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  hasLabel('person').
+  group().
+    by(values('name', 'age').fold()).
+  unfold().
+  filter(
+    select(values).
+    count(local).
+    is(gt(1)))`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/multipleQueriesAtOnce.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/multipleQueriesAtOnce.test.ts
new file mode 100644
index 0000000..bfb8fda
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/multipleQueriesAtOnce.test.ts
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('Each of multiple queries formatted at once should be formatted as if they were formatted individually', () => {
+  // Test that linebreaks don't happen too soon because the formatter fails to distinguish between lines from the end
+  // of one query and the start of the next
+  expect(
+    formatQuery(
+      `g.V(1).out().values('name')
+
+g.V(1).out().map{ it.get().value('name') }
+
+g.V(1).out().map(values('name'))`,
+      {
+        indentation: 0,
+        maxLineLength: 45,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V(1).out().values('name')
+
+g.V(1).out().map{ it.get().value('name') }
+
+g.V(1).out().map(values('name'))`);
+
+  expect(
+    formatQuery(
+      `g.V().branch{ it.get().value('name') }.option('marko', values('age')).option(none, values('name'))
+
+g.V().branch(values('name')).option('marko', values('age')).option(none, values('name'))
+
+g.V().choose(has('name','marko'),values('age'), values('name'))`,
+      {
+        indentation: 0,
+        maxLineLength: 70,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`g.V().
+  branch{ it.get().value('name') }.
+    option('marko', values('age')).
+    option(none, values('name'))
+
+g.V().
+  branch(values('name')).
+    option('marko', values('age')).
+    option(none, values('name'))
+
+g.V().choose(has('name', 'marko'), values('age'), values('name'))`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/nonGremlinIndentation.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/nonGremlinIndentation.test.ts
new file mode 100644
index 0000000..88c3f2a
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/nonGremlinIndentation.test.ts
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+// TODO: These tests seem related to those in closureIndentation.test.ts, so it might be worth checking if they can be merged.
+test('Both top-level and inlined non-Gremlin code should be indented together with the Gremlin query', () => {
+  // Test that top-level and inlined non-Gremlin code are not indented when indentation is 0
+  expect(
+    formatQuery(
+      `hasField = { field -> __.has(field) }
+
+profitQuery = g.V().
+filter(hasField('sell_price')).
+filter(hasField('buy_price')).
+project('product', 'profit').
+by('name').
+by{ it.get().value('sell_price') -
+    it.get().value('buy_price') };`,
+      {
+        indentation: 0,
+        maxLineLength: 70,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`hasField = { field -> __.has(field) }
+
+profitQuery = g.V().
+  filter(hasField('sell_price')).
+  filter(hasField('buy_price')).
+  project('product', 'profit').
+    by('name').
+    by{ it.get().value('sell_price') -
+        it.get().value('buy_price') };`);
+
+  // Test that top-level and inlined non-Gremlin code are not indented when indentation is 20
+  expect(
+    formatQuery(
+      `hasField = { field -> __.has(field) }
+      
+profitQuery = g.V().
+filter(hasField('sell_price')).
+filter(hasField('buy_price')).
+project('product', 'profit').
+by('name').
+by{ it.get().value('sell_price') -
+    it.get().value('buy_price') };`,
+      {
+        indentation: 20,
+        maxLineLength: 70,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(`                    hasField = { field -> __.has(field) }
+
+                    profitQuery = g.V().
+                      filter(hasField('sell_price')).
+                      filter(hasField('buy_price')).
+                      project('product', 'profit').
+                        by('name').
+                        by{ it.get().value('sell_price') -
+                            it.get().value('buy_price') };`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/__tests__/nonMethodIndentation.test.ts b/gremlint/gremlint/src/formatQuery/__tests__/nonMethodIndentation.test.ts
new file mode 100644
index 0000000..2b2903a
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/__tests__/nonMethodIndentation.test.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatQuery } from '..';
+
+test('Non-methods in a traversal should be indented correctly, even if this might never occur in a valid query', () => {
+  expect(
+    formatQuery('g.V().stepWhichIsNotAMethod.stepWhichIsAMethod()', {
+      indentation: 0,
+      maxLineLength: 45,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  stepWhichIsNotAMethod.stepWhichIsAMethod()`);
+
+  expect(
+    formatQuery('g.V().stepWhichIsNotAMethod', {
+      indentation: 0,
+      maxLineLength: 25,
+      shouldPlaceDotsAfterLineBreaks: false,
+    }),
+  ).toBe(`g.V().
+  stepWhichIsNotAMethod`);
+});
diff --git a/gremlint/gremlint/src/formatQuery/consts.ts b/gremlint/gremlint/src/formatQuery/consts.ts
new file mode 100644
index 0000000..6799877
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/consts.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const STEP_MODULATORS = [
+  'as',
+  'as_',
+  'by',
+  'emit',
+  'option',
+  'from',
+  'from_',
+  'to',
+  'read',
+  'times',
+  'until',
+  'with',
+  'with_',
+  'write',
+];
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts
new file mode 100644
index 0000000..9bfd023
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import recreateQueryOnelinerFromSyntaxTree from '../recreateQueryOnelinerFromSyntaxTree';
+import {
+  FormattedClosureSyntaxTree,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  TokenType,
+  UnformattedClosureCodeBlock,
+  UnformattedClosureLineOfCode,
+  UnformattedClosureSyntaxTree,
+} from '../types';
+import { withNoEndDotInfo } from './utils';
+
+const getClosureLineOfCodeIndentation = (
+  relativeIndentation: number,
+  horizontalPosition: number,
+  methodWidth: number,
+  lineNumber: number,
+) => {
+  if (lineNumber === 0) return Math.max(relativeIndentation, 0);
+  return Math.max(relativeIndentation + horizontalPosition + methodWidth + 1, 0);
+};
+
+const getFormattedClosureLineOfCode = (horizontalPosition: number, methodWidth: number) => (
+  { lineOfCode, relativeIndentation }: UnformattedClosureLineOfCode,
+  lineNumber: number,
+) => ({
+  lineOfCode,
+  relativeIndentation,
+  localIndentation: getClosureLineOfCodeIndentation(relativeIndentation, horizontalPosition, methodWidth, lineNumber),
+});
+
+const getFormattedClosureCodeBlock = (
+  unformattedClosureCodeBlock: UnformattedClosureCodeBlock,
+  horizontalPosition: number,
+  methodWidth: number,
+) => {
+  return unformattedClosureCodeBlock.map(getFormattedClosureLineOfCode(horizontalPosition, methodWidth));
+};
+
+export const formatClosure = (formatSyntaxTree: GremlinSyntaxTreeFormatter) => (config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedClosureSyntaxTree,
+): FormattedClosureSyntaxTree => {
+  const { closureCodeBlock: unformattedClosureCodeBlock, method: unformattedMethod } = syntaxTree;
+  const { localIndentation, horizontalPosition, maxLineLength, shouldEndWithDot } = config;
+  const recreatedQuery = recreateQueryOnelinerFromSyntaxTree(localIndentation)(syntaxTree);
+  const formattedMethod = formatSyntaxTree(withNoEndDotInfo(config))(unformattedMethod);
+  const methodWidth = formattedMethod.width;
+
+  if (recreatedQuery.length <= maxLineLength) {
+    return {
+      type: TokenType.Closure,
+      method: formattedMethod,
+      closureCodeBlock: getFormattedClosureCodeBlock(unformattedClosureCodeBlock, horizontalPosition, methodWidth),
+      localIndentation,
+      width: recreatedQuery.trim().length,
+      shouldStartWithDot: false,
+      shouldEndWithDot: Boolean(shouldEndWithDot),
+    };
+  }
+
+  return {
+    type: TokenType.Closure,
+    method: formattedMethod,
+    closureCodeBlock: getFormattedClosureCodeBlock(unformattedClosureCodeBlock, horizontalPosition, methodWidth),
+    localIndentation: 0,
+    width: 0,
+    shouldStartWithDot: false,
+    shouldEndWithDot: Boolean(shouldEndWithDot),
+  };
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts
new file mode 100644
index 0000000..fd4f92b
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import recreateQueryOnelinerFromSyntaxTree from '../recreateQueryOnelinerFromSyntaxTree';
+import {
+  FormattedMethodSyntaxTree,
+  FormattedSyntaxTree,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  TokenType,
+  UnformattedMethodSyntaxTree,
+} from '../types';
+import { last, pipe, sum } from '../utils';
+import {
+  withHorizontalPosition,
+  withIncreasedHorizontalPosition,
+  withIncreasedIndentation,
+  withNoEndDotInfo,
+  withZeroDotInfo,
+  withZeroIndentation,
+} from './utils';
+
+// Groups arguments into argument groups an adds a localIndentation property
+export const formatMethod = (formatSyntaxTree: GremlinSyntaxTreeFormatter) => (config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedMethodSyntaxTree,
+): FormattedMethodSyntaxTree => {
+  const recreatedQuery = recreateQueryOnelinerFromSyntaxTree(config.localIndentation)(syntaxTree);
+  const method = formatSyntaxTree(withNoEndDotInfo(config))(syntaxTree.method);
+  const argumentsWillNotBeWrapped = recreatedQuery.length <= config.maxLineLength;
+  if (argumentsWillNotBeWrapped) {
+    return {
+      type: TokenType.Method,
+      method,
+      // The arguments property is here so that the resulted syntax tree can
+      // still be understood by recreateQueryOnelinerFromSyntaxTree
+      arguments: syntaxTree.arguments,
+      argumentGroups: [
+        syntaxTree.arguments.reduce((argumentGroup: FormattedSyntaxTree[], syntaxTree) => {
+          return [
+            ...argumentGroup,
+            formatSyntaxTree(
+              // Since the method's arguments will be on the same line, their horizontal position is increased by the
+              // method's width plus the width of the opening parenthesis
+              pipe(
+                withZeroIndentation,
+                withZeroDotInfo,
+                withIncreasedHorizontalPosition(
+                  method.width + 1 + argumentGroup.map(({ width }) => width).reduce(sum, 0) + argumentGroup.length,
+                ),
+              )(config),
+            )(syntaxTree),
+          ];
+        }, []),
+      ],
+      argumentsShouldStartOnNewLine: false,
+      localIndentation: config.localIndentation,
+      shouldStartWithDot: false,
+      shouldEndWithDot: Boolean(config.shouldEndWithDot),
+      width: recreatedQuery.trim().length,
+    };
+  }
+  // shouldEndWithDot has to reside on the method object, so the end dot can be
+  // placed after the method parentheses. shouldStartWithDot has to be passed on
+  // further down so the start dot can be placed after the indentation.
+  const argumentGroups = syntaxTree.arguments.map((step) => [
+    formatSyntaxTree(
+      pipe(withIncreasedIndentation(2), withZeroDotInfo, withHorizontalPosition(config.localIndentation + 2))(config),
+    )(step),
+  ]);
+  const lastArgumentGroup = last(argumentGroups);
+  // Add the width of the last line of parameters, the dots between them and the indentation of the parameters
+  const width = lastArgumentGroup
+    ? lastArgumentGroup.map(({ width }) => width).reduce(sum, 0) + lastArgumentGroup.length - 1
+    : 0;
+  return {
+    type: TokenType.Method,
+    method,
+    arguments: syntaxTree.arguments,
+    argumentGroups,
+    argumentsShouldStartOnNewLine: true,
+    shouldStartWithDot: false,
+    shouldEndWithDot: Boolean(config.shouldEndWithDot),
+    localIndentation: 0,
+    width,
+  };
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatNonGremlin.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatNonGremlin.ts
new file mode 100644
index 0000000..94caf85
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatNonGremlin.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FormattedNonGremlinSyntaxTree, GremlintInternalConfig, UnformattedNonGremlinSyntaxTree } from '../types';
+import { count, last } from '../utils';
+
+export const formatNonGremlin = (_config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedNonGremlinSyntaxTree,
+): FormattedNonGremlinSyntaxTree => {
+  return {
+    ...syntaxTree,
+    width: count(last(syntaxTree.code.split('\n'))),
+  };
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatString.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatString.ts
new file mode 100644
index 0000000..19660cd
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatString.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FormattedStringSyntaxTree, GremlintInternalConfig, TokenType, UnformattedStringSyntaxTree } from '../types';
+
+export const formatString = (config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedStringSyntaxTree,
+): FormattedStringSyntaxTree => {
+  return {
+    type: TokenType.String,
+    string: syntaxTree.string,
+    localIndentation: config.localIndentation,
+    width: syntaxTree.string.length + 2,
+  };
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/index.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/index.ts
new file mode 100644
index 0000000..bfc657c
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/index.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import reduceSingleStepInStepGroup from './reduceSingleStepInStepGroup';
+import reduceLastStepInStepGroup from './reduceLastStepInStepGroup';
+import reduceFirstStepInStepGroup from './reduceFirstStepInStepGroup';
+import reduceMiddleStepInStepGroup from './reduceMiddleStepInStepGroup';
+import { isStepFirstStepInStepGroup, shouldStepBeLastStepInStepGroup } from './utils';
+import { choose } from '../../../utils';
+import {
+  GremlinStepGroup,
+  FormattedSyntaxTree,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  UnformattedSyntaxTree,
+} from '../../../types';
+
+export const getStepGroups = (
+  formatSyntaxTree: GremlinSyntaxTreeFormatter,
+  steps: UnformattedSyntaxTree[],
+  config: GremlintInternalConfig,
+): GremlinStepGroup[] => {
+  return steps.reduce(
+    choose(
+      shouldStepBeLastStepInStepGroup(config),
+      choose(
+        isStepFirstStepInStepGroup,
+        reduceSingleStepInStepGroup(formatSyntaxTree, config),
+        reduceLastStepInStepGroup(formatSyntaxTree, config),
+      ),
+      choose(
+        isStepFirstStepInStepGroup,
+        reduceFirstStepInStepGroup(formatSyntaxTree, config),
+        reduceMiddleStepInStepGroup(formatSyntaxTree, config),
+      ),
+    ),
+    {
+      stepsInStepGroup: [] as FormattedSyntaxTree[],
+      stepGroups: [] as GremlinStepGroup[],
+    },
+  ).stepGroups;
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceFirstStepInStepGroup.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceFirstStepInStepGroup.ts
new file mode 100644
index 0000000..0b6601d
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceFirstStepInStepGroup.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  FormattedSyntaxTree,
+  GremlinStepGroup,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  UnformattedSyntaxTree,
+} from '../../../types';
+import { pipe } from '../../../utils';
+import { withDotInfo, withHorizontalPosition, withIndentation } from '../../utils';
+import { isTraversalSource } from './utils';
+
+// If it is the first step in a group and also not the last one, format it
+// with indentation, otherwise, remove the indentation
+const reduceFirstStepInStepGroup = (formatSyntaxTree: GremlinSyntaxTreeFormatter, config: GremlintInternalConfig) => (
+  {
+    stepsInStepGroup,
+    stepGroups,
+  }: {
+    stepsInStepGroup: FormattedSyntaxTree[];
+    stepGroups: GremlinStepGroup[];
+  },
+  step: UnformattedSyntaxTree,
+) => {
+  const localIndentation =
+    config.localIndentation + (stepGroups[0] && isTraversalSource(stepGroups[0].steps[0]) ? 2 : 0);
+
+  const isFirstStepGroup = stepGroups.length === 0;
+
+  // It is the first step in a group and should start with a dot if it is
+  // not the first stepGroup and config.shouldPlaceDotsAfterLineBreaks
+  const shouldStartWithDot = !isFirstStepGroup && config.shouldPlaceDotsAfterLineBreaks;
+
+  // It is the first step in a group, but not the last, so it should not
+  // end with a dot.
+  const shouldEndWithDot = false;
+
+  return {
+    stepsInStepGroup: [
+      formatSyntaxTree(
+        pipe(
+          withIndentation(localIndentation),
+          withDotInfo({ shouldStartWithDot, shouldEndWithDot }),
+          withHorizontalPosition(localIndentation),
+        )(config),
+      )(step),
+    ],
+    stepGroups,
+  };
+};
+
+export default reduceFirstStepInStepGroup;
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceLastStepInStepGroup.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceLastStepInStepGroup.ts
new file mode 100644
index 0000000..1caefdc
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceLastStepInStepGroup.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  FormattedSyntaxTree,
+  GremlinStepGroup,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  UnformattedSyntaxTree,
+} from '../../../types';
+import { pipe, sum } from '../../../utils';
+import { withDotInfo, withIncreasedHorizontalPosition, withZeroIndentation } from '../../utils';
+
+// If it should be the last step in a line
+// We don't want to newline after words which are not methods. For
+// instance, g.V() should be one one line, as should __.as
+const reduceLastStepInStepGroup = (formatSyntaxTree: GremlinSyntaxTreeFormatter, config: GremlintInternalConfig) => (
+  {
+    stepsInStepGroup,
+    stepGroups,
+  }: {
+    stepsInStepGroup: FormattedSyntaxTree[];
+    stepGroups: GremlinStepGroup[];
+  },
+  step: UnformattedSyntaxTree,
+  index: number,
+  steps: UnformattedSyntaxTree[],
+) => {
+  const isLastStepGroup = index === steps.length - 1;
+  // If it is the last (and also not first) step in a group
+  // This is not the first step in the step group, so it should not
+  // start with a dot
+  const shouldStartWithDot = false;
+
+  const shouldEndWithDot = !isLastStepGroup && !config.shouldPlaceDotsAfterLineBreaks;
+
+  return {
+    stepsInStepGroup: [],
+    stepGroups: [
+      ...stepGroups,
+      {
+        steps: [
+          ...stepsInStepGroup,
+          formatSyntaxTree(
+            pipe(
+              withZeroIndentation,
+              withDotInfo({ shouldStartWithDot, shouldEndWithDot }),
+              withIncreasedHorizontalPosition(
+                // If I recall correctly, the + stepsInStepGroup.length handles the horizontal increase caused by the dots joining the steps
+                stepsInStepGroup.map(({ width }) => width).reduce(sum, 0) + stepsInStepGroup.length,
+              ),
+            )(config),
+          )(step),
+        ],
+      },
+    ],
+  };
+};
+
+export default reduceLastStepInStepGroup;
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceMiddleStepInStepGroup.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceMiddleStepInStepGroup.ts
new file mode 100644
index 0000000..6d2059e
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceMiddleStepInStepGroup.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  FormattedSyntaxTree,
+  GremlinStepGroup,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  UnformattedSyntaxTree,
+} from '../../../types';
+import { pipe, sum } from '../../../utils';
+import { withDotInfo, withHorizontalPosition, withZeroIndentation } from '../../utils';
+
+const reduceMiddleStepInStepGroup = (formatSyntaxTree: GremlinSyntaxTreeFormatter, config: GremlintInternalConfig) => (
+  {
+    stepsInStepGroup,
+    stepGroups,
+  }: {
+    stepsInStepGroup: FormattedSyntaxTree[];
+    stepGroups: GremlinStepGroup[];
+  },
+  step: UnformattedSyntaxTree,
+) => {
+  const horizontalPosition =
+    config.localIndentation + stepsInStepGroup.map(({ width }) => width).reduce(sum, 0) + stepsInStepGroup.length;
+
+  return {
+    stepsInStepGroup: [
+      ...stepsInStepGroup,
+      formatSyntaxTree(
+        pipe(
+          withZeroIndentation,
+          withDotInfo({ shouldStartWithDot: false, shouldEndWithDot: false }),
+          withHorizontalPosition(horizontalPosition),
+        )(config),
+      )(step),
+    ],
+    stepGroups,
+  };
+};
+
+export default reduceMiddleStepInStepGroup;
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts
new file mode 100644
index 0000000..4b90fe7
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  FormattedSyntaxTree,
+  GremlinStepGroup,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  UnformattedSyntaxTree,
+} from '../../../types';
+import { pipe } from '../../../utils';
+import { withDotInfo, withHorizontalPosition, withIndentation } from '../../utils';
+import { isModulator, isTraversalSource } from './utils';
+
+// If it should be the last step in a line
+// We don't want to newline after words which are not methods. For
+// instance, g.V() should be one one line, as should __.as
+const reduceSingleStepInStepGroup = (formatSyntaxTree: GremlinSyntaxTreeFormatter, config: GremlintInternalConfig) => (
+  {
+    stepsInStepGroup,
+    stepGroups,
+  }: {
+    stepsInStepGroup: FormattedSyntaxTree[];
+    stepGroups: GremlinStepGroup[];
+  },
+  step: UnformattedSyntaxTree,
+  index: number,
+  steps: UnformattedSyntaxTree[],
+) => {
+  const isFirstStepGroup = stepGroups.length === 0;
+  const isLastStepGroup = index === steps.length - 1;
+  const traversalSourceIndentationIncrease = stepGroups[0] && isTraversalSource(stepGroups[0].steps[0]) ? 2 : 0;
+  const modulatorIndentationIncrease = isModulator(step) ? 2 : 0;
+  const localIndentation = config.localIndentation + traversalSourceIndentationIncrease + modulatorIndentationIncrease;
+
+  // This is the only step in the step group, so it is the first step in
+  // the step group. It should only start with a dot if it is not the
+  // first stepGroup and config.shouldPlaceDotsAfterLineBreaks
+  const shouldStartWithDot = !isFirstStepGroup && config.shouldPlaceDotsAfterLineBreaks;
+
+  // It is the last step in a group and should only end with dot if not
+  // config.shouldPlaceDotsAfterLineBreaks this is not the last step in
+  // steps
+  const shouldEndWithDot = !isLastStepGroup && !config.shouldPlaceDotsAfterLineBreaks;
+
+  return {
+    stepsInStepGroup: [],
+    stepGroups: [
+      ...stepGroups,
+      {
+        steps: [
+          formatSyntaxTree(
+            pipe(
+              withIndentation(localIndentation),
+              withDotInfo({ shouldStartWithDot, shouldEndWithDot }),
+              withHorizontalPosition(localIndentation + +config.shouldPlaceDotsAfterLineBreaks),
+            )(config),
+          )(step),
+        ],
+      },
+    ],
+  };
+};
+
+export default reduceSingleStepInStepGroup;
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts
new file mode 100644
index 0000000..13392fb
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  FormattedSyntaxTree,
+  GremlinStepGroup,
+  GremlintInternalConfig,
+  TokenType,
+  UnformattedSyntaxTree,
+} from '../../../types';
+import { STEP_MODULATORS } from '../../../consts';
+import recreateQueryOnelinerFromSyntaxTree from '../../../recreateQueryOnelinerFromSyntaxTree';
+
+export const isTraversalSource = (step: UnformattedSyntaxTree | FormattedSyntaxTree): boolean => {
+  return step.type === TokenType.Word && step.word === 'g';
+};
+
+export const isModulator = (step: UnformattedSyntaxTree | FormattedSyntaxTree): boolean => {
+  if (step.type !== TokenType.Method && step.type !== TokenType.Closure) return false;
+  if (step.method.type !== TokenType.Word) return false;
+  return STEP_MODULATORS.includes(step.method.word);
+};
+
+export const isStepFirstStepInStepGroup = ({ stepsInStepGroup }: { stepsInStepGroup: FormattedSyntaxTree[] }) => {
+  return !stepsInStepGroup.length;
+};
+
+const isLineTooLongWithSubsequentModulators = (config: GremlintInternalConfig) => (
+  {
+    stepsInStepGroup,
+    stepGroups,
+  }: {
+    stepsInStepGroup: FormattedSyntaxTree[];
+    stepGroups: GremlinStepGroup[];
+  },
+  step: UnformattedSyntaxTree,
+  index: number,
+  steps: UnformattedSyntaxTree[],
+) => {
+  const stepsWithSubsequentModulators = steps.slice(index + 1).reduce(
+    (aggregator, step) => {
+      const { stepsInStepGroup, hasReachedFinalModulator } = aggregator;
+      if (hasReachedFinalModulator) return aggregator;
+      if (isModulator(step)) {
+        return {
+          ...aggregator,
+          stepsInStepGroup: [...stepsInStepGroup, step],
+        };
+      }
+      return { ...aggregator, hasReachedFinalModulator: true };
+    },
+    {
+      stepsInStepGroup: [...stepsInStepGroup, step],
+      hasReachedFinalModulator: false,
+    },
+  ).stepsInStepGroup;
+
+  const stepGroupIndentationIncrease = (() => {
+    const traversalSourceIndentationIncrease = stepGroups[0] && isTraversalSource(stepGroups[0].steps[0]) ? 2 : 0;
+    const modulatorIndentationIncrease = isModulator([...stepsInStepGroup, step][0]) ? 2 : 0;
+    const indentationIncrease = traversalSourceIndentationIncrease + modulatorIndentationIncrease;
+    return indentationIncrease;
+  })();
+
+  const recreatedQueryWithSubsequentModulators = recreateQueryOnelinerFromSyntaxTree(
+    config.localIndentation + stepGroupIndentationIncrease,
+  )({
+    type: TokenType.Traversal,
+    steps: stepsWithSubsequentModulators,
+  });
+
+  const lineIsTooLongWithSubsequentModulators = recreatedQueryWithSubsequentModulators.length > config.maxLineLength;
+  return lineIsTooLongWithSubsequentModulators;
+};
+
+// If the first step in a group is a modulator, then it must also be the last step in the group
+export const shouldStepBeLastStepInStepGroup = (config: GremlintInternalConfig) => (
+  {
+    stepsInStepGroup,
+    stepGroups,
+  }: {
+    stepsInStepGroup: FormattedSyntaxTree[];
+    stepGroups: GremlinStepGroup[];
+  },
+  step: UnformattedSyntaxTree,
+  index: number,
+  steps: UnformattedSyntaxTree[],
+) => {
+  const isFirstStepInStepGroup = !stepsInStepGroup.length;
+
+  const isLastStep = index === steps.length - 1;
+  const nextStepIsModulator = !isLastStep && isModulator(steps[index + 1]);
+
+  const lineIsTooLongWithSubsequentModulators = isLineTooLongWithSubsequentModulators(config)(
+    { stepsInStepGroup, stepGroups },
+    step,
+    index,
+    steps,
+  );
+
+  // If the first step in a group is a modulator, then it must also be the last step in the group
+  const stepShouldBeLastStepInStepGroup =
+    isLastStep ||
+    (isFirstStepInStepGroup && isModulator(step)) ||
+    ((step.type === TokenType.Method || step.type === TokenType.Closure) &&
+      !(nextStepIsModulator && !lineIsTooLongWithSubsequentModulators));
+  return stepShouldBeLastStepInStepGroup;
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts
new file mode 100644
index 0000000..74417f4
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import recreateQueryOnelinerFromSyntaxTree from '../../recreateQueryOnelinerFromSyntaxTree';
+import {
+  FormattedSyntaxTree,
+  FormattedTraversalSyntaxTree,
+  GremlinSyntaxTreeFormatter,
+  GremlintInternalConfig,
+  TokenType,
+  UnformattedTraversalSyntaxTree,
+} from '../../types';
+import { last, pipe, sum } from '../../utils';
+import { withIncreasedHorizontalPosition, withZeroIndentation } from '../utils';
+import { getStepGroups } from './getStepGroups';
+import { isTraversalSource } from './getStepGroups/utils';
+
+// Groups steps into step groups and adds a localIndentation property
+export const formatTraversal = (formatSyntaxTree: GremlinSyntaxTreeFormatter) => (config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedTraversalSyntaxTree,
+): FormattedTraversalSyntaxTree => {
+  const initialHorizontalPositionIndentationIncrease =
+    syntaxTree.steps[0] && isTraversalSource(syntaxTree.steps[0]) ? syntaxTree.initialHorizontalPosition : 0;
+  const recreatedQuery = recreateQueryOnelinerFromSyntaxTree(
+    config.localIndentation + initialHorizontalPositionIndentationIncrease,
+  )(syntaxTree);
+  if (recreatedQuery.length <= config.maxLineLength) {
+    return {
+      type: TokenType.Traversal,
+      steps: syntaxTree.steps,
+      stepGroups: [
+        {
+          steps: syntaxTree.steps.reduce((steps, step, stepIndex) => {
+            const formattedStep =
+              stepIndex === 0
+                ? formatSyntaxTree(config)(step)
+                : // Since the traversal's steps will be on the same line, their horizontal position is increased by the
+                  // steps's width plus the width of the dots between them
+                  formatSyntaxTree(
+                    pipe(
+                      withZeroIndentation,
+                      withIncreasedHorizontalPosition(
+                        syntaxTree.initialHorizontalPosition +
+                          steps.map(({ width }) => width).reduce(sum, 0) +
+                          steps.length,
+                      ),
+                    )(config),
+                  )(step);
+            return [...steps, formattedStep];
+          }, [] as FormattedSyntaxTree[]),
+        },
+      ],
+      initialHorizontalPosition: syntaxTree.initialHorizontalPosition,
+      localIndentation: 0,
+      width: recreatedQuery.trim().length,
+    };
+  }
+  const stepGroups = getStepGroups(formatSyntaxTree, syntaxTree.steps, config);
+  const lastStepGroup = last(stepGroups);
+  const width = lastStepGroup
+    ? lastStepGroup.steps.map(({ width }) => width).reduce(sum, 0) + stepGroups.length - 1
+    : 0;
+  return {
+    type: TokenType.Traversal,
+    steps: syntaxTree.steps,
+    stepGroups,
+    initialHorizontalPosition: syntaxTree.initialHorizontalPosition,
+    localIndentation: 0,
+    width,
+  };
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatWord.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatWord.ts
new file mode 100644
index 0000000..d8304cc
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/formatWord.ts
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FormattedWordSyntaxTree, GremlintInternalConfig, TokenType, UnformattedWordSyntaxTree } from '../types';
+
+export const formatWord = (config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedWordSyntaxTree,
+): FormattedWordSyntaxTree => {
+  return {
+    type: TokenType.Word,
+    word: syntaxTree.word,
+    localIndentation: config.localIndentation,
+    shouldStartWithDot: Boolean(config.shouldStartWithDot),
+    shouldEndWithDot: Boolean(config.shouldEndWithDot),
+    width: syntaxTree.word.length,
+  };
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/index.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/index.ts
new file mode 100644
index 0000000..5018e26
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/index.ts
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FormattedSyntaxTree, GremlintInternalConfig, TokenType, UnformattedSyntaxTree } from '../types';
+import { formatClosure } from './formatClosure';
+import { formatMethod } from './formatMethod';
+import { formatNonGremlin } from './formatNonGremlin';
+import { formatString } from './formatString';
+import { formatTraversal } from './formatTraversal';
+import { formatWord } from './formatWord';
+
+const formatSyntaxTree = (config: GremlintInternalConfig) => (
+  syntaxTree: UnformattedSyntaxTree,
+): FormattedSyntaxTree => {
+  switch (syntaxTree.type) {
+    case TokenType.NonGremlinCode:
+      return formatNonGremlin(config)(syntaxTree);
+    case TokenType.Traversal:
+      return formatTraversal(formatSyntaxTree)(config)(syntaxTree);
+    case TokenType.Method:
+      return formatMethod(formatSyntaxTree)(config)(syntaxTree);
+    case TokenType.Closure:
+      return formatClosure(formatSyntaxTree)(config)(syntaxTree);
+    case TokenType.String:
+      return formatString(config)(syntaxTree);
+    case TokenType.Word:
+      return formatWord(config)(syntaxTree);
+  }
+};
+
+export const formatSyntaxTrees = (config: GremlintInternalConfig) => (syntaxTrees: UnformattedSyntaxTree[]) => {
+  return syntaxTrees.map(formatSyntaxTree(config));
+};
diff --git a/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/utils.ts b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/utils.ts
new file mode 100644
index 0000000..6b79988
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/formatSyntaxTrees/utils.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { GremlintInternalConfig } from '../types';
+
+export const withIndentation = (localIndentation: number) => (
+  config: GremlintInternalConfig,
+): GremlintInternalConfig => ({
+  ...config,
+  localIndentation,
+});
+
+export const withZeroIndentation = withIndentation(0);
+
+export const withIncreasedIndentation = (indentationIncrease: number) => (
+  config: GremlintInternalConfig,
+): GremlintInternalConfig => ({
+  ...config,
+  localIndentation: config.localIndentation + indentationIncrease,
+});
+
+export const withDotInfo = ({
+  shouldStartWithDot,
+  shouldEndWithDot,
+}: {
+  shouldStartWithDot: boolean;
+  shouldEndWithDot: boolean;
+}) => (config: GremlintInternalConfig): GremlintInternalConfig => ({
+  ...config,
+  shouldStartWithDot,
+  shouldEndWithDot,
+});
+
+export const withZeroDotInfo = (config: GremlintInternalConfig): GremlintInternalConfig => ({
+  ...config,
+  shouldStartWithDot: false,
+  shouldEndWithDot: false,
+});
+
+export const withNoEndDotInfo = (config: GremlintInternalConfig): GremlintInternalConfig => ({
+  ...config,
+  shouldEndWithDot: false,
+});
+
+export const withHorizontalPosition = (horizontalPosition: number) => (
+  config: GremlintInternalConfig,
+): GremlintInternalConfig => ({
+  ...config,
+  horizontalPosition,
+});
+
+export const withIncreasedHorizontalPosition = (horizontalPositionIncrease: number) => (
+  config: GremlintInternalConfig,
+): GremlintInternalConfig => ({
+  ...config,
+  horizontalPosition: config.horizontalPosition + horizontalPositionIncrease,
+});
diff --git a/gremlint/gremlint/src/formatQuery/index.ts b/gremlint/gremlint/src/formatQuery/index.ts
new file mode 100644
index 0000000..4f27e3e
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/index.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatSyntaxTrees } from './formatSyntaxTrees';
+import { parseToSyntaxTrees } from './parseToSyntaxTrees';
+import { recreateQueryStringFromFormattedSyntaxTrees } from './recreateQueryStringFromFormattedSyntaxTrees';
+import { GremlintInternalConfig, GremlintUserConfig } from './types';
+import { pipe } from './utils';
+
+const withDefaults = (config: Partial<GremlintUserConfig>): GremlintUserConfig => ({
+  indentation: 0,
+  maxLineLength: 80,
+  shouldPlaceDotsAfterLineBreaks: false,
+  ...config,
+});
+
+const getInternalGremlintConfig = ({
+  indentation,
+  maxLineLength,
+  shouldPlaceDotsAfterLineBreaks,
+}: GremlintUserConfig): GremlintInternalConfig => ({
+  globalIndentation: indentation,
+  localIndentation: 0,
+  maxLineLength: maxLineLength - indentation,
+  shouldPlaceDotsAfterLineBreaks,
+  shouldStartWithDot: false,
+  shouldEndWithDot: false,
+  horizontalPosition: 0,
+});
+
+export const formatQuery = (query: string, config?: Partial<GremlintUserConfig>): string => {
+  const internalConfig = getInternalGremlintConfig(withDefaults(config ?? {}));
+  return pipe(
+    parseToSyntaxTrees,
+    formatSyntaxTrees(internalConfig),
+    recreateQueryStringFromFormattedSyntaxTrees(internalConfig),
+  )(query);
+};
diff --git a/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/__tests__/extractGremlinQueries.test.ts b/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/__tests__/extractGremlinQueries.test.ts
new file mode 100644
index 0000000..d66fb2e
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/__tests__/extractGremlinQueries.test.ts
@@ -0,0 +1,564 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { extractGremlinQueries } from '../extractGremlinQueries';
+
+test('Extract the parts of the code that can be parsed as Gremlin', () => {
+  expect(
+    extractGremlinQueries(
+      `graph = TinkerFactory.createModern()
+g = graph.traversal()
+g.V().has('name','marko').out('knows').values('name')`,
+    ),
+  ).toStrictEqual([`g.V().has('name','marko').out('knows').values('name')`]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().has('name','marko').next()
+g.V(marko).out('knows')
+g.V(marko).out('knows').values('name')`,
+    ),
+  ).toStrictEqual([
+    `g.V().has('name','marko').next()`,
+    `g.V(marko).out('knows')`,
+    `g.V(marko).out('knows').values('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g = graph.traversal();
+List<Vertex> vertices = g.V().toList()`,
+    ),
+  ).toStrictEqual([`g.V().toList()`]);
+
+  expect(
+    extractGremlinQueries(
+      `v1 = g.addV('person').property('name','marko').next()
+v2 = g.addV('person').property('name','stephen').next()
+g.V(v1).addE('knows').to(v2).property('weight',0.75).iterate()`,
+    ),
+  ).toStrictEqual([
+    `g.addV('person').property('name','marko').next()`,
+    `g.addV('person').property('name','stephen').next()`,
+    `g.V(v1).addE('knows').to(v2).property('weight',0.75).iterate()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `marko = g.V().has('person','name','marko').next()
+peopleMarkoKnows = g.V().has('person','name','marko').out('knows').toList()`,
+    ),
+  ).toStrictEqual([
+    `g.V().has('person','name','marko').next()`,
+    `g.V().has('person','name','marko').out('knows').toList()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `graph = TinkerGraph.open()
+g = graph.traversal()
+v = g.addV().property('name','marko').property('name','marko a. rodriguez').next()
+g.V(v).properties('name').count()
+v.property(list, 'name', 'm. a. rodriguez')
+g.V(v).properties('name').count()
+g.V(v).properties()
+g.V(v).properties('name')
+g.V(v).properties('name').hasValue('marko')
+g.V(v).properties('name').hasValue('marko').property('acl','private')
+g.V(v).properties('name').hasValue('marko a. rodriguez')
+g.V(v).properties('name').hasValue('marko a. rodriguez').property('acl','public')
+g.V(v).properties('name').has('acl','public').value()
+g.V(v).properties('name').has('acl','public').drop()
+g.V(v).properties('name').has('acl','public').value()
+g.V(v).properties('name').has('acl','private').value()
+g.V(v).properties()
+g.V(v).properties().properties()
+g.V(v).properties().property('date',2014)
+g.V(v).properties().property('creator','stephen')
+g.V(v).properties().properties()
+g.V(v).properties('name').valueMap()
+g.V(v).property('name','okram')
+g.V(v).properties('name')
+g.V(v).values('name')`,
+    ),
+  ).toStrictEqual([
+    `g.addV().property('name','marko').property('name','marko a. rodriguez').next()`,
+    `g.V(v).properties('name').count()`,
+    `g.V(v).properties('name').count()`,
+    `g.V(v).properties()`,
+    `g.V(v).properties('name')`,
+    `g.V(v).properties('name').hasValue('marko')`,
+    `g.V(v).properties('name').hasValue('marko').property('acl','private')`,
+    `g.V(v).properties('name').hasValue('marko a. rodriguez')`,
+    `g.V(v).properties('name').hasValue('marko a. rodriguez').property('acl','public')`,
+    `g.V(v).properties('name').has('acl','public').value()`,
+    `g.V(v).properties('name').has('acl','public').drop()`,
+    `g.V(v).properties('name').has('acl','public').value()`,
+    `g.V(v).properties('name').has('acl','private').value()`,
+    `g.V(v).properties()`,
+    `g.V(v).properties().properties()`,
+    `g.V(v).properties().property('date',2014)`,
+    `g.V(v).properties().property('creator','stephen')`,
+    `g.V(v).properties().properties()`,
+    `g.V(v).properties('name').valueMap()`,
+    `g.V(v).property('name','okram')`,
+    `g.V(v).properties('name')`,
+    `g.V(v).values('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().as('a').
+      properties('location').as('b').
+      hasNot('endTime').as('c').
+      select('a','b','c').by('name').by(value).by('startTime') // determine the current location of each person
+g.V().has('name','gremlin').inE('uses').
+      order().by('skill',asc).as('a').
+      outV().as('b').
+      select('a','b').by('skill').by('name') // rank the users of gremlin by their skill level`,
+    ),
+  ).toStrictEqual([
+    `g.V().as('a').
+      properties('location').as('b').
+      hasNot('endTime').as('c').
+      select('a','b','c').by('name').by(value).by('startTime')`,
+    `g.V().has('name','gremlin').inE('uses').
+      order().by('skill',asc).as('a').
+      outV().as('b').
+      select('a','b').by('skill').by('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V(1).out().values('name')
+g.V(1).out().map {it.get().value('name')}
+g.V(1).out().map(values('name'))`,
+    ),
+  ).toStrictEqual([
+    `g.V(1).out().values('name')`,
+    `g.V(1).out().map {it.get().value('name')}`,
+    `g.V(1).out().map(values('name'))`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().filter {it.get().label() == 'person'}
+g.V().filter(label().is('person'))
+g.V().hasLabel('person')`,
+    ),
+  ).toStrictEqual([
+    `g.V().filter {it.get().label() == 'person'}`,
+    `g.V().filter(label().is('person'))`,
+    `g.V().hasLabel('person')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().hasLabel('person').sideEffect(System.out.&println)
+g.V().sideEffect(outE().count().store("o")).
+      sideEffect(inE().count().store("i")).cap("o","i")`,
+    ),
+  ).toStrictEqual([
+    `g.V().hasLabel('person').sideEffect(System.out.&println)`,
+    `g.V().sideEffect(outE().count().store("o")).
+      sideEffect(inE().count().store("i")).cap("o","i")`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().branch {it.get().value('name')}.
+      option('marko', values('age')).
+      option(none, values('name'))
+g.V().branch(values('name')).
+      option('marko', values('age')).
+      option(none, values('name'))
+g.V().choose(has('name','marko'),
+             values('age'),
+             values('name'))`,
+    ),
+  ).toStrictEqual([
+    `g.V().branch {it.get().value('name')}.
+      option('marko', values('age')).
+      option(none, values('name'))`,
+    `g.V().branch(values('name')).
+      option('marko', values('age')).
+      option(none, values('name'))`,
+    `g.V().choose(has('name','marko'),
+             values('age'),
+             values('name'))`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().out('created').hasNext()
+g.V().out('created').next()
+g.V().out('created').next(2)
+g.V().out('nothing').tryNext()
+g.V().out('created').toList()
+g.V().out('created').toSet()
+g.V().out('created').toBulkSet()
+results = ['blah',3]
+g.V().out('created').fill(results)
+g.addV('person').iterate()`,
+    ),
+  ).toStrictEqual([
+    `g.V().out('created').hasNext()`,
+    `g.V().out('created').next()`,
+    `g.V().out('created').next(2)`,
+    `g.V().out('nothing').tryNext()`,
+    `g.V().out('created').toList()`,
+    `g.V().out('created').toSet()`,
+    `g.V().out('created').toBulkSet()`,
+    `g.V().out('created').fill(results)`,
+    `g.addV('person').iterate()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V(1).as('a').out('created').in('created').where(neq('a')).
+  addE('co-developer').from('a').property('year',2009)
+g.V(3,4,5).aggregate('x').has('name','josh').as('a').
+  select('x').unfold().hasLabel('software').addE('createdBy').to('a')
+g.V().as('a').out('created').addE('createdBy').to('a').property('acl','public')
+g.V(1).as('a').out('knows').
+  addE('livesNear').from('a').property('year',2009).
+  inV().inE('livesNear').values('year')
+g.V().match(
+        __.as('a').out('knows').as('b'),
+        __.as('a').out('created').as('c'),
+        __.as('b').out('created').as('c')).
+      addE('friendlyCollaborator').from('a').to('b').
+        property(id,23).property('project',select('c').values('name'))
+g.E(23).valueMap()
+marko = g.V().has('name','marko').next()
+peter = g.V().has('name','peter').next()
+g.V(marko).addE('knows').to(peter)
+g.addE('knows').from(marko).to(peter)`,
+    ),
+  ).toStrictEqual([
+    `g.V(1).as('a').out('created').in('created').where(neq('a')).
+  addE('co-developer').from('a').property('year',2009)`,
+    `g.V(3,4,5).aggregate('x').has('name','josh').as('a').
+  select('x').unfold().hasLabel('software').addE('createdBy').to('a')`,
+    `g.V().as('a').out('created').addE('createdBy').to('a').property('acl','public')`,
+    `g.V(1).as('a').out('knows').
+  addE('livesNear').from('a').property('year',2009).
+  inV().inE('livesNear').values('year')`,
+    `g.V().match(
+        __.as('a').out('knows').as('b'),
+        __.as('a').out('created').as('c'),
+        __.as('b').out('created').as('c')).
+      addE('friendlyCollaborator').from('a').to('b').
+        property(id,23).property('project',select('c').values('name'))`,
+    `g.E(23).valueMap()`,
+    `g.V().has('name','marko').next()`,
+    `g.V().has('name','peter').next()`,
+    `g.V(marko).addE('knows').to(peter)`,
+    `g.addE('knows').from(marko).to(peter)`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.addV('person').property('name','stephen')
+g.V().values('name')
+g.V().outE('knows').addV().property('name','nothing')
+g.V().has('name','nothing')
+g.V().has('name','nothing').bothE()`,
+    ),
+  ).toStrictEqual([
+    `g.addV('person').property('name','stephen')`,
+    `g.V().values('name')`,
+    `g.V().outE('knows').addV().property('name','nothing')`,
+    `g.V().has('name','nothing')`,
+    `g.V().has('name','nothing').bothE()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V(1).property('country','usa')
+g.V(1).property('city','santa fe').property('state','new mexico').valueMap()
+g.V(1).property(list,'age',35)
+g.V(1).valueMap()
+g.V(1).property('friendWeight',outE('knows').values('weight').sum(),'acl','private')
+g.V(1).properties('friendWeight').valueMap()`,
+    ),
+  ).toStrictEqual([
+    `g.V(1).property('country','usa')`,
+    `g.V(1).property('city','santa fe').property('state','new mexico').valueMap()`,
+    `g.V(1).property(list,'age',35)`,
+    `g.V(1).valueMap()`,
+    `g.V(1).property('friendWeight',outE('knows').values('weight').sum(),'acl','private')`,
+    `g.V(1).properties('friendWeight').valueMap()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V(1).out('created')
+g.V(1).out('created').aggregate('x')
+g.V(1).out('created').aggregate(global, 'x')
+g.V(1).out('created').aggregate('x').in('created')
+g.V(1).out('created').aggregate('x').in('created').out('created')
+g.V(1).out('created').aggregate('x').in('created').out('created').
+       where(without('x')).values('name')`,
+    ),
+  ).toStrictEqual([
+    `g.V(1).out('created')`,
+    `g.V(1).out('created').aggregate('x')`,
+    `g.V(1).out('created').aggregate(global, 'x')`,
+    `g.V(1).out('created').aggregate('x').in('created')`,
+    `g.V(1).out('created').aggregate('x').in('created').out('created')`,
+    `g.V(1).out('created').aggregate('x').in('created').out('created').
+       where(without('x')).values('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().out('knows').aggregate('x').cap('x')
+g.V().out('knows').aggregate('x').by('name').cap('x')`,
+    ),
+  ).toStrictEqual([
+    `g.V().out('knows').aggregate('x').cap('x')`,
+    `g.V().out('knows').aggregate('x').by('name').cap('x')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().aggregate(global, 'x').limit(1).cap('x')
+g.V().aggregate(local, 'x').limit(1).cap('x')
+g.withoutStrategies(EarlyLimitStrategy).V().aggregate(local,'x').limit(1).cap('x')`,
+    ),
+  ).toStrictEqual([
+    `g.V().aggregate(global, 'x').limit(1).cap('x')`,
+    `g.V().aggregate(local, 'x').limit(1).cap('x')`,
+    `g.withoutStrategies(EarlyLimitStrategy).V().aggregate(local,'x').limit(1).cap('x')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().as('a').out('created').as('b').select('a','b')
+g.V().as('a').out('created').as('b').select('a','b').by('name')`,
+    ),
+  ).toStrictEqual([
+    `g.V().as('a').out('created').as('b').select('a','b')`,
+    `g.V().as('a').out('created').as('b').select('a','b').by('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().sideEffect{println "first: \${it}"}.sideEffect{println "second: \${it}"}.iterate()
+g.V().sideEffect{println "first: \${it}"}.barrier().sideEffect{println "second: \${it}"}.iterate()`,
+    ),
+  ).toStrictEqual([
+    `g.V().sideEffect{println "first: \${it}"}.sideEffect{println "second: \${it}"}.iterate()`,
+    `g.V().sideEffect{println "first: \${it}"}.barrier().sideEffect{println "second: \${it}"}.iterate()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `graph = TinkerGraph.open()
+g = graph.traversal()
+g.io('data/grateful-dead.xml').read().iterate()
+g = graph.traversal().withoutStrategies(LazyBarrierStrategy)
+clockWithResult(1){g.V().both().both().both().count().next()}
+clockWithResult(1){g.V().repeat(both()).times(3).count().next()}
+clockWithResult(1){g.V().both().barrier().both().barrier().both().barrier().count().next()}`,
+    ),
+  ).toStrictEqual([`g.io('data/grateful-dead.xml').read().iterate()`]);
+
+  expect(
+    extractGremlinQueries(
+      `graph = TinkerGraph.open()
+g = graph.traversal()
+g.io('data/grateful-dead.xml').read().iterate()
+clockWithResult(1){g.V().both().both().both().count().next()}
+g.V().both().both().both().count().iterate().toString()`,
+    ),
+  ).toStrictEqual([
+    `g.io('data/grateful-dead.xml').read().iterate()`,
+    `g.V().both().both().both().count().iterate().toString()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().group().by(bothE().count())
+g.V().group().by(bothE().count()).by('name')
+g.V().group().by(bothE().count()).by(count())`,
+    ),
+  ).toStrictEqual([
+    `g.V().group().by(bothE().count())`,
+    `g.V().group().by(bothE().count()).by('name')`,
+    `g.V().group().by(bothE().count()).by(count())`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().groupCount('a').by(label).cap('a')
+g.V().groupCount('a').by(label).groupCount('b').by(outE().count()).cap('a','b')`,
+    ),
+  ).toStrictEqual([
+    `g.V().groupCount('a').by(label).cap('a')`,
+    `g.V().groupCount('a').by(label).groupCount('b').by(outE().count()).cap('a','b')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().hasLabel('person').
+      choose(values('age').is(lte(30)),
+        __.in(),
+        __.out()).values('name')
+g.V().hasLabel('person').
+      choose(values('age')).
+        option(27, __.in()).
+        option(32, __.out()).values('name')`,
+    ),
+  ).toStrictEqual([
+    `g.V().hasLabel('person').
+      choose(values('age').is(lte(30)),
+        __.in(),
+        __.out()).values('name')`,
+    `g.V().hasLabel('person').
+      choose(values('age')).
+        option(27, __.in()).
+        option(32, __.out()).values('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().choose(hasLabel('person'), out('created')).values('name')
+g.V().choose(hasLabel('person'), out('created'), identity()).values('name')`,
+    ),
+  ).toStrictEqual([
+    `g.V().choose(hasLabel('person'), out('created')).values('name')`,
+    `g.V().choose(hasLabel('person'), out('created'), identity()).values('name')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V(1).coalesce(outE('knows'), outE('created')).inV().path().by('name').by(label)
+g.V(1).coalesce(outE('created'), outE('knows')).inV().path().by('name').by(label)
+g.V(1).property('nickname', 'okram')
+g.V().hasLabel('person').coalesce(values('nickname'), values('name'))`,
+    ),
+  ).toStrictEqual([
+    `g.V(1).coalesce(outE('knows'), outE('created')).inV().path().by('name').by(label)`,
+    `g.V(1).coalesce(outE('created'), outE('knows')).inV().path().by('name').by(label)`,
+    `g.V(1).property('nickname', 'okram')`,
+    `g.V().hasLabel('person').coalesce(values('nickname'), values('name'))`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().coin(0.5)
+g.V().coin(0.0)
+g.V().coin(1.0)`,
+    ),
+  ).toStrictEqual([`g.V().coin(0.5)`, `g.V().coin(0.0)`, `g.V().coin(1.0)`]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().
+  connectedComponent().
+    with(ConnectedComponent.propertyName, 'component').
+  project('name','component').
+    by('name').
+    by('component')
+g.V().hasLabel('person').
+  connectedComponent().
+    with(ConnectedComponent.propertyName, 'component').
+    with(ConnectedComponent.edges, outE('knows')).
+  project('name','component').
+    by('name').
+    by('component')`,
+    ),
+  ).toStrictEqual([
+    `g.V().
+  connectedComponent().
+    with(ConnectedComponent.propertyName, 'component').
+  project('name','component').
+    by('name').
+    by('component')`,
+    `g.V().hasLabel('person').
+  connectedComponent().
+    with(ConnectedComponent.propertyName, 'component').
+    with(ConnectedComponent.edges, outE('knows')).
+  project('name','component').
+    by('name').
+    by('component')`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().choose(hasLabel('person'),
+    values('name'),
+    constant('inhuman'))
+g.V().coalesce(
+    hasLabel('person').values('name'),
+    constant('inhuman'))`,
+    ),
+  ).toStrictEqual([
+    `g.V().choose(hasLabel('person'),
+    values('name'),
+    constant('inhuman'))`,
+    `g.V().coalesce(
+    hasLabel('person').values('name'),
+    constant('inhuman'))`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V().count()
+g.V().hasLabel('person').count()
+g.V().hasLabel('person').outE('created').count().path()
+g.V().hasLabel('person').outE('created').count().map {it.get() * 10}.path()`,
+    ),
+  ).toStrictEqual([
+    `g.V().count()`,
+    `g.V().hasLabel('person').count()`,
+    `g.V().hasLabel('person').outE('created').count().path()`,
+    `g.V().hasLabel('person').outE('created').count().map {it.get() * 10}.path()`,
+  ]);
+
+  expect(
+    extractGremlinQueries(
+      `g.V(1).both().both()
+g.V(1).both().both().cyclicPath()
+g.V(1).both().both().cyclicPath().path()
+g.V(1).as('a').out('created').as('b').
+  in('created').as('c').
+  cyclicPath().
+  path()
+g.V(1).as('a').out('created').as('b').
+  in('created').as('c').
+  cyclicPath().from('a').to('b').
+  path()`,
+    ),
+  ).toStrictEqual([
+    `g.V(1).both().both()`,
+    `g.V(1).both().both().cyclicPath()`,
+    `g.V(1).both().both().cyclicPath().path()`,
+    `g.V(1).as('a').out('created').as('b').
+  in('created').as('c').
+  cyclicPath().
+  path()`,
+    `g.V(1).as('a').out('created').as('b').
+  in('created').as('c').
+  cyclicPath().from('a').to('b').
+  path()`,
+  ]);
+});
diff --git a/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/extractGremlinQueries.ts b/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/extractGremlinQueries.ts
new file mode 100644
index 0000000..ab00746
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/extractGremlinQueries.ts
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+const LEFT_WHITE_PARENTHESIS = '⦅';
+const RIGHT_WHITE_PARENTHESIS = '⦆';
+const LEFT_WHITE_SQUARE_BRACKET = '⟦';
+const RIGHT_WHITE_SQUARE_BRACKET = '⟧';
+const LEFT_WHITE_CURLY_BRACKET = '⦃';
+const RIGHT_WHITE_CURLY_BRACKET = '⦄';
+const WHITE_DOT = '。';
+
+const encodeAllNestedBracketsAndDots = (code: string): string => {
+  const { word } = code.split('').reduce(
+    (state, char) => {
+      if (char === '.') {
+        return {
+          ...state,
+          word:
+            !state.isInsideSingleQuoteString &&
+            !state.parenthesesCount &&
+            !state.squareBracketsCount &&
+            !state.curlyBracketsCount
+              ? state.word + '.'
+              : state.word + WHITE_DOT,
+        };
+      }
+      if (char === '(') {
+        return {
+          ...state,
+          parenthesesCount: state.parenthesesCount + (state.isInsideSingleQuoteString ? 0 : 1),
+          word:
+            !state.isInsideSingleQuoteString &&
+            !state.parenthesesCount &&
+            !state.squareBracketsCount &&
+            !state.curlyBracketsCount
+              ? state.word + '('
+              : state.word + LEFT_WHITE_PARENTHESIS,
+        };
+      }
+      if (char === '[') {
+        return {
+          ...state,
+          squareBracketsCount: state.squareBracketsCount + (state.isInsideSingleQuoteString ? 0 : 1),
+          word:
+            !state.isInsideSingleQuoteString &&
+            !state.parenthesesCount &&
+            !state.squareBracketsCount &&
+            !state.curlyBracketsCount
+              ? state.word + '['
+              : state.word + LEFT_WHITE_SQUARE_BRACKET,
+        };
+      }
+      if (char === '{') {
+        return {
+          ...state,
+          curlyBracketsCount: state.curlyBracketsCount + (state.isInsideSingleQuoteString ? 0 : 1),
+          word:
+            !state.isInsideSingleQuoteString &&
+            !state.parenthesesCount &&
+            !state.squareBracketsCount &&
+            !state.curlyBracketsCount
+              ? state.word + '{'
+              : state.word + LEFT_WHITE_CURLY_BRACKET,
+        };
+      }
+      if (char === ')') {
+        return {
+          ...state,
+          parenthesesCount: state.parenthesesCount - (state.isInsideSingleQuoteString ? 0 : 1),
+          word:
+            !state.isInsideSingleQuoteString &&
+            state.parenthesesCount === 1 &&
+            !state.squareBracketsCount &&
+            !state.curlyBracketsCount
+              ? state.word + ')'
+              : state.word + RIGHT_WHITE_PARENTHESIS,
+        };
+      }
+      if (char === ']') {
+        return {
+          ...state,
+          squareBracketsCount: state.squareBracketsCount - (state.isInsideSingleQuoteString ? 0 : 1),
+          word:
+            !state.isInsideSingleQuoteString &&
+            !state.parenthesesCount &&
+            state.squareBracketsCount === 1 &&
+            !state.curlyBracketsCount
+              ? state.word + ']'
+              : state.word + RIGHT_WHITE_SQUARE_BRACKET,
+        };
+      }
+      if (char === '}') {
+        return {
+          ...state,
+          curlyBracketsCount: state.curlyBracketsCount - (state.isInsideSingleQuoteString ? 0 : 1),
+          word:
+            !state.isInsideSingleQuoteString &&
+            !state.parenthesesCount &&
+            !state.squareBracketsCount &&
+            state.curlyBracketsCount === 1
+              ? state.word + '}'
+              : state.word + RIGHT_WHITE_CURLY_BRACKET,
+        };
+      }
+      if (char === "'") {
+        return {
+          ...state,
+          isInsideSingleQuoteString: !state.isInsideSingleQuoteString,
+          word: state.word + "'",
+        };
+      }
+      return {
+        ...state,
+        word: state.word + char,
+      };
+    },
+    { word: '', parenthesesCount: 0, squareBracketsCount: 0, curlyBracketsCount: 0, isInsideSingleQuoteString: false },
+  );
+  return word;
+};
+
+const decodeEncodedBracketsAndDots = (code: string) => {
+  return code
+    .split(WHITE_DOT)
+    .join('.')
+    .split(LEFT_WHITE_PARENTHESIS)
+    .join('(')
+    .split(RIGHT_WHITE_PARENTHESIS)
+    .join(')')
+    .split(LEFT_WHITE_SQUARE_BRACKET)
+    .join('[')
+    .split(RIGHT_WHITE_SQUARE_BRACKET)
+    .join(']')
+    .split(LEFT_WHITE_CURLY_BRACKET)
+    .join('{')
+    .split(RIGHT_WHITE_CURLY_BRACKET)
+    .join('}');
+};
+
+const SPACE = `\\s`;
+const HORIZONTAL_SPACE = `[^\\S\\r\\n]`;
+const DOT = `\\.`;
+const METHOD_STEP = `\\w+${HORIZONTAL_SPACE}*\\([^\\)]*\\)`;
+const CLOSURE_STEP = `\\w+${HORIZONTAL_SPACE}*\\{[^\\}]*\\}`;
+const WORD_STEP = `\\w+`;
+const GREMLIN_STEP = `(${METHOD_STEP}|${CLOSURE_STEP}|${WORD_STEP})`;
+const STEP_CONNECTOR = `(${SPACE}*${DOT}${SPACE}*)`;
+const GREMLIN_QUERY = `g(${STEP_CONNECTOR}${GREMLIN_STEP})+`;
+
+const gremlinQueryRegExp = new RegExp(GREMLIN_QUERY, 'g');
+
+export const extractGremlinQueries = (code: string) => {
+  const encodedCode = encodeAllNestedBracketsAndDots(code);
+  const gremlinQueries = encodedCode.match(gremlinQueryRegExp);
+  if (!gremlinQueries) return [];
+  return gremlinQueries.map(decodeEncodedBracketsAndDots);
+};
diff --git a/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/index.ts b/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/index.ts
new file mode 100644
index 0000000..ad5f8a2
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/parseToSyntaxTrees/index.ts
@@ -0,0 +1,391 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  UnformattedClosureCodeBlock,
+  TokenType,
+  UnformattedSyntaxTree,
+  UnformattedNonGremlinSyntaxTree,
+} from '../types';
+import { last, neq, pipe } from '../utils';
+import { extractGremlinQueries } from './extractGremlinQueries';
+
+const tokenizeOnTopLevelPunctuation = (query: string): string[] => {
+  let word = '';
+  let parenthesesCount = 0;
+  let squareBracketCount = 0;
+  let curlyBracketCount = 0;
+  let isInsideSingleQuoteString = false;
+  query.split('').forEach((char) => {
+    if (char === '(' && !isInsideSingleQuoteString) {
+      parenthesesCount++;
+      word += '(';
+      return;
+    }
+    if (char === '[' && !isInsideSingleQuoteString) {
+      squareBracketCount++;
+      word += '[';
+      return;
+    }
+    if (char === '{' && !isInsideSingleQuoteString) {
+      curlyBracketCount++;
+      word += '{';
+      return;
+    }
+    if (char === ')' && !isInsideSingleQuoteString) {
+      parenthesesCount--;
+      word += ')';
+      return;
+    }
+    if (char === ']' && !isInsideSingleQuoteString) {
+      squareBracketCount--;
+      word += ']';
+      return;
+    }
+    if (char === '}' && !isInsideSingleQuoteString) {
+      curlyBracketCount--;
+      word += '}';
+      return;
+    }
+    if (char === "'") {
+      isInsideSingleQuoteString = !isInsideSingleQuoteString;
+      word += "'";
+      return;
+    }
+    if (char === '.') {
+      word +=
+        isInsideSingleQuoteString || parenthesesCount || squareBracketCount || curlyBracketCount
+          ? '.'
+          : String.fromCharCode(28);
+      return;
+    }
+    word += char;
+  });
+  return word
+    .split(String.fromCharCode(28))
+    .filter((token) => token !== '')
+    .map((token) => token.trim());
+};
+
+const tokenizeOnTopLevelComma = (query: string): string[] => {
+  let word = '';
+  let parenthesesCount = 0;
+  let squareBracketsCount = 0;
+  let curlyBracketsCount = 0;
+  let isInsideSingleQuoteString = false;
+  query.split('').forEach((char) => {
+    if (char === '(' && !isInsideSingleQuoteString) {
+      parenthesesCount++;
+      word += '(';
+      return;
+    }
+    if (char === '[' && !isInsideSingleQuoteString) {
+      squareBracketsCount++;
+      word += '[';
+      return;
+    }
+    if (char === '{' && !isInsideSingleQuoteString) {
+      curlyBracketsCount++;
+      word += '{';
+      return;
+    }
+    if (char === ')' && !isInsideSingleQuoteString) {
+      parenthesesCount--;
+      word += ')';
+      return;
+    }
+    if (char === ']' && !isInsideSingleQuoteString) {
+      squareBracketsCount--;
+      word += ']';
+      return;
+    }
+    if (char === '}' && !isInsideSingleQuoteString) {
+      curlyBracketsCount--;
+      word += '}';
+      return;
+    }
+    if (char === "'") {
+      isInsideSingleQuoteString = !isInsideSingleQuoteString;
+      word += "'";
+      return;
+    }
+    if (char === ',') {
+      word +=
+        isInsideSingleQuoteString || parenthesesCount || squareBracketsCount || curlyBracketsCount
+          ? ','
+          : String.fromCharCode(28);
+      return;
+    }
+    word += char;
+  });
+  return word
+    .split(String.fromCharCode(28))
+    .filter((token) => token !== '')
+    .map((token) => token.trim());
+};
+
+const tokenizeOnTopLevelParentheses = (query: string): string[] => {
+  let word = '';
+  let parenthesesCount = 0;
+  let squareBracketsCount = 0;
+  let curlyBracketsCount = 0;
+  let isInsideSingleQuoteString = false;
+  query.split('').forEach((char) => {
+    if (char === '(' && !isInsideSingleQuoteString) {
+      if (parenthesesCount === 0) {
+        word += String.fromCharCode(28);
+      }
+      parenthesesCount++;
+      word += '(';
+      return;
+    }
+    if (char === '[' && !isInsideSingleQuoteString) {
+      squareBracketsCount++;
+      word += '[';
+      return;
+    }
+    if (char === '{' && !isInsideSingleQuoteString) {
+      curlyBracketsCount++;
+      word += '{';
+      return;
+    }
+    if (char === ')' && !isInsideSingleQuoteString) {
+      parenthesesCount--;
+      word += ')';
+      return;
+    }
+    if (char === ']' && !isInsideSingleQuoteString) {
+      squareBracketsCount--;
+      word += ']';
+      return;
+    }
+    if (char === '}' && !isInsideSingleQuoteString) {
+      curlyBracketsCount--;
+      word += '}';
+      return;
+    }
+    if (char === "'") {
+      isInsideSingleQuoteString = !isInsideSingleQuoteString;
+      word += "'";
+      return;
+    }
+    word += char;
+  });
+  return word
+    .split(String.fromCharCode(28))
+    .filter((token) => token !== '')
+    .map((token) => token.trim());
+};
+
+const tokenizeOnTopLevelCurlyBrackets = (query: string): string[] => {
+  let word = '';
+  let parenthesesCount = 0;
+  let squareBracketsCount = 0;
+  let curlyBracketsCount = 0;
+  let isInsideSingleQuoteString = false;
+  query.split('').forEach((char) => {
+    if (char === '(' && !isInsideSingleQuoteString) {
+      parenthesesCount++;
+      word += '(';
+      return;
+    }
+    if (char === '[' && !isInsideSingleQuoteString) {
+      squareBracketsCount++;
+      word += '[';
+      return;
+    }
+    if (char === '{' && !isInsideSingleQuoteString) {
+      if (curlyBracketsCount === 0) {
+        word += String.fromCharCode(28);
+      }
+      curlyBracketsCount++;
+      word += '{';
+      return;
+    }
+    if (char === ')' && !isInsideSingleQuoteString) {
+      parenthesesCount--;
+      word += ')';
+      return;
+    }
+    if (char === ']' && !isInsideSingleQuoteString) {
+      squareBracketsCount--;
+      word += ']';
+      return;
+    }
+    if (char === '}' && !isInsideSingleQuoteString) {
+      curlyBracketsCount--;
+      word += '}';
+      return;
+    }
+    if (char === "'") {
+      isInsideSingleQuoteString = !isInsideSingleQuoteString;
+      word += "'";
+      return;
+    }
+    word += char;
+  });
+  return word
+    .split(String.fromCharCode(28))
+    .filter((token) => token !== '')
+    .map((token) => token.trim());
+};
+
+const isWrappedInParentheses = (token: string): boolean => {
+  if (token.length < 2) return false;
+  if (token.charAt(0) !== '(') return false;
+  if (token.slice(-1) !== ')') return false;
+  return true;
+};
+
+const isWrappedInCurlyBrackets = (token: string): boolean => {
+  if (token.length < 2) return false;
+  if (token.charAt(0) !== '{') return false;
+  if (token.slice(-1) !== '}') return false;
+  return true;
+};
+
+const isString = (token: string): boolean => {
+  if (token.length < 2) return false;
+  if (token.charAt(0) !== token.substr(-1)) return false;
+  if (['"', "'"].includes(token.charAt(0))) return true;
+  return false;
+};
+
+const isMethodInvocation = (token: string): boolean => {
+  return pipe(tokenizeOnTopLevelParentheses, last, isWrappedInParentheses)(token);
+};
+
+const isClosureInvocation = (token: string): boolean => {
+  return pipe(tokenizeOnTopLevelCurlyBrackets, last, isWrappedInCurlyBrackets)(token);
+};
+
+const trimParentheses = (expression: string): string => expression.slice(1, -1);
+
+const trimCurlyBrackets = (expression: string): string => expression.slice(1, -1);
+
+const getMethodTokenAndArgumentTokensFromMethodInvocation = (
+  token: string,
+): { methodToken: string; argumentTokens: string[] } => {
+  // The word before the first parenthesis is the method name
+  // The token may be a double application of a curried function, so we cannot
+  // assume that the first opening parenthesis is closed by the last closing
+  // parenthesis
+  const tokens = tokenizeOnTopLevelParentheses(token);
+  return {
+    methodToken: tokens.slice(0, -1).join(''),
+    argumentTokens: pipe(trimParentheses, tokenizeOnTopLevelComma)(tokens.slice(-1)[0]),
+  };
+};
+
+const getIndentation = (lineOfCode: string): number => lineOfCode.split('').findIndex(neq(' '));
+
+const getMethodTokenAndClosureCodeBlockFromClosureInvocation = (
+  token: string,
+  fullQuery: string,
+): { methodToken: string; closureCodeBlock: UnformattedClosureCodeBlock } => {
+  // The word before the first curly bracket is the method name
+  // The token may be a double application of a curried function, so we cannot
+  // assume that the first opening curly bracket is closed by the last closing
+  // curly bracket
+  const tokens = tokenizeOnTopLevelCurlyBrackets(token);
+  const methodToken = tokens.slice(0, -1).join('');
+  const closureCodeBlockToken = trimCurlyBrackets(tokens.slice(-1)[0]);
+  const initialClosureCodeBlockIndentation = fullQuery
+    .substr(0, fullQuery.indexOf(closureCodeBlockToken))
+    .split('\n')
+    .slice(-1)[0].length;
+  return {
+    methodToken,
+    closureCodeBlock: trimCurlyBrackets(tokens.slice(-1)[0])
+      .split('\n')
+      .map((lineOfCode, i) => ({
+        lineOfCode: lineOfCode.trimStart(),
+        relativeIndentation:
+          i === 0 ? getIndentation(lineOfCode) : getIndentation(lineOfCode) - initialClosureCodeBlockIndentation,
+      })),
+  };
+};
+
+const parseCodeBlockToSyntaxTree = (fullCode: string, shouldCalculateInitialHorizontalPosition?: boolean) => (
+  codeBlock: string,
+): UnformattedSyntaxTree => {
+  const tokens = tokenizeOnTopLevelPunctuation(codeBlock);
+  if (tokens.length === 1) {
+    const token = tokens[0];
+    if (isMethodInvocation(token)) {
+      const { methodToken, argumentTokens } = getMethodTokenAndArgumentTokensFromMethodInvocation(token);
+      return {
+        type: TokenType.Method,
+        method: parseCodeBlockToSyntaxTree(fullCode)(methodToken),
+        arguments: argumentTokens.map(parseCodeBlockToSyntaxTree(fullCode)),
+      };
+    }
+    if (isClosureInvocation(token)) {
+      const { methodToken, closureCodeBlock } = getMethodTokenAndClosureCodeBlockFromClosureInvocation(token, fullCode);
+      return {
+        type: TokenType.Closure,
+        method: parseCodeBlockToSyntaxTree(fullCode)(methodToken),
+        closureCodeBlock,
+      };
+    }
+    if (isString(token)) {
+      return {
+        type: TokenType.String,
+        string: token,
+      };
+    }
+    return {
+      type: TokenType.Word,
+      word: token,
+    };
+  }
+  return {
+    type: TokenType.Traversal,
+    steps: tokens.map(parseCodeBlockToSyntaxTree(fullCode)),
+    initialHorizontalPosition: shouldCalculateInitialHorizontalPosition
+      ? fullCode.substr(0, fullCode.indexOf(codeBlock)).split('\n').slice(-1)[0].length
+      : 0,
+  };
+};
+
+export const parseNonGremlinCodeToSyntaxTree = (code: string): UnformattedNonGremlinSyntaxTree => ({
+  type: TokenType.NonGremlinCode,
+  code,
+});
+
+export const parseToSyntaxTrees = (code: string): UnformattedSyntaxTree[] => {
+  const queries = extractGremlinQueries(code);
+  const { syntaxTrees, remainingCode } = queries.reduce(
+    (state, query: string): { syntaxTrees: UnformattedSyntaxTree[]; remainingCode: string } => {
+      const indexOfQuery = state.remainingCode.indexOf(query);
+      const nonGremlinCode = state.remainingCode.substr(0, indexOfQuery);
+      return {
+        syntaxTrees: [
+          ...state.syntaxTrees,
+          parseNonGremlinCodeToSyntaxTree(nonGremlinCode),
+          parseCodeBlockToSyntaxTree(code, true)(query),
+        ],
+        remainingCode: state.remainingCode.substr(indexOfQuery + query.length),
+      };
+    },
+    { syntaxTrees: [] as UnformattedSyntaxTree[], remainingCode: code },
+  );
+  if (!remainingCode) return syntaxTrees;
+  return [...syntaxTrees, parseNonGremlinCodeToSyntaxTree(remainingCode)];
+};
diff --git a/gremlint/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts b/gremlint/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts
new file mode 100644
index 0000000..9075d7e
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  TokenType,
+  UnformattedClosureSyntaxTree,
+  UnformattedMethodSyntaxTree,
+  UnformattedNonGremlinSyntaxTree,
+  UnformattedStringSyntaxTree,
+  UnformattedTraversalSyntaxTree,
+  UnformattedWordSyntaxTree,
+} from './types';
+import { last, spaces } from './utils';
+
+type GremlinOnelinerSyntaxTree =
+  | UnformattedNonGremlinSyntaxTree
+  | Pick<UnformattedTraversalSyntaxTree, 'type' | 'steps'>
+  | Pick<UnformattedMethodSyntaxTree, 'type' | 'method' | 'arguments'>
+  | Pick<UnformattedClosureSyntaxTree, 'type' | 'method' | 'closureCodeBlock'>
+  | Pick<UnformattedStringSyntaxTree, 'type' | 'string'>
+  | Pick<UnformattedWordSyntaxTree, 'type' | 'word'>;
+
+const recreateQueryOnelinerFromSyntaxTree = (localIndentation: number = 0) => (
+  syntaxTree: GremlinOnelinerSyntaxTree,
+): string => {
+  switch (syntaxTree.type) {
+    // This case will never occur
+    case TokenType.NonGremlinCode:
+      return syntaxTree.code;
+    case TokenType.Traversal:
+      return spaces(localIndentation) + syntaxTree.steps.map(recreateQueryOnelinerFromSyntaxTree()).join('.');
+    case TokenType.Method:
+      return (
+        spaces(localIndentation) +
+        recreateQueryOnelinerFromSyntaxTree()(syntaxTree.method) +
+        '(' +
+        syntaxTree.arguments.map(recreateQueryOnelinerFromSyntaxTree()).join(', ') +
+        ')'
+      );
+    case TokenType.Closure:
+      return (
+        spaces(localIndentation) +
+        recreateQueryOnelinerFromSyntaxTree()(syntaxTree.method) +
+        '{' +
+        last(
+          syntaxTree.closureCodeBlock.map(
+            ({ lineOfCode, relativeIndentation }) => `${spaces(Math.max(relativeIndentation, 0))}${lineOfCode}`,
+          ),
+        ) +
+        '}'
+      );
+    case TokenType.String:
+      return spaces(localIndentation) + syntaxTree.string;
+    case TokenType.Word:
+      return spaces(localIndentation) + syntaxTree.word;
+  }
+};
+
+export default recreateQueryOnelinerFromSyntaxTree;
diff --git a/gremlint/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts b/gremlint/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts
new file mode 100644
index 0000000..e7d9f6a
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FormattedSyntaxTree, GremlintInternalConfig, TokenType } from './types';
+import { eq, spaces } from './utils';
+
+const recreateQueryStringFromFormattedSyntaxTree = (syntaxTree: FormattedSyntaxTree): string => {
+  if (syntaxTree.type === TokenType.NonGremlinCode) {
+    return syntaxTree.code;
+  }
+  if (syntaxTree.type === TokenType.Traversal) {
+    return syntaxTree.stepGroups
+      .map((stepGroup) => stepGroup.steps.map(recreateQueryStringFromFormattedSyntaxTree).join('.'))
+      .join('\n');
+  }
+  if (syntaxTree.type === TokenType.Method) {
+    return (
+      (syntaxTree.shouldStartWithDot ? '.' : '') +
+      [
+        recreateQueryStringFromFormattedSyntaxTree(syntaxTree.method) + '(',
+        syntaxTree.argumentGroups
+          .map((args) => args.map(recreateQueryStringFromFormattedSyntaxTree).join(', '))
+          .join(',\n') +
+          ')' +
+          (syntaxTree.shouldEndWithDot ? '.' : ''),
+      ].join(syntaxTree.argumentsShouldStartOnNewLine ? '\n' : '')
+    );
+  }
+  if (syntaxTree.type === TokenType.Closure) {
+    return (
+      (syntaxTree.shouldStartWithDot ? '.' : '') +
+      recreateQueryStringFromFormattedSyntaxTree(syntaxTree.method) +
+      '{' +
+      syntaxTree.closureCodeBlock
+        .map(({ lineOfCode, localIndentation }, i) => `${spaces(localIndentation)}${lineOfCode}`)
+        .join('\n') +
+      '}' +
+      (syntaxTree.shouldEndWithDot ? '.' : '')
+    );
+  }
+  if (syntaxTree.type === TokenType.String) {
+    return spaces(syntaxTree.localIndentation) + syntaxTree.string;
+  }
+  if (syntaxTree.type === TokenType.Word) {
+    return (
+      spaces(syntaxTree.localIndentation) +
+      (syntaxTree.shouldStartWithDot ? '.' : '') +
+      syntaxTree.word +
+      (syntaxTree.shouldEndWithDot ? '.' : '')
+    );
+  }
+  // The following line is just here to convince TypeScript that the return type
+  // is string and not string | undefined.
+  return '';
+};
+
+const withIndentationIfNotEmpty = (indentation: number) => (lineOfCode: string): string => {
+  if (!lineOfCode) return lineOfCode;
+  return spaces(indentation) + lineOfCode;
+};
+
+const lineIsEmpty = (lineOfCode: string): boolean => {
+  return lineOfCode.split('').every(eq(' '));
+};
+
+const removeIndentationFromEmptyLines = (lineOfCode: string): string => {
+  if (lineIsEmpty(lineOfCode)) return '';
+  return lineOfCode;
+};
+
+export const recreateQueryStringFromFormattedSyntaxTrees = ({ globalIndentation }: GremlintInternalConfig) => (
+  syntaxTrees: FormattedSyntaxTree[],
+): string => {
+  return syntaxTrees
+    .map(recreateQueryStringFromFormattedSyntaxTree)
+    .join('')
+    .split('\n')
+    .map(withIndentationIfNotEmpty(globalIndentation))
+    .map(removeIndentationFromEmptyLines)
+    .join('\n');
+};
diff --git a/gremlint/gremlint/src/formatQuery/types.ts b/gremlint/gremlint/src/formatQuery/types.ts
new file mode 100644
index 0000000..f85e91a
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/types.ts
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type GremlintUserConfig = {
+  indentation: number;
+  maxLineLength: number;
+  shouldPlaceDotsAfterLineBreaks: boolean;
+};
+
+export type GremlintInternalConfig = {
+  globalIndentation: number;
+  localIndentation: number;
+  maxLineLength: number;
+  shouldPlaceDotsAfterLineBreaks: boolean;
+  shouldStartWithDot: boolean;
+  shouldEndWithDot: boolean;
+  horizontalPosition: number; // Will be used by child syntax trees and is the horizontal position its child content starts, so a non-indented hasLabel(...) has a horizontal position of 9
+};
+
+export enum TokenType {
+  NonGremlinCode = 'NON_GREMLIN_CODE',
+  Traversal = 'TRAVERSAL',
+  Method = 'METHOD',
+  Closure = 'CLOSURE',
+  String = 'STRING',
+  Word = 'WORD',
+}
+
+export type UnformattedNonGremlinSyntaxTree = {
+  type: TokenType.NonGremlinCode;
+  code: string;
+};
+
+export type UnformattedTraversalSyntaxTree = {
+  type: TokenType.Traversal;
+  steps: UnformattedSyntaxTree[];
+  // Initial horizontal position of the first line of the query. This is needed in order to be able to preserve relative
+  // indentation between lines inside a non-Gremlin code block that starts on the first line of the query.
+  initialHorizontalPosition: number;
+};
+
+export type UnformattedMethodSyntaxTree = {
+  type: TokenType.Method;
+  method: UnformattedSyntaxTree;
+  arguments: UnformattedSyntaxTree[];
+};
+
+export type UnformattedClosureLineOfCode = {
+  lineOfCode: string;
+  // Relative indentation compared to the opening curly bracket, so relativeIndentation of In {it.get} is 0.
+  relativeIndentation: number;
+};
+
+export type UnformattedClosureCodeBlock = UnformattedClosureLineOfCode[];
+
+export type UnformattedClosureSyntaxTree = {
+  type: TokenType.Closure;
+  method: UnformattedSyntaxTree;
+  closureCodeBlock: UnformattedClosureCodeBlock;
+};
+
+export type UnformattedStringSyntaxTree = {
+  type: TokenType.String;
+  string: string;
+};
+
+export type UnformattedWordSyntaxTree = {
+  type: TokenType.Word;
+  word: string;
+};
+
+export type UnformattedSyntaxTree =
+  | UnformattedMethodSyntaxTree
+  | UnformattedClosureSyntaxTree
+  | UnformattedStringSyntaxTree
+  | UnformattedWordSyntaxTree
+  | UnformattedTraversalSyntaxTree
+  | UnformattedNonGremlinSyntaxTree;
+
+export type FormattedNonGremlinSyntaxTree = UnformattedNonGremlinSyntaxTree & {
+  width: number;
+};
+
+export type GremlinStepGroup = {
+  steps: FormattedSyntaxTree[];
+};
+
+export type FormattedTraversalSyntaxTree = {
+  type: TokenType.Traversal;
+  steps: UnformattedSyntaxTree[];
+  stepGroups: GremlinStepGroup[];
+  initialHorizontalPosition: number;
+  localIndentation: number;
+  width: number;
+};
+
+export type FormattedMethodSyntaxTree = {
+  type: TokenType.Method;
+  method: FormattedSyntaxTree;
+  arguments: UnformattedSyntaxTree[];
+  argumentGroups: FormattedSyntaxTree[][];
+  argumentsShouldStartOnNewLine: boolean;
+  localIndentation: number;
+  width: number;
+  shouldStartWithDot: boolean;
+  shouldEndWithDot: boolean;
+};
+
+type FormattedClosureLineOfCode = {
+  lineOfCode: string;
+  relativeIndentation: number;
+  localIndentation: number;
+};
+
+type FormattedClosureCodeBlock = FormattedClosureLineOfCode[];
+
+export type FormattedClosureSyntaxTree = {
+  type: TokenType.Closure;
+  method: FormattedSyntaxTree;
+  closureCodeBlock: FormattedClosureCodeBlock;
+  localIndentation: number;
+  width: number;
+  shouldStartWithDot: boolean;
+  shouldEndWithDot: boolean;
+};
+
+export type FormattedStringSyntaxTree = {
+  type: TokenType.String;
+  string: string;
+  width: number;
+  localIndentation: number;
+};
+
+export type FormattedWordSyntaxTree = {
+  type: TokenType.Word;
+  word: string;
+  localIndentation: number;
+  width: number;
+  shouldStartWithDot: boolean;
+  shouldEndWithDot: boolean;
+};
+
+export type FormattedSyntaxTree =
+  | FormattedTraversalSyntaxTree
+  | FormattedMethodSyntaxTree
+  | FormattedClosureSyntaxTree
+  | FormattedStringSyntaxTree
+  | FormattedWordSyntaxTree
+  | FormattedNonGremlinSyntaxTree;
+
+export type GremlinSyntaxTreeFormatter = (
+  config: GremlintInternalConfig,
+) => (syntaxTree: UnformattedSyntaxTree) => FormattedSyntaxTree;
diff --git a/gremlint/gremlint/src/formatQuery/utils.ts b/gremlint/gremlint/src/formatQuery/utils.ts
new file mode 100644
index 0000000..4651759
--- /dev/null
+++ b/gremlint/gremlint/src/formatQuery/utils.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const last = <T>(array: T[]): T | undefined => array.slice(-1)[0];
+
+export const pipe = (...fns: ((value: any) => any)[]) => (value: any) => fns.reduce((value, fn) => fn(value), value);
+
+export const spaces = (numberOfSpaces: number): string => Array(numberOfSpaces + 1).join(' ');
+
+export const eq = (a: unknown) => (b: unknown): boolean => a === b;
+
+export const neq = (a: unknown) => (b: unknown): boolean => a !== b;
+
+export const sum = (a: number, b: number): number => a + b;
+
+export const count = (array: any): number => array?.length ?? 0;
+
+export const choose = (
+  getCondition: (...params: any[]) => any,
+  getThen: (...params: any[]) => any,
+  getElse: (...params: any[]) => any,
+) => (...params: any[]) => {
+  return getCondition(...params) ? getThen(...params) : getElse(...params);
+};
diff --git a/gremlint/gremlint/src/index.ts b/gremlint/gremlint/src/index.ts
new file mode 100644
index 0000000..26da959
--- /dev/null
+++ b/gremlint/gremlint/src/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { formatQuery } from './formatQuery';
diff --git a/gremlint/gremlint/tsconfig.json b/gremlint/gremlint/tsconfig.json
new file mode 100644
index 0000000..797ad94
--- /dev/null
+++ b/gremlint/gremlint/tsconfig.json
@@ -0,0 +1,12 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "commonjs",
+    "declaration": true,
+    "outDir": "./lib",
+    "strict": true,
+    "lib": ["DOM", "ES2017"]
+  },
+  "include": ["src"],
+  "exclude": ["node_modules", "**/__tests__/*"]
+}
diff --git a/gremlint/gremlint/tslint.json b/gremlint/gremlint/tslint.json
new file mode 100644
index 0000000..5761be8
--- /dev/null
+++ b/gremlint/gremlint/tslint.json
@@ -0,0 +1,6 @@
+{
+  "extends": ["tslint:recommended", "tslint-config-prettier"],
+  "rules": {
+    "no-shadowed-variable": false
+  }
+}