diff --git a/tools/challenge-parser/package-lock.json b/tools/challenge-parser/package-lock.json index 1e9ccf016c..8045b11c77 100644 --- a/tools/challenge-parser/package-lock.json +++ b/tools/challenge-parser/package-lock.json @@ -287,17 +287,35 @@ } }, "mdast-util-directive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-1.0.0.tgz", - "integrity": "sha512-04nOvmHrQfcgPQDAn9x0gW05vhqXmkGPP9G7o8BE+7Oy16oyTAgJpJyVFTscPyEzfsP7p7LSKJAXWqTCBvPjuw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-1.0.1.tgz", + "integrity": "sha512-VuO1za7BMtWMg8KA8eZrTBorEnCOOW5CXfIuNzUXe7YPie/wLgmNk/jxLMY8m+mzuqnO5eN0JuvlgFtO9EJpbQ==", "requires": { - "mdast-util-to-markdown": "^0.5.0", + "mdast-util-to-markdown": "^0.6.0", "parse-entities": "^2.0.0", "repeat-string": "^1.0.0", "stringify-entities": "^3.1.0", "unist-util-visit-parents": "^3.0.0" }, "dependencies": { + "mdast-util-to-markdown": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.1.tgz", + "integrity": "sha512-4qJtZ0qdyYeexAXoOZiU0uHIFVncJAmCkHkSluAsvDaVWODtPyNEo9I1ns0T4ulxu2EHRH5u/bt1cV0pdHCX+A==", + "requires": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + } + }, + "mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==" + }, "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", @@ -417,12 +435,23 @@ } }, "micromark-extension-directive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-1.0.0.tgz", - "integrity": "sha512-6FjvznI5GpUysZqGtTEMeDaC/D3FdWFVc3CC5gDZB3fBtqiaIRBhCNg4fbqvrFSC0T2eqRbO2dJ7ZFU86gAtEQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-1.1.1.tgz", + "integrity": "sha512-xkv00i9brg3LkOMleJc/mlDZlb2TF2TomSS0D4AVhWVDZx4OmuYxFlqtKHuiDXXiVp4mhcZfHsuWdvELdT7I9g==", "requires": { - "micromark": "~2.10.0", + "micromark": "~2.11.0", "parse-entities": "^2.0.0" + }, + "dependencies": { + "micromark": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.2.tgz", + "integrity": "sha512-IXuP76p2uj8uMg4FQc1cRE7lPCLsfAXuEfdjtdO55VRiFO1asrCSQ5g43NmPqFtRwzEnEhafRVzn2jg0UiKArQ==", + "requires": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + } } }, "micromark-extension-frontmatter": { @@ -568,6 +597,34 @@ "mdast-util-from-markdown": "^0.8.0" } }, + "remark-stringify": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", + "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", + "requires": { + "mdast-util-to-markdown": "^0.6.0" + }, + "dependencies": { + "mdast-util-to-markdown": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.1.tgz", + "integrity": "sha512-4qJtZ0qdyYeexAXoOZiU0uHIFVncJAmCkHkSluAsvDaVWODtPyNEo9I1ns0T4ulxu2EHRH5u/bt1cV0pdHCX+A==", + "requires": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + } + }, + "mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==" + } + } + }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", diff --git a/tools/challenge-parser/package.json b/tools/challenge-parser/package.json index f108954f9c..32cefa780d 100644 --- a/tools/challenge-parser/package.json +++ b/tools/challenge-parser/package.json @@ -23,6 +23,7 @@ "remark-frontmatter": "^3.0.0", "remark-html": "^12.0.0", "remark-parse": "^9.0.0", + "remark-stringify": "^9.0.1", "to-vfile": "^5.0.1", "unified": "^7.0.0", "unist-util-find": "^1.0.1", diff --git a/tools/challenge-parser/parser/__fixtures__/ast-directives.json b/tools/challenge-parser/parser/__fixtures__/ast-directives.json new file mode 100644 index 0000000000..5d719f653d --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/ast-directives.json @@ -0,0 +1,185 @@ +{ + "type": "root", + "children": [ + { + "type": "heading", + "depth": 1, + "children": [ + { + "type": "text", + "value": "--description--", + "position": { + "start": { "line": 1, "column": 3, "offset": 2 }, + "end": { "line": 1, "column": 18, "offset": 17 } + } + } + ], + "position": { + "start": { "line": 1, "column": 1, "offset": 0 }, + "end": { "line": 1, "column": 18, "offset": 17 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "textDirective", + "name": "root", + "attributes": {}, + "children": [], + "position": { + "start": { "line": 3, "column": 1, "offset": 19 }, + "end": { "line": 3, "column": 6, "offset": 24 } + } + }, + { + "type": "text", + "value": " appears, ", + "position": { + "start": { "line": 3, "column": 6, "offset": 24 }, + "end": { "line": 3, "column": 16, "offset": 34 } + } + }, + { + "type": "textDirective", + "name": "import", + "attributes": {}, + "children": [], + "position": { + "start": { "line": 3, "column": 16, "offset": 34 }, + "end": { "line": 3, "column": 23, "offset": 41 } + } + }, + { + "type": "text", + "value": " appears", + "position": { + "start": { "line": 3, "column": 23, "offset": 41 }, + "end": { "line": 3, "column": 31, "offset": 49 } + } + } + ], + "position": { + "start": { "line": 3, "column": 1, "offset": 19 }, + "end": { "line": 3, "column": 31, "offset": 49 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "the next paragraph should appear", + "position": { + "start": { "line": 5, "column": 1, "offset": 51 }, + "end": { "line": 5, "column": 33, "offset": 83 } + } + } + ], + "position": { + "start": { "line": 5, "column": 1, "offset": 51 }, + "end": { "line": 5, "column": 33, "offset": 83 } + } + }, + { + "type": "leafDirective", + "name": "import", + "attributes": {}, + "children": [], + "position": { + "start": { "line": 7, "column": 1, "offset": 85 }, + "end": { "line": 7, "column": 9, "offset": 93 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "even though it's an import directive, but if we use the full syntax ", + "position": { + "start": { "line": 9, "column": 1, "offset": 95 }, + "end": { "line": 9, "column": 58, "offset": 152 } + } + }, + { + "type": "inlineCode", + "value": "::directive-name{attr=\"name\" attr2=\"a/path\"}", + "position": { + "start": { "line": 9, "column": 58, "offset": 152 }, + "end": { "line": 9, "column": 100, "offset": 194 } + } + } + ], + "position": { + "start": { "line": 9, "column": 1, "offset": 95 }, + "end": { "line": 9, "column": 100, "offset": 194 } + } + }, + { + "type": "leafDirective", + "name": "import", + "attributes": { "component": "name", "from": "script.md" }, + "children": [], + "position": { + "start": { "line": 11, "column": 1, "offset": 196 }, + "end": { "line": 11, "column": 44, "offset": 239 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "it goes.", + "position": { + "start": { "line": 13, "column": 1, "offset": 241 }, + "end": { "line": 13, "column": 9, "offset": 249 } + } + } + ], + "position": { + "start": { "line": 13, "column": 1, "offset": 241 }, + "end": { "line": 13, "column": 9, "offset": 249 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "::: name [inline-content] {key=val}\na container directive\n:::", + "position": { + "start": { "line": 15, "column": 1, "offset": 251 }, + "end": { "line": 17, "column": 4, "offset": 312 } + } + } + ], + "position": { + "start": { "line": 15, "column": 1, "offset": 251 }, + "end": { "line": 17, "column": 4, "offset": 312 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": ":::", + "position": { + "start": { "line": 19, "column": 1, "offset": 314 }, + "end": { "line": 19, "column": 4, "offset": 317 } + } + } + ], + "position": { + "start": { "line": 19, "column": 1, "offset": 314 }, + "end": { "line": 19, "column": 4, "offset": 317 } + } + } + ], + "position": { + "start": { "line": 1, "column": 1, "offset": 0 }, + "end": { "line": 20, "column": 1, "offset": 318 } + } +} diff --git a/tools/challenge-parser/parser/__fixtures__/ast-imports-extra.json b/tools/challenge-parser/parser/__fixtures__/ast-imports-extra.json new file mode 100644 index 0000000000..d37e08b01c --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/ast-imports-extra.json @@ -0,0 +1,321 @@ +{ + "type": "root", + "children": [ + { + "type": "leafDirective", + "name": "import", + "attributes": { "component": "Script", "from": "./script.md" }, + "children": [], + "position": { + "start": { "line": 1, "column": 1, "offset": 0 }, + "end": { "line": 1, "column": 49, "offset": 48 } + } + }, + { + "type": "leafDirective", + "name": "import", + "attributes": { "component": "Second", "from": "./script-two.md" }, + "children": [], + "position": { + "start": { "line": 2, "column": 1, "offset": 49 }, + "end": { "line": 2, "column": 53, "offset": 101 } + } + }, + { + "type": "leafDirective", + "name": "import", + "attributes": {}, + "children": [], + "position": { + "start": { "line": 3, "column": 1, "offset": 102 }, + "end": { "line": 3, "column": 9, "offset": 110 } + } + }, + { + "type": "heading", + "depth": 1, + "children": [ + { + "type": "text", + "value": "--description--", + "position": { + "start": { "line": 5, "column": 3, "offset": 114 }, + "end": { "line": 5, "column": 18, "offset": 129 } + } + } + ], + "position": { + "start": { "line": 5, "column": 1, "offset": 112 }, + "end": { "line": 5, "column": 18, "offset": 129 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Paragraph 1 ::import", + "position": { + "start": { "line": 7, "column": 1, "offset": 131 }, + "end": { "line": 7, "column": 21, "offset": 151 } + } + } + ], + "position": { + "start": { "line": 7, "column": 1, "offset": 131 }, + "end": { "line": 7, "column": 21, "offset": 151 } + } + }, + { + "type": "code", + "lang": "html", + "meta": null, + "value": "code example", + "position": { + "start": { "line": 9, "column": 1, "offset": 153 }, + "end": { "line": 11, "column": 4, "offset": 177 } + } + }, + { + "type": "heading", + "depth": 1, + "children": [ + { + "type": "text", + "value": "--instructions--", + "position": { + "start": { "line": 13, "column": 3, "offset": 181 }, + "end": { "line": 13, "column": 19, "offset": 197 } + } + } + ], + "position": { + "start": { "line": 13, "column": 1, "offset": 179 }, + "end": { "line": 13, "column": 19, "offset": 197 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Paragraph 0", + "position": { + "start": { "line": 15, "column": 1, "offset": 199 }, + "end": { "line": 15, "column": 12, "offset": 210 } + } + } + ], + "position": { + "start": { "line": 15, "column": 1, "offset": 199 }, + "end": { "line": 15, "column": 12, "offset": 210 } + } + }, + { + "type": "code", + "lang": "html", + "meta": null, + "value": "code example 0", + "position": { + "start": { "line": 17, "column": 1, "offset": 212 }, + "end": { "line": 19, "column": 4, "offset": 238 } + } + }, + { + "type": "heading", + "depth": 1, + "children": [ + { + "type": "text", + "value": "--hints--", + "position": { + "start": { "line": 21, "column": 3, "offset": 242 }, + "end": { "line": 21, "column": 12, "offset": 251 } + } + } + ], + "position": { + "start": { "line": 21, "column": 1, "offset": 240 }, + "end": { "line": 21, "column": 12, "offset": 251 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "First hint", + "position": { + "start": { "line": 23, "column": 1, "offset": 253 }, + "end": { "line": 23, "column": 11, "offset": 263 } + } + } + ], + "position": { + "start": { "line": 23, "column": 1, "offset": 253 }, + "end": { "line": 23, "column": 11, "offset": 263 } + } + }, + { + "type": "code", + "lang": "js", + "meta": null, + "value": "// test code", + "position": { + "start": { "line": 25, "column": 1, "offset": 265 }, + "end": { "line": 27, "column": 4, "offset": 287 } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Second hint with ", + "position": { + "start": { "line": 29, "column": 1, "offset": 289 }, + "end": { "line": 29, "column": 18, "offset": 306 } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { "line": 29, "column": 18, "offset": 306 }, + "end": { "line": 29, "column": 24, "offset": 312 } + } + }, + { + "type": "text", + "value": "code", + "position": { + "start": { "line": 29, "column": 24, "offset": 312 }, + "end": { "line": 29, "column": 28, "offset": 316 } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { "line": 29, "column": 28, "offset": 316 }, + "end": { "line": 29, "column": 35, "offset": 323 } + } + } + ], + "position": { + "start": { "line": 29, "column": 1, "offset": 289 }, + "end": { "line": 29, "column": 35, "offset": 323 } + } + }, + { + "type": "code", + "lang": "js", + "meta": null, + "value": "// more test code", + "position": { + "start": { "line": 31, "column": 1, "offset": 325 }, + "end": { "line": 33, "column": 4, "offset": 352 } + } + }, + { + "type": "heading", + "depth": 1, + "children": [ + { + "type": "text", + "value": "--seed--", + "position": { + "start": { "line": 36, "column": 3, "offset": 357 }, + "end": { "line": 36, "column": 11, "offset": 365 } + } + } + ], + "position": { + "start": { "line": 36, "column": 1, "offset": 355 }, + "end": { "line": 36, "column": 11, "offset": 365 } + } + }, + { + "type": "heading", + "depth": 2, + "children": [ + { + "type": "text", + "value": "--seed-contents--", + "position": { + "start": { "line": 38, "column": 4, "offset": 370 }, + "end": { "line": 38, "column": 21, "offset": 387 } + } + } + ], + "position": { + "start": { "line": 38, "column": 1, "offset": 367 }, + "end": { "line": 38, "column": 21, "offset": 387 } + } + }, + { + "type": "code", + "lang": "html", + "meta": null, + "value": "\n \n \n", + "position": { + "start": { "line": 40, "column": 1, "offset": 389 }, + "end": { "line": 45, "column": 4, "offset": 434 } + } + }, + { + "type": "code", + "lang": "css", + "meta": null, + "value": "body {\n background: green;\n}", + "position": { + "start": { "line": 47, "column": 1, "offset": 436 }, + "end": { "line": 51, "column": 4, "offset": 476 } + } + }, + { + "type": "leafDirective", + "name": "use", + "attributes": { "component": "Second" }, + "children": [], + "position": { + "start": { "line": 53, "column": 1, "offset": 478 }, + "end": { "line": 53, "column": 26, "offset": 503 } + } + }, + { + "type": "leafDirective", + "name": "id", + "attributes": { "id": "custom-name" }, + "children": [], + "position": { + "start": { "line": 55, "column": 1, "offset": 505 }, + "end": { "line": 55, "column": 19, "offset": 523 } + } + }, + { + "type": "code", + "lang": "js", + "meta": null, + "value": "var x = 'y';", + "position": { + "start": { "line": 57, "column": 1, "offset": 525 }, + "end": { "line": 59, "column": 4, "offset": 547 } + } + }, + { + "type": "leafDirective", + "name": "use", + "attributes": { "component": "Script" }, + "children": [], + "position": { + "start": { "line": 61, "column": 1, "offset": 549 }, + "end": { "line": 61, "column": 26, "offset": 574 } + } + } + ], + "position": { + "start": { "line": 1, "column": 1, "offset": 0 }, + "end": { "line": 62, "column": 1, "offset": 575 } + } +} diff --git a/tools/challenge-parser/parser/__fixtures__/with-directives.md b/tools/challenge-parser/parser/__fixtures__/with-directives.md new file mode 100644 index 0000000000..844bf71787 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-directives.md @@ -0,0 +1,17 @@ +# --description-- + +:root appears, :import appears + +the next paragraph should appear + +::import + +even though it's an import directive, but if we use the full syntax `::directive-name{attr="name" attr2="a/path"}` + +::import{component="name" from="script.md"} + +it goes. + +::: name [inline-content] {key=val} +a container directive +::: diff --git a/tools/challenge-parser/parser/__fixtures__/with-imports-extra.md b/tools/challenge-parser/parser/__fixtures__/with-imports-extra.md new file mode 100644 index 0000000000..3b729fe6d0 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-imports-extra.md @@ -0,0 +1,61 @@ +::import{component="Script" from="./script.md" } +::import{component="Second" from="./script-two.md" } +::import + +# --description-- + +Paragraph 1 ::import + +```html +code example +``` + +# --instructions-- + +Paragraph 0 + +```html +code example 0 +``` + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + + +# --seed-- + +## --seed-contents-- + +```html + + + + +``` + +```css +body { + background: green; +} +``` + +::use{component="Second"} + +::id{#custom-name} + +```js +var x = 'y'; +``` + +::use{component="Script"} diff --git a/tools/challenge-parser/parser/__snapshots__/index.acceptance.test.js.snap b/tools/challenge-parser/parser/__snapshots__/index.acceptance.test.js.snap index ea04e9de46..2f4f15130b 100644 --- a/tools/challenge-parser/parser/__snapshots__/index.acceptance.test.js.snap +++ b/tools/challenge-parser/parser/__snapshots__/index.acceptance.test.js.snap @@ -1,5 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`challenge parser it should not parse directives we do not use 1`] = ` +Object { + "description": "
+

:root appears, :import appears

+

the next paragraph should appear

+::import +

even though it's an import directive, but if we use the full syntax ::directive-name{attr=\\"name\\" attr2=\\"a/path\\"}

+

it goes.

+

::: name [inline-content] {key=val} +a container directive +:::

+
", + "solutions": Array [ + Object {}, + ], + "tests": Array [], +} +`; + exports[`challenge parser it should parse video questions 1`] = ` Object { "description": "
diff --git a/tools/challenge-parser/parser/index.acceptance.test.js b/tools/challenge-parser/parser/index.acceptance.test.js index 38b76a0fb8..a323dade33 100644 --- a/tools/challenge-parser/parser/index.acceptance.test.js +++ b/tools/challenge-parser/parser/index.acceptance.test.js @@ -53,4 +53,11 @@ describe('challenge parser', () => { ); expect(parsed).toMatchSnapshot(); }); + + it('it should not parse directives we do not use', async () => { + const parsed = await parseMD( + path.resolve(__dirname, '__fixtures__/with-directives.md') + ); + expect(parsed).toMatchSnapshot(); + }); }); diff --git a/tools/challenge-parser/parser/index.js b/tools/challenge-parser/parser/index.js index 958b1fe9f0..3de98730fc 100644 --- a/tools/challenge-parser/parser/index.js +++ b/tools/challenge-parser/parser/index.js @@ -3,6 +3,7 @@ const remark = require('remark-parse'); const directive = require('remark-directive'); const frontmatter = require('remark-frontmatter'); const addTests = require('./plugins/add-tests'); +const restoreDirectives = require('./plugins/restore-directives'); const replaceImports = require('./plugins/replace-imports'); const addFrontmatter = require('./plugins/add-frontmatter'); const addText = require('./plugins/add-text'); @@ -33,14 +34,18 @@ const processor = unified() // ::use{component="Script"} // appears. .use(replaceImports) - // the final five plugins insert content into file.data - .use(addText, ['description', 'instructions']) + // the final five 'add' plugins insert content into file.data // TODO: rename test->hint everywhere? It should make things easier to reason // about. - .use(addTests) - .use(addVideoQuestion) .use(addSeed) - .use(addSolution); + .use(addSolution) + // the directives will have been parsed and used by this point, any remaining + // 'directives' will be from text like the css selector :root. These should be + // converted back to text before they're added to the challenge object. + .use(restoreDirectives) + .use(addVideoQuestion) + .use(addTests) + .use(addText, ['description', 'instructions']); exports.parseMD = function parseMD(filename) { return new Promise((resolve, reject) => { diff --git a/tools/challenge-parser/parser/plugins/replace-imports.js b/tools/challenge-parser/parser/plugins/replace-imports.js index 242ce66c88..5c8191d812 100644 --- a/tools/challenge-parser/parser/plugins/replace-imports.js +++ b/tools/challenge-parser/parser/plugins/replace-imports.js @@ -31,6 +31,10 @@ function plugin() { } const importPromises = importedFiles.map(async ({ attributes }) => { const { from, component } = attributes; + // if these are missing, bail, since it's not an import. + if (!from || !component) { + return null; + } const location = path.resolve(file.dirname, from); return await read(location) .then(parse) @@ -65,13 +69,27 @@ function plugin() { // Also, we remove the import statements here. Promise.all(importPromises) .then(() => { - remove(tree, { type: 'leafDirective', name: 'import' }); + remove(tree, isImportNode); next(); }) - .catch(next); + .catch(err => { + console.error('error processing ::import'); + console.error(err); + next(err); + }); } } +function isImportNode({ type, name, attributes }) { + if (!attributes) return false; + return ( + type === 'leafDirective' && + name === 'import' && + attributes.component && + attributes.from + ); +} + function validateImports(fileTree) { let valid = true; diff --git a/tools/challenge-parser/parser/plugins/replace-imports.test.js b/tools/challenge-parser/parser/plugins/replace-imports.test.js index 45b50c080e..24232dbb64 100644 --- a/tools/challenge-parser/parser/plugins/replace-imports.test.js +++ b/tools/challenge-parser/parser/plugins/replace-imports.test.js @@ -1,4 +1,4 @@ -/* global describe it expect */ +/* global describe it expect jest */ const path = require('path'); const cloneDeep = require('lodash/cloneDeep'); const toVfile = require('to-vfile'); @@ -7,12 +7,14 @@ const selectAll = require('unist-util-select').selectAll; const addImports = require('./replace-imports'); const originalImportsAST = require('../__fixtures__/ast-imports.json'); const originalImportsTwoAST = require('../__fixtures__/ast-imports-two.json'); +const originalImportsExtraAST = require('../__fixtures__/ast-imports-extra.json'); const originalSimpleAST = require('../__fixtures__/ast-simple.json'); const originalMarkerAST = require('../__fixtures__/ast-marker-imports.json'); describe('replace-imports', () => { let importsAST; let importsTwoAST; + let importsExtraAST; let simpleAST; let markerAST; let correctFile; @@ -21,6 +23,7 @@ describe('replace-imports', () => { beforeEach(() => { importsAST = cloneDeep(originalImportsAST); importsTwoAST = cloneDeep(originalImportsTwoAST); + importsExtraAST = cloneDeep(originalImportsExtraAST); simpleAST = cloneDeep(originalSimpleAST); markerAST = cloneDeep(originalMarkerAST); correctFile = toVfile( @@ -56,12 +59,15 @@ describe('replace-imports', () => { }); it('should fail when the imported file cannot be found', done => { + expect.assertions(1); + console.error = jest.fn(); const plugin = addImports(); // we have to rely on the next callback, because that is how you get error // messages out of transformers const next = err => { if (err) { + expect(console.error).toHaveBeenCalledTimes(2); done(); } else { done('An error should have been thrown by addImports'); @@ -118,6 +124,26 @@ describe('replace-imports', () => { plugin(importsAST, correctFile, next); }); + it('should not remove an ::import without the required attributes', done => { + expect.assertions(2); + const selector = 'leafDirective[name=import]'; + const plugin = addImports(); + const importNodes = selectAll(selector, importsExtraAST); + + expect(importNodes.length).toBe(3); + + const next = err => { + if (err) { + done(err); + } else { + const importNodes = selectAll(selector, importsExtraAST); + expect(importNodes.length).toBe(1); + done(); + } + }; + plugin(importsExtraAST, correctFile, next); + }); + it('should remove all matching ::use statements', done => { expect.assertions(2); const selector = 'leafDirective[name=use]'; @@ -205,9 +231,12 @@ describe('replace-imports', () => { }); it('should reject imported files with editable region markers', done => { + expect.assertions(1); + console.error = jest.fn(); const plugin = addImports(); const next = err => { if (err) { + expect(console.error).toHaveBeenCalledTimes(2); done(); } else { done('An error should have been thrown by addImports'); diff --git a/tools/challenge-parser/parser/plugins/restore-directives.js b/tools/challenge-parser/parser/plugins/restore-directives.js new file mode 100644 index 0000000000..1771c97205 --- /dev/null +++ b/tools/challenge-parser/parser/plugins/restore-directives.js @@ -0,0 +1,32 @@ +const visit = require('unist-util-visit'); +const { matches } = require('unist-util-select'); +const directive = require('mdast-util-directive'); +var toMarkdown = require('mdast-util-to-markdown'); + +function plugin() { + return transformer; + + function transformer(tree) { + visit(tree, visitor); + + function visitor(node, id, parent) { + // currently `remark-directive` seems to be ignoring containerDirectives + // but, assuming that will get fixed, we test for it anyway. + const isDirective = + matches('leafDirective', node) || + matches('textDirective', node) || + matches('containerDirective', node); + + if (isDirective) { + parent.children[id] = { + type: 'text', + value: toMarkdown(node, { + extensions: [directive.toMarkdown] + }).trim() + }; + } + } + } +} + +module.exports = plugin; diff --git a/tools/challenge-parser/parser/plugins/restore-directives.test.js b/tools/challenge-parser/parser/plugins/restore-directives.test.js new file mode 100644 index 0000000000..dc597317d3 --- /dev/null +++ b/tools/challenge-parser/parser/plugins/restore-directives.test.js @@ -0,0 +1,66 @@ +/* global describe it expect */ +const cloneDeep = require('lodash/cloneDeep'); +const { selectAll } = require('unist-util-select'); +const find = require('unist-util-find'); + +const restoreDirectives = require('./restore-directives'); +const directivesOriginalAST = require('../__fixtures__/ast-directives.json'); + +describe('restore-directives', () => { + let directivesAST; + beforeEach(() => { + directivesAST = cloneDeep(directivesOriginalAST); + }); + + it('should return a function', () => { + expect.assertions(1); + const plugin = restoreDirectives(); + + expect(typeof plugin).toEqual('function'); + }); + // TODO: if remark-directive starts processing containers, add them to the + // tests + it('should remove any directives in the AST', () => { + expect.assertions(4); + const plugin = restoreDirectives(); + let leaves = selectAll('leafDirective', directivesAST); + let text = selectAll('textDirective', directivesAST); + expect(leaves.length).toBe(2); + expect(text.length).toBe(2); + plugin(directivesAST); + leaves = selectAll('leafDirective', directivesAST); + text = selectAll('textDirective', directivesAST); + expect(leaves.length).toBe(0); + expect(text.length).toBe(0); + }); + + it('should put the original text into the AST', () => { + expect.assertions(4); + const plugin = restoreDirectives(); + + let nodeWithImport = find( + directivesAST, + node => node.value && node.value.includes('::import') + ); + let nodeWithRoot = find( + directivesAST, + node => node.value && node.value.includes(':root') + ); + + expect(nodeWithImport).not.toBeTruthy(); + expect(nodeWithRoot).not.toBeTruthy(); + plugin(directivesAST); + + nodeWithImport = find( + directivesAST, + node => node.value && node.value.includes('::import') + ); + nodeWithRoot = find( + directivesAST, + node => node.value && node.value.includes(':root') + ); + + expect(nodeWithImport).toBeTruthy(); + expect(nodeWithRoot).toBeTruthy(); + }); +}); diff --git a/tools/challenge-parser/parser/tools/full-parse.js b/tools/challenge-parser/parser/tools/full-parse.js index a1992d218f..06bdbb5e47 100644 --- a/tools/challenge-parser/parser/tools/full-parse.js +++ b/tools/challenge-parser/parser/tools/full-parse.js @@ -1,10 +1,10 @@ const { inspect } = require('util'); const path = require('path'); -const { parsemd } = require('../index'); +const { parseMD } = require('../index'); (async () => { - const fullPath = path.resolve(__dirname, '../__fixtures__/realistic.md'); - const output = await parsemd(fullPath); + const fullPath = path.resolve(__dirname, './example.md'); + const output = await parseMD(fullPath); console.log(inspect(output, null, null, true)); })(); diff --git a/tools/challenge-parser/parser/tools/parse-md.js b/tools/challenge-parser/parser/tools/parse-md.js index 969340a753..b656434d0e 100644 --- a/tools/challenge-parser/parser/tools/parse-md.js +++ b/tools/challenge-parser/parser/tools/parse-md.js @@ -1,12 +1,15 @@ const { read } = require('to-vfile'); const remark = require('remark'); -const html = require('remark-html'); +const directives = require('remark-directive'); +const stringify = require('remark-stringify'); (async () => { const path = './example.md'; const file = await read(path); const contents = await remark() - .use(html) + .use(directives) + .use(() => tree => console.dir(tree, { depth: null, colors: true })) + .use(stringify) .process(file); console.log(contents); })();