diff --git a/lerna.json b/lerna.json
index 72b3c6ee01..d5ca80a629 100644
--- a/lerna.json
+++ b/lerna.json
@@ -10,6 +10,7 @@
"tools/scripts/build",
"tools/scripts/formatter",
"tools/scripts/formatter/fcc-md-to-gfm",
+ "tools/formatter",
"tools/challenge-helper-scripts"
],
"version": "independent"
diff --git a/tools/formatter/package-lock.json b/tools/formatter/package-lock.json
new file mode 100644
index 0000000000..6386003bf9
--- /dev/null
+++ b/tools/formatter/package-lock.json
@@ -0,0 +1,393 @@
+{
+ "name": "fcc-formatter",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@types/unist": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
+ "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "bail": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
+ "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ=="
+ },
+ "ccount": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz",
+ "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw=="
+ },
+ "character-entities": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="
+ },
+ "character-entities-html4": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz",
+ "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g=="
+ },
+ "character-entities-legacy": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="
+ },
+ "character-reference-invalid": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
+ },
+ "collapse-white-space": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz",
+ "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ=="
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "fault": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
+ "requires": {
+ "format": "^0.2.0"
+ }
+ },
+ "format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs="
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "is-alphabetical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="
+ },
+ "is-alphanumeric": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz",
+ "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ="
+ },
+ "is-alphanumerical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+ "requires": {
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
+ },
+ "is-decimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
+ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="
+ },
+ "is-hexadecimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
+ },
+ "is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="
+ },
+ "is-whitespace-character": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
+ "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w=="
+ },
+ "is-word-character": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz",
+ "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA=="
+ },
+ "js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.20",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
+ },
+ "longest-streak": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz",
+ "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="
+ },
+ "markdown-escapes": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
+ "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
+ },
+ "markdown-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
+ "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==",
+ "requires": {
+ "repeat-string": "^1.0.0"
+ }
+ },
+ "mdast-builder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/mdast-builder/-/mdast-builder-1.1.1.tgz",
+ "integrity": "sha512-a3KBk/LmYD6wKsWi8WJrGU/rXR4yuF4Men0JO0z6dSZCm5FrXXWTRDjqK0vGSqa+1M6p9edeuypZAZAzSehTUw==",
+ "requires": {
+ "@types/unist": "^2.0.3"
+ }
+ },
+ "mdast-util-compact": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz",
+ "integrity": "sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==",
+ "requires": {
+ "unist-util-visit": "^2.0.0"
+ }
+ },
+ "parse-entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
+ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "requires": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ }
+ },
+ "remark-frontmatter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-2.0.0.tgz",
+ "integrity": "sha512-uNOQt4tO14qBFWXenF0MLC4cqo3dv8qiHPGyjCl1rwOT0LomSHpcElbjjVh5CwzElInB38HD8aSRVugKQjeyHA==",
+ "requires": {
+ "fault": "^1.0.1"
+ }
+ },
+ "remark-parse": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.3.tgz",
+ "integrity": "sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==",
+ "requires": {
+ "ccount": "^1.0.0",
+ "collapse-white-space": "^1.0.2",
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-whitespace-character": "^1.0.0",
+ "is-word-character": "^1.0.0",
+ "markdown-escapes": "^1.0.0",
+ "parse-entities": "^2.0.0",
+ "repeat-string": "^1.5.4",
+ "state-toggle": "^1.0.0",
+ "trim": "0.0.1",
+ "trim-trailing-lines": "^1.0.0",
+ "unherit": "^1.0.4",
+ "unist-util-remove-position": "^2.0.0",
+ "vfile-location": "^3.0.0",
+ "xtend": "^4.0.1"
+ }
+ },
+ "remark-stringify": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-8.1.1.tgz",
+ "integrity": "sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A==",
+ "requires": {
+ "ccount": "^1.0.0",
+ "is-alphanumeric": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-whitespace-character": "^1.0.0",
+ "longest-streak": "^2.0.1",
+ "markdown-escapes": "^1.0.0",
+ "markdown-table": "^2.0.0",
+ "mdast-util-compact": "^2.0.0",
+ "parse-entities": "^2.0.0",
+ "repeat-string": "^1.5.4",
+ "state-toggle": "^1.0.0",
+ "stringify-entities": "^3.0.0",
+ "unherit": "^1.0.4",
+ "xtend": "^4.0.1"
+ }
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+ },
+ "replace-ext": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+ "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs="
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ },
+ "state-toggle": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
+ "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ=="
+ },
+ "stringify-entities": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz",
+ "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==",
+ "requires": {
+ "character-entities-html4": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "to-vfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-6.1.0.tgz",
+ "integrity": "sha512-BxX8EkCxOAZe+D/ToHdDsJcVI4HqQfmw0tCkp31zf3dNP/XWIAjU4CmeuSwsSoOzOTqHPOL0KUzyZqJplkD0Qw==",
+ "requires": {
+ "is-buffer": "^2.0.0",
+ "vfile": "^4.0.0"
+ }
+ },
+ "trim": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+ "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0="
+ },
+ "trim-trailing-lines": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz",
+ "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA=="
+ },
+ "trough": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
+ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
+ },
+ "unherit": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
+ "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==",
+ "requires": {
+ "inherits": "^2.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "unified": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz",
+ "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==",
+ "requires": {
+ "bail": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-buffer": "^2.0.0",
+ "is-plain-obj": "^2.0.0",
+ "trough": "^1.0.0",
+ "vfile": "^4.0.0"
+ }
+ },
+ "unist-util-is": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz",
+ "integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ=="
+ },
+ "unist-util-remove-position": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz",
+ "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==",
+ "requires": {
+ "unist-util-visit": "^2.0.0"
+ }
+ },
+ "unist-util-stringify-position": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
+ "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
+ "requires": {
+ "@types/unist": "^2.0.2"
+ }
+ },
+ "unist-util-visit": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz",
+ "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^4.0.0",
+ "unist-util-visit-parents": "^3.0.0"
+ }
+ },
+ "unist-util-visit-parents": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz",
+ "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^4.0.0"
+ }
+ },
+ "vfile": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.0.tgz",
+ "integrity": "sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "is-buffer": "^2.0.0",
+ "replace-ext": "1.0.0",
+ "unist-util-stringify-position": "^2.0.0",
+ "vfile-message": "^2.0.0"
+ }
+ },
+ "vfile-location": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.1.0.tgz",
+ "integrity": "sha512-FCZ4AN9xMcjFIG1oGmZKo61PjwJHRVA+0/tPUP2ul4uIwjGGndIxavEMRpWn5p4xwm/ZsdXp9YNygf1ZyE4x8g=="
+ },
+ "vfile-message": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
+ "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-stringify-position": "^2.0.0"
+ }
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
+ }
+ }
+}
diff --git a/tools/formatter/package.json b/tools/formatter/package.json
new file mode 100644
index 0000000000..33650908b6
--- /dev/null
+++ b/tools/formatter/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "fcc-translation-annotation",
+ "version": "1.0.0",
+ "description": "",
+ "main": "translation-annotation/annotate.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "js-yaml": "^3.14.0",
+ "lodash": "^4.17.20",
+ "mdast-builder": "^1.1.1",
+ "remark-frontmatter": "^2.0.0",
+ "remark-parse": "^8.0.3",
+ "remark-stringify": "^8.1.1",
+ "to-vfile": "^6.1.0",
+ "unified": "^9.2.0"
+ }
+}
diff --git a/tools/formatter/translation-annotation/annotate.js b/tools/formatter/translation-annotation/annotate.js
new file mode 100644
index 0000000000..85ae86c275
--- /dev/null
+++ b/tools/formatter/translation-annotation/annotate.js
@@ -0,0 +1,20 @@
+const { getText } = require('./get-challenge-text');
+const { challengeToString } = require('./create-challenge-string');
+const { parseMD } = require('../../challenge-md-parser/mdx');
+
+module.exports.annotate = async function annotate(filePath) {
+ return generateTranscribableChallenge(filePath)
+ .then(challengeToString)
+ .catch(err => {
+ console.log('Error transforming');
+ console.log(filePath);
+ console.log(err);
+ });
+};
+
+async function generateTranscribableChallenge(fullPath) {
+ return Promise.all([parseMD(fullPath), getText(fullPath)]).then(results => ({
+ ...results[0],
+ ...results[1]
+ }));
+}
diff --git a/tools/formatter/translation-annotation/create-challenge-string.js b/tools/formatter/translation-annotation/create-challenge-string.js
new file mode 100644
index 0000000000..fbfd85820c
--- /dev/null
+++ b/tools/formatter/translation-annotation/create-challenge-string.js
@@ -0,0 +1,113 @@
+const { pick } = require('lodash');
+const yaml = require('js-yaml');
+
+const frontmatterProperties = [
+ 'id',
+ 'title',
+ 'challengeType',
+ 'videoId',
+ 'videoUrl',
+ 'forumTopicId',
+ 'isPrivate',
+ 'required',
+ 'helpCategory'
+];
+
+const otherProperties = [
+ 'description',
+ 'instructions',
+ 'tests',
+ 'solutions',
+ 'files',
+ 'question'
+];
+
+const notranslateStart = '';
+const notranslateEnd = '';
+
+function createFrontmatter(data) {
+ Object.keys(data).forEach(key => {
+ if (!frontmatterProperties.includes(key) && !otherProperties.includes(key))
+ throw Error(`Unknown property '${key}'`);
+ });
+
+ // TODO: sort the keys? It doesn't matter from a machine perspective, but
+ // it does from human-readability one. We could get lucky and have the order
+ // be preserved accidentally.
+ const frontData = pick(data, frontmatterProperties);
+ const frontYAML = yaml.dump(frontData);
+
+ return `---
+${frontYAML}---
+
+`;
+}
+
+// NOTE: trimEnd is used since trailing whitespace is rarely used (it can create
+// a
, but that's uncommon and hard to read)
+function createHints({ tests }) {
+ if (!tests) return '';
+ const strTests = tests
+ .map(
+ ({ text, testString }) => `${text.trimEnd()}
+
+${notranslateStart}
+${testString.trimEnd()}
+${notranslateEnd}
+`
+ )
+ .join('\n');
+ return createSection('hints', strTests);
+}
+
+function createQuestion({ question }) {
+ if (!question) return '';
+ const { text, answers, solution } = question;
+ const formattedAnswers = answers.map(
+ answer => `${notranslateStart}
+${answer.trimEnd()}
+${notranslateEnd}
+`
+ ).join(`
+---
+
+`);
+ const formattedQuestion =
+ createSection('text', text, { depth: 2 }) +
+ createSection('answers', formattedAnswers, { depth: 2 }) +
+ createSection('video-solution', solution, { depth: 2, translate: false });
+ return createSection('question', formattedQuestion);
+}
+
+function createInstructions({ instructions }) {
+ return createSection('instructions', instructions);
+}
+
+function createDescription({ description }) {
+ return createSection('description', description);
+}
+
+function createSection(heading, contents, options) {
+ const { depth = 1, translate = true } = options || {};
+ return contents && contents.toString().trim()
+ ? `${notranslateStart}
+${''.padEnd(depth, '#')} --${heading}--
+${translate ? notranslateEnd + '\n' : ''}
+${contents.toString().trimEnd()}
+${translate ? '' : notranslateEnd + '\n'}
+`
+ : '';
+}
+
+function challengeToString(data) {
+ const chalString =
+ createFrontmatter(data) +
+ createDescription(data) +
+ createInstructions(data) +
+ createQuestion(data) +
+ createHints(data);
+ // all sections have a trailing '\n', the last one of which needs removing
+ return chalString.slice(0, -1);
+}
+
+exports.challengeToString = challengeToString;
diff --git a/tools/formatter/translation-annotation/get-challenge-text.js b/tools/formatter/translation-annotation/get-challenge-text.js
new file mode 100644
index 0000000000..f6d2639ce5
--- /dev/null
+++ b/tools/formatter/translation-annotation/get-challenge-text.js
@@ -0,0 +1,26 @@
+const unified = require('unified');
+const vfile = require('to-vfile');
+const markdown = require('remark-parse');
+const frontmatter = require('remark-frontmatter');
+
+const textToData = require('./plugins/text-to-data');
+const testsToData = require('./plugins/tests-to-data');
+const questionToData = require('./plugins/question-to-data');
+
+const textProcessor = unified()
+ .use(markdown)
+ .use(textToData)
+ .use(testsToData)
+ .use(questionToData)
+ .use(frontmatter, ['yaml']);
+
+exports.getText = createProcessor(textProcessor);
+
+function createProcessor(processor) {
+ return async msg => {
+ const file = typeof msg === 'string' ? vfile.readSync(msg) : msg;
+ const tree = processor.parse(file);
+ await processor.run(tree, file);
+ return file.data;
+ };
+}
diff --git a/tools/formatter/translation-annotation/plugins/question-to-data.js b/tools/formatter/translation-annotation/plugins/question-to-data.js
new file mode 100644
index 0000000000..1245995423
--- /dev/null
+++ b/tools/formatter/translation-annotation/plugins/question-to-data.js
@@ -0,0 +1,64 @@
+const { root } = require('mdast-builder');
+const getAllBetween = require('../../../challenge-md-parser/mdx/plugins/utils/between-headings');
+const {
+ splitOnThematicBreak
+} = require('../../../challenge-md-parser/mdx/plugins/utils/split-on-thematic-break');
+
+const { stringifyMd } = require('./text-to-data');
+
+function plugin() {
+ return transformer;
+
+ function transformer(tree, file) {
+ const questionNodes = getAllBetween(tree, '--question--');
+ if (questionNodes.length > 0) {
+ const questionTree = root(questionNodes);
+ const textNodes = getAllBetween(questionTree, '--text--');
+ const answersNodes = getAllBetween(questionTree, '--answers--');
+ const solutionNodes = getAllBetween(questionTree, '--video-solution--');
+
+ const question = getQuestion(textNodes, answersNodes, solutionNodes);
+
+ file.data.question = question;
+ }
+ }
+}
+
+function getQuestion(textNodes, answersNodes, solutionNodes) {
+ const text = stringifyMd(textNodes);
+ const answers = getAnswers(answersNodes);
+ const solution = getSolution(solutionNodes);
+
+ if (!text) throw Error('text is missing from question');
+ if (!answers) throw Error('answers are missing from question');
+ if (!solution) throw Error('solution is missing from question');
+
+ return { text, answers, solution };
+}
+
+function getAnswers(answersNodes) {
+ const answerGroups = splitOnThematicBreak(answersNodes);
+ return answerGroups.map(answer => stringifyMd(answer));
+}
+
+function getSolution(solutionNodes) {
+ let solution;
+ try {
+ if (solutionNodes.length > 1) throw Error('Too many nodes');
+ if (solutionNodes[0].children.length > 1)
+ throw Error('Too many child nodes');
+ const solutionString = solutionNodes[0].children[0].value;
+ if (solutionString === '') throw Error('Non-empty string required');
+
+ solution = Number(solutionString);
+ if (Number.isNaN(solution)) throw Error('Not a number');
+ if (solution < 1) throw Error('Not positive number');
+ } catch (e) {
+ console.log(e);
+ throw Error('A video solution should be a positive integer');
+ }
+
+ return solution;
+}
+
+module.exports = plugin;
diff --git a/tools/formatter/translation-annotation/plugins/tests-to-data.js b/tools/formatter/translation-annotation/plugins/tests-to-data.js
new file mode 100644
index 0000000000..9d2f89d09c
--- /dev/null
+++ b/tools/formatter/translation-annotation/plugins/tests-to-data.js
@@ -0,0 +1,32 @@
+const chunk = require('lodash/chunk');
+const getAllBetween = require('../../../challenge-md-parser/mdx/plugins/utils/between-headings');
+const { stringifyMd } = require('./text-to-data');
+
+function plugin() {
+ return transformer;
+
+ function transformer(tree, file) {
+ const hintNodes = getAllBetween(tree, '--hints--');
+ if (hintNodes.length % 2 !== 0)
+ throw Error('Tests must be in (text, ```testString```) order');
+
+ const tests = chunk(hintNodes, 2).map(getTest);
+ file.data.tests = tests;
+ }
+}
+
+function getTest(hintNodes) {
+ const [textNode, testStringNode] = hintNodes;
+ const text = stringifyMd([textNode]);
+ const testString = stringifyMd([testStringNode]);
+
+ if (!text) throw Error('text is missing from hint');
+ // stub tests (i.e. text, but no testString) are allowed, but the md must
+ // have a code block, even if it is empty.
+ if (!testString && testString !== '')
+ throw Error('testString (code block) is missing from hint');
+
+ return { text, testString };
+}
+
+module.exports = plugin;
diff --git a/tools/formatter/translation-annotation/plugins/text-to-data.js b/tools/formatter/translation-annotation/plugins/text-to-data.js
new file mode 100644
index 0000000000..ba5ebf1ee9
--- /dev/null
+++ b/tools/formatter/translation-annotation/plugins/text-to-data.js
@@ -0,0 +1,26 @@
+const stringify = require('remark-stringify');
+const { root } = require('mdast-builder');
+const unified = require('unified');
+const getAllBetween = require('../../../challenge-md-parser/mdx/plugins/utils/between-headings');
+
+const stringifyMd = nodes =>
+ unified()
+ .use(stringify, { fences: true, emphasis: '*' })
+ .stringify(root(nodes));
+
+// NOTE: we need a new plugin (rather than using the challenge parser's plugin)
+// simply because it adds html to the descriptions. It's easier to start from
+// scratch.
+function plugin() {
+ return transformer;
+
+ function transformer(tree, file) {
+ file.data.description = stringifyMd(getAllBetween(tree, '--description--'));
+ file.data.instructions = stringifyMd(
+ getAllBetween(tree, '--instructions--')
+ );
+ }
+}
+
+module.exports = plugin;
+module.exports.stringifyMd = stringifyMd;