diff --git a/src/lexer.zig b/src/lexer.zig index 71bfedf..68d8f07 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -2299,7 +2299,13 @@ pub const Lexer = struct { // consume line along with `\n` prefix string_ptr = line_end; // Extract text after the indent - const text_content = if (str.len > indents) str[indents..] else ""; + // For whitespace-only lines, preserve the whitespace as-is (don't subtract indent) + const text_content = if (trimmed.len == 0) + str // Preserve whitespace-only lines exactly + else if (str.len > indents) + str[indents..] + else + ""; tokens_list.append(self.allocator, text_content) catch return false; } else if (line_indent > self.indent_stack.items[0]) { // line is indented less than the first line but is still indented @@ -2328,7 +2334,16 @@ pub const Lexer = struct { if (ii < token_indent_list.items.len and token_indent_list.items[ii]) { self.incrementColumn(indents); } - self.addText(.text, token_text, "", 0); + // For pipeless text, emit empty text tokens to preserve blank lines + // (addText skips empty content, but blank lines need to be preserved) + if (token_text.len == 0) { + var empty_token = self.tok(.text, .none); + empty_token.val = .{ .string = "" }; + self.tokens.append(self.allocator, empty_token) catch return false; + self.tokEnd(&empty_token); + } else { + self.addText(.text, token_text, "", 0); + } } var end_token = self.tok(.end_pipeless_text, .none); diff --git a/src/playground/benchmark b/src/playground/benchmark deleted file mode 100755 index a4d3400..0000000 Binary files a/src/playground/benchmark and /dev/null differ diff --git a/src/playground/run_zig b/src/playground/run_zig deleted file mode 100755 index e69de29..0000000 diff --git a/src/test-data/pug-strip-comments/test/__snapshots__/index.test.js.snap b/src/test-data/pug-strip-comments/test/__snapshots__/index.test.js.snap deleted file mode 100644 index 7ee98d2..0000000 --- a/src/test-data/pug-strip-comments/test/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,1173 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`comment-in-comment.input.json 1`] = ` -Object { - "code": "PUG:UNEXPECTED_TOKEN", - "line": 2, - "msg": "\`comment\` encountered when already in a comment", -} -`; - -exports[`comments.input.json 1`] = ` -Array [ - Object { - "line": 2, - "type": "newline", - }, - Object { - "buffer": true, - "line": 2, - "type": "comment", - "val": " foo", - }, - Object { - "line": 3, - "type": "newline", - }, - Object { - "line": 3, - "selfClosing": false, - "type": "tag", - "val": "ul", - }, - Object { - "line": 4, - "type": "indent", - "val": 2, - }, - Object { - "buffer": true, - "line": 4, - "type": "comment", - "val": " bar", - }, - Object { - "line": 5, - "type": "newline", - }, - Object { - "line": 5, - "selfClosing": false, - "type": "tag", - "val": "li", - }, - Object { - "line": 5, - "type": "text", - "val": "one", - }, - Object { - "line": 6, - "type": "newline", - }, - Object { - "buffer": true, - "line": 6, - "type": "comment", - "val": " baz", - }, - Object { - "line": 7, - "type": "newline", - }, - Object { - "line": 7, - "selfClosing": false, - "type": "tag", - "val": "li", - }, - Object { - "line": 7, - "type": "text", - "val": "two", - }, - Object { - "line": 9, - "type": "outdent", - }, - Object { - "buffer": true, - "line": 9, - "type": "comment", - "val": "", - }, - Object { - "line": 9, - "type": "start-pipeless-text", - }, - Object { - "line": 10, - "type": "text", - "val": "ul", - }, - Object { - "line": 11, - "type": "newline", - }, - Object { - "line": 11, - "type": "text", - "val": " li foo", - }, - Object { - "line": 12, - "type": "newline", - }, - Object { - "line": 12, - "type": "text", - "val": "", - }, - Object { - "line": 12, - "type": "end-pipeless-text", - }, - Object { - "line": 13, - "type": "newline", - }, - Object { - "buffer": true, - "line": 13, - "type": "comment", - "val": " block", - }, - Object { - "line": 13, - "type": "start-pipeless-text", - }, - Object { - "line": 14, - "type": "text", - "val": "// inline follow", - }, - Object { - "line": 15, - "type": "newline", - }, - Object { - "line": 15, - "type": "text", - "val": "li three", - }, - Object { - "line": 16, - "type": "newline", - }, - Object { - "line": 16, - "type": "text", - "val": "", - }, - Object { - "line": 16, - "type": "end-pipeless-text", - }, - Object { - "line": 17, - "type": "newline", - }, - Object { - "buffer": true, - "line": 17, - "type": "comment", - "val": " block", - }, - Object { - "line": 17, - "type": "start-pipeless-text", - }, - Object { - "line": 18, - "type": "text", - "val": "// inline followed by tags", - }, - Object { - "line": 19, - "type": "newline", - }, - Object { - "line": 19, - "type": "text", - "val": "ul", - }, - Object { - "line": 20, - "type": "newline", - }, - Object { - "line": 20, - "type": "text", - "val": " li four", - }, - Object { - "line": 21, - "type": "newline", - }, - Object { - "line": 21, - "type": "text", - "val": "", - }, - Object { - "line": 21, - "type": "end-pipeless-text", - }, - Object { - "line": 22, - "type": "newline", - }, - Object { - "buffer": true, - "line": 22, - "type": "comment", - "val": "if IE lt 9", - }, - Object { - "line": 22, - "type": "start-pipeless-text", - }, - Object { - "line": 23, - "type": "text", - "val": "// inline", - }, - Object { - "line": 24, - "type": "newline", - }, - Object { - "line": 24, - "type": "text", - "val": "script(src='/lame.js')", - }, - Object { - "line": 25, - "type": "newline", - }, - Object { - "line": 25, - "type": "text", - "val": "// end-inline", - }, - Object { - "line": 26, - "type": "newline", - }, - Object { - "line": 26, - "type": "text", - "val": "", - }, - Object { - "line": 26, - "type": "end-pipeless-text", - }, - Object { - "line": 27, - "type": "newline", - }, - Object { - "line": 31, - "type": "newline", - }, - Object { - "line": 32, - "type": "newline", - }, - Object { - "line": 32, - "type": "text", - "val": " ", - }, - Object { - "line": 33, - "type": "newline", - }, - Object { - "line": 33, - "type": "text", - "val": "asfd", - }, - Object { - "line": 35, - "type": "newline", - }, - Object { - "line": 35, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 35, - "type": "text", - "val": "five", - }, - Object { - "line": 37, - "type": "newline", - }, - Object { - "line": 37, - "type": "class", - "val": "foo", - }, - Object { - "line": 37, - "type": "text", - "val": "// not a comment", - }, - Object { - "line": 38, - "type": "newline", - }, - Object { - "line": 38, - "type": "eos", - }, -] -`; - -exports[`comments.input.json 2`] = ` -Array [ - Object { - "line": 2, - "type": "newline", - }, - Object { - "line": 3, - "type": "newline", - }, - Object { - "line": 3, - "selfClosing": false, - "type": "tag", - "val": "ul", - }, - Object { - "line": 4, - "type": "indent", - "val": 2, - }, - Object { - "line": 5, - "type": "newline", - }, - Object { - "line": 5, - "selfClosing": false, - "type": "tag", - "val": "li", - }, - Object { - "line": 5, - "type": "text", - "val": "one", - }, - Object { - "line": 6, - "type": "newline", - }, - Object { - "line": 7, - "type": "newline", - }, - Object { - "line": 7, - "selfClosing": false, - "type": "tag", - "val": "li", - }, - Object { - "line": 7, - "type": "text", - "val": "two", - }, - Object { - "line": 9, - "type": "outdent", - }, - Object { - "line": 13, - "type": "newline", - }, - Object { - "line": 17, - "type": "newline", - }, - Object { - "line": 22, - "type": "newline", - }, - Object { - "line": 27, - "type": "newline", - }, - Object { - "buffer": false, - "line": 27, - "type": "comment", - "val": "", - }, - Object { - "line": 28, - "type": "text", - "val": "", - }, - Object { - "line": 28, - "type": "start-pipeless-text", - }, - Object { - "line": 29, - "type": "text", - "val": "asfd", - }, - Object { - "line": 30, - "type": "newline", - }, - Object { - "line": 30, - "type": "text", - "val": "", - }, - Object { - "line": 30, - "type": "end-pipeless-text", - }, - Object { - "line": 31, - "type": "newline", - }, - Object { - "buffer": false, - "line": 31, - "type": "comment", - "val": "", - }, - Object { - "line": 32, - "type": "newline", - }, - Object { - "line": 32, - "type": "text", - "val": " ", - }, - Object { - "line": 33, - "type": "newline", - }, - Object { - "line": 33, - "type": "text", - "val": "asfd", - }, - Object { - "line": 35, - "type": "newline", - }, - Object { - "line": 35, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 35, - "type": "text", - "val": "five", - }, - Object { - "line": 37, - "type": "newline", - }, - Object { - "line": 37, - "type": "class", - "val": "foo", - }, - Object { - "line": 37, - "type": "text", - "val": "// not a comment", - }, - Object { - "line": 38, - "type": "newline", - }, - Object { - "line": 38, - "type": "eos", - }, -] -`; - -exports[`comments.input.json 3`] = ` -Array [ - Object { - "line": 2, - "type": "newline", - }, - Object { - "line": 3, - "type": "newline", - }, - Object { - "line": 3, - "selfClosing": false, - "type": "tag", - "val": "ul", - }, - Object { - "line": 4, - "type": "indent", - "val": 2, - }, - Object { - "line": 5, - "type": "newline", - }, - Object { - "line": 5, - "selfClosing": false, - "type": "tag", - "val": "li", - }, - Object { - "line": 5, - "type": "text", - "val": "one", - }, - Object { - "line": 6, - "type": "newline", - }, - Object { - "line": 7, - "type": "newline", - }, - Object { - "line": 7, - "selfClosing": false, - "type": "tag", - "val": "li", - }, - Object { - "line": 7, - "type": "text", - "val": "two", - }, - Object { - "line": 9, - "type": "outdent", - }, - Object { - "line": 13, - "type": "newline", - }, - Object { - "line": 17, - "type": "newline", - }, - Object { - "line": 22, - "type": "newline", - }, - Object { - "line": 27, - "type": "newline", - }, - Object { - "line": 31, - "type": "newline", - }, - Object { - "line": 32, - "type": "newline", - }, - Object { - "line": 32, - "type": "text", - "val": " ", - }, - Object { - "line": 33, - "type": "newline", - }, - Object { - "line": 33, - "type": "text", - "val": "asfd", - }, - Object { - "line": 35, - "type": "newline", - }, - Object { - "line": 35, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 35, - "type": "text", - "val": "five", - }, - Object { - "line": 37, - "type": "newline", - }, - Object { - "line": 37, - "type": "class", - "val": "foo", - }, - Object { - "line": 37, - "type": "text", - "val": "// not a comment", - }, - Object { - "line": 38, - "type": "newline", - }, - Object { - "line": 38, - "type": "eos", - }, -] -`; - -exports[`comments.source.input.json 1`] = ` -Array [ - Object { - "line": 4, - "type": "newline", - }, - Object { - "line": 6, - "type": "newline", - }, - Object { - "line": 9, - "type": "eos", - }, -] -`; - -exports[`comments.source.input.json 2`] = ` -Array [ - Object { - "buffer": false, - "line": 1, - "type": "comment", - "val": "", - }, - Object { - "line": 1, - "type": "start-pipeless-text", - }, - Object { - "line": 2, - "type": "text", - "val": "s/s.", - }, - Object { - "line": 3, - "type": "newline", - }, - Object { - "line": 3, - "type": "text", - "val": "", - }, - Object { - "line": 3, - "type": "end-pipeless-text", - }, - Object { - "line": 4, - "type": "newline", - }, - Object { - "buffer": false, - "line": 4, - "type": "comment", - "val": " test/cases/comments.source.pug", - }, - Object { - "line": 5, - "type": "text", - "val": "", - }, - Object { - "line": 6, - "type": "newline", - }, - Object { - "buffer": false, - "line": 6, - "type": "comment", - "val": "", - }, - Object { - "line": 6, - "type": "start-pipeless-text", - }, - Object { - "line": 7, - "type": "text", - "val": "test/cases/comments.source.pug", - }, - Object { - "line": 8, - "type": "newline", - }, - Object { - "line": 8, - "type": "text", - "val": "when", - }, - Object { - "line": 9, - "type": "newline", - }, - Object { - "line": 9, - "type": "text", - "val": "()", - }, - Object { - "line": 9, - "type": "end-pipeless-text", - }, - Object { - "line": 9, - "type": "eos", - }, -] -`; - -exports[`comments.source.input.json 3`] = ` -Array [ - Object { - "line": 4, - "type": "newline", - }, - Object { - "line": 6, - "type": "newline", - }, - Object { - "line": 9, - "type": "eos", - }, -] -`; - -exports[`comments-in-case.input.json 1`] = ` -Array [ - Object { - "line": 1, - "type": "doctype", - "val": "html", - }, - Object { - "line": 2, - "type": "newline", - }, - Object { - "line": 2, - "selfClosing": false, - "type": "tag", - "val": "html", - }, - Object { - "line": 3, - "type": "indent", - "val": 2, - }, - Object { - "line": 3, - "selfClosing": false, - "type": "tag", - "val": "body", - }, - Object { - "line": 4, - "type": "indent", - "val": 3, - }, - Object { - "buffer": false, - "escape": false, - "line": 4, - "type": "code", - "val": "var s = 'this'", - }, - Object { - "line": 5, - "type": "newline", - }, - Object { - "line": 5, - "type": "case", - "val": "s", - }, - Object { - "line": 6, - "type": "indent", - "val": 5, - }, - Object { - "line": 7, - "type": "newline", - }, - Object { - "line": 7, - "type": "when", - "val": "'this'", - }, - Object { - "line": 8, - "type": "indent", - "val": 7, - }, - Object { - "line": 8, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 8, - "type": "text", - "val": "It's this!", - }, - Object { - "line": 9, - "type": "outdent", - }, - Object { - "line": 9, - "type": "when", - "val": "'that'", - }, - Object { - "line": 10, - "type": "indent", - "val": 7, - }, - Object { - "line": 10, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 10, - "type": "text", - "val": "It's that!", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "eos", - }, -] -`; - -exports[`comments-in-case.input.json 2`] = ` -Array [ - Object { - "line": 1, - "type": "doctype", - "val": "html", - }, - Object { - "line": 2, - "type": "newline", - }, - Object { - "line": 2, - "selfClosing": false, - "type": "tag", - "val": "html", - }, - Object { - "line": 3, - "type": "indent", - "val": 2, - }, - Object { - "line": 3, - "selfClosing": false, - "type": "tag", - "val": "body", - }, - Object { - "line": 4, - "type": "indent", - "val": 3, - }, - Object { - "buffer": false, - "escape": false, - "line": 4, - "type": "code", - "val": "var s = 'this'", - }, - Object { - "line": 5, - "type": "newline", - }, - Object { - "line": 5, - "type": "case", - "val": "s", - }, - Object { - "line": 6, - "type": "indent", - "val": 5, - }, - Object { - "buffer": false, - "line": 6, - "type": "comment", - "val": " Comment", - }, - Object { - "line": 7, - "type": "newline", - }, - Object { - "line": 7, - "type": "when", - "val": "'this'", - }, - Object { - "line": 8, - "type": "indent", - "val": 7, - }, - Object { - "line": 8, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 8, - "type": "text", - "val": "It's this!", - }, - Object { - "line": 9, - "type": "outdent", - }, - Object { - "line": 9, - "type": "when", - "val": "'that'", - }, - Object { - "line": 10, - "type": "indent", - "val": 7, - }, - Object { - "line": 10, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 10, - "type": "text", - "val": "It's that!", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "eos", - }, -] -`; - -exports[`comments-in-case.input.json 3`] = ` -Array [ - Object { - "line": 1, - "type": "doctype", - "val": "html", - }, - Object { - "line": 2, - "type": "newline", - }, - Object { - "line": 2, - "selfClosing": false, - "type": "tag", - "val": "html", - }, - Object { - "line": 3, - "type": "indent", - "val": 2, - }, - Object { - "line": 3, - "selfClosing": false, - "type": "tag", - "val": "body", - }, - Object { - "line": 4, - "type": "indent", - "val": 3, - }, - Object { - "buffer": false, - "escape": false, - "line": 4, - "type": "code", - "val": "var s = 'this'", - }, - Object { - "line": 5, - "type": "newline", - }, - Object { - "line": 5, - "type": "case", - "val": "s", - }, - Object { - "line": 6, - "type": "indent", - "val": 5, - }, - Object { - "line": 7, - "type": "newline", - }, - Object { - "line": 7, - "type": "when", - "val": "'this'", - }, - Object { - "line": 8, - "type": "indent", - "val": 7, - }, - Object { - "line": 8, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 8, - "type": "text", - "val": "It's this!", - }, - Object { - "line": 9, - "type": "outdent", - }, - Object { - "line": 9, - "type": "when", - "val": "'that'", - }, - Object { - "line": 10, - "type": "indent", - "val": 7, - }, - Object { - "line": 10, - "selfClosing": false, - "type": "tag", - "val": "p", - }, - Object { - "line": 10, - "type": "text", - "val": "It's that!", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "outdent", - }, - Object { - "line": 10, - "type": "eos", - }, -] -`; - -exports[`end.input.json 1`] = ` -Object { - "code": "PUG:UNEXPECTED_TOKEN", - "line": 4, - "msg": "\`end-pipeless-text\` encountered when not in pipeless text mode", -} -`; - -exports[`startstart.input.json 1`] = ` -Object { - "code": "PUG:UNEXPECTED_TOKEN", - "line": 3, - "msg": "\`start-pipeless-text\` encountered when already in pipeless text mode", -} -`; diff --git a/src/test-data/pug-strip-comments/test/cases/comments-in-case.input.json b/src/test-data/pug-strip-comments/test/cases/comments-in-case.input.json deleted file mode 100644 index 161122d..0000000 --- a/src/test-data/pug-strip-comments/test/cases/comments-in-case.input.json +++ /dev/null @@ -1,26 +0,0 @@ -{"type":"doctype","line":1,"val":"html"} -{"type":"newline","line":2} -{"type":"tag","line":2,"val":"html","selfClosing":false} -{"type":"indent","line":3,"val":2} -{"type":"tag","line":3,"val":"body","selfClosing":false} -{"type":"indent","line":4,"val":3} -{"type":"code","line":4,"val":"var s = 'this'","escape":false,"buffer":false} -{"type":"newline","line":5} -{"type":"case","line":5,"val":"s"} -{"type":"indent","line":6,"val":5} -{"type":"comment","line":6,"val":" Comment","buffer":false} -{"type":"newline","line":7} -{"type":"when","line":7,"val":"'this'"} -{"type":"indent","line":8,"val":7} -{"type":"tag","line":8,"val":"p","selfClosing":false} -{"type":"text","line":8,"val":"It's this!"} -{"type":"outdent","line":9} -{"type":"when","line":9,"val":"'that'"} -{"type":"indent","line":10,"val":7} -{"type":"tag","line":10,"val":"p","selfClosing":false} -{"type":"text","line":10,"val":"It's that!"} -{"type":"outdent","line":10} -{"type":"outdent","line":10} -{"type":"outdent","line":10} -{"type":"outdent","line":10} -{"type":"eos","line":10} \ No newline at end of file diff --git a/src/test-data/pug-strip-comments/test/cases/comments.input.json b/src/test-data/pug-strip-comments/test/cases/comments.input.json deleted file mode 100644 index a8c0206..0000000 --- a/src/test-data/pug-strip-comments/test/cases/comments.input.json +++ /dev/null @@ -1,76 +0,0 @@ -{"type":"newline","line":2} -{"type":"comment","line":2,"val":" foo","buffer":true} -{"type":"newline","line":3} -{"type":"tag","line":3,"val":"ul","selfClosing":false} -{"type":"indent","line":4,"val":2} -{"type":"comment","line":4,"val":" bar","buffer":true} -{"type":"newline","line":5} -{"type":"tag","line":5,"val":"li","selfClosing":false} -{"type":"text","line":5,"val":"one"} -{"type":"newline","line":6} -{"type":"comment","line":6,"val":" baz","buffer":true} -{"type":"newline","line":7} -{"type":"tag","line":7,"val":"li","selfClosing":false} -{"type":"text","line":7,"val":"two"} -{"type":"outdent","line":9} -{"type":"comment","line":9,"val":"","buffer":true} -{"type":"start-pipeless-text","line":9} -{"type":"text","line":10,"val":"ul"} -{"type":"newline","line":11} -{"type":"text","line":11,"val":" li foo"} -{"type":"newline","line":12} -{"type":"text","line":12,"val":""} -{"type":"end-pipeless-text","line":12} -{"type":"newline","line":13} -{"type":"comment","line":13,"val":" block","buffer":true} -{"type":"start-pipeless-text","line":13} -{"type":"text","line":14,"val":"// inline follow"} -{"type":"newline","line":15} -{"type":"text","line":15,"val":"li three"} -{"type":"newline","line":16} -{"type":"text","line":16,"val":""} -{"type":"end-pipeless-text","line":16} -{"type":"newline","line":17} -{"type":"comment","line":17,"val":" block","buffer":true} -{"type":"start-pipeless-text","line":17} -{"type":"text","line":18,"val":"// inline followed by tags"} -{"type":"newline","line":19} -{"type":"text","line":19,"val":"ul"} -{"type":"newline","line":20} -{"type":"text","line":20,"val":" li four"} -{"type":"newline","line":21} -{"type":"text","line":21,"val":""} -{"type":"end-pipeless-text","line":21} -{"type":"newline","line":22} -{"type":"comment","line":22,"val":"if IE lt 9","buffer":true} -{"type":"start-pipeless-text","line":22} -{"type":"text","line":23,"val":"// inline"} -{"type":"newline","line":24} -{"type":"text","line":24,"val":"script(src='/lame.js')"} -{"type":"newline","line":25} -{"type":"text","line":25,"val":"// end-inline"} -{"type":"newline","line":26} -{"type":"text","line":26,"val":""} -{"type":"end-pipeless-text","line":26} -{"type":"newline","line":27} -{"type":"comment","line":27,"val":"","buffer":false} -{"type":"text","line":28,"val":""} -{"type":"start-pipeless-text","line":28} -{"type":"text","line":29,"val":"asfd"} -{"type":"newline","line":30} -{"type":"text","line":30,"val":""} -{"type":"end-pipeless-text","line":30} -{"type":"newline","line":31} -{"type":"comment","line":31,"val":"","buffer":false} -{"type":"newline","line":32} -{"type":"text","line":32,"val":" "} -{"type":"newline","line":33} -{"type":"text","line":33,"val":"asfd"} -{"type":"newline","line":35} -{"type":"tag","line":35,"val":"p","selfClosing":false} -{"type":"text","line":35,"val":"five"} -{"type":"newline","line":37} -{"type":"class","line":37,"val":"foo"} -{"type":"text","line":37,"val":"// not a comment"} -{"type":"newline","line":38} -{"type":"eos","line":38} \ No newline at end of file diff --git a/src/test-data/pug-strip-comments/test/cases/comments.source.input.json b/src/test-data/pug-strip-comments/test/cases/comments.source.input.json deleted file mode 100644 index fba08da..0000000 --- a/src/test-data/pug-strip-comments/test/cases/comments.source.input.json +++ /dev/null @@ -1,19 +0,0 @@ -{"type":"comment","line":1,"val":"","buffer":false} -{"type":"start-pipeless-text","line":1} -{"type":"text","line":2,"val":"s/s."} -{"type":"newline","line":3} -{"type":"text","line":3,"val":""} -{"type":"end-pipeless-text","line":3} -{"type":"newline","line":4} -{"type":"comment","line":4,"val":" test/cases/comments.source.pug","buffer":false} -{"type":"text","line":5,"val":""} -{"type":"newline","line":6} -{"type":"comment","line":6,"val":"","buffer":false} -{"type":"start-pipeless-text","line":6} -{"type":"text","line":7,"val":"test/cases/comments.source.pug"} -{"type":"newline","line":8} -{"type":"text","line":8,"val":"when"} -{"type":"newline","line":9} -{"type":"text","line":9,"val":"()"} -{"type":"end-pipeless-text","line":9} -{"type":"eos","line":9} \ No newline at end of file diff --git a/src/test-data/pug-strip-comments/test/errors/comment-in-comment.input.json b/src/test-data/pug-strip-comments/test/errors/comment-in-comment.input.json deleted file mode 100644 index 771e19d..0000000 --- a/src/test-data/pug-strip-comments/test/errors/comment-in-comment.input.json +++ /dev/null @@ -1,3 +0,0 @@ -{"type":"comment","line":1,"val":"","buffer":false} -{"type":"comment","line":2,"val":"","buffer":false} -{"type":"eos","line":3} \ No newline at end of file diff --git a/src/test-data/pug-strip-comments/test/errors/end.input.json b/src/test-data/pug-strip-comments/test/errors/end.input.json deleted file mode 100644 index c99176d..0000000 --- a/src/test-data/pug-strip-comments/test/errors/end.input.json +++ /dev/null @@ -1,4 +0,0 @@ -{"type":"comment","line":1,"val":"","buffer":false} -{"type":"end-pipeless-text","line":4} -{"type":"newline","line":4} -{"type":"eos","line":5} \ No newline at end of file diff --git a/src/test-data/pug-strip-comments/test/errors/startstart.input.json b/src/test-data/pug-strip-comments/test/errors/startstart.input.json deleted file mode 100644 index 6a97589..0000000 --- a/src/test-data/pug-strip-comments/test/errors/startstart.input.json +++ /dev/null @@ -1,6 +0,0 @@ -{"type":"comment","line":1,"val":"","buffer":false} -{"type":"start-pipeless-text","line":2} -{"type":"start-pipeless-text","line":3} -{"type":"end-pipeless-text","line":4} -{"type":"newline","line":4} -{"type":"eos","line":5} \ No newline at end of file diff --git a/src/test-data/pug-strip-comments/test/index.test.js b/src/test-data/pug-strip-comments/test/index.test.js deleted file mode 100644 index f0e3ffc..0000000 --- a/src/test-data/pug-strip-comments/test/index.test.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var assert = require('assert'); -var lineJSON = require('line-json'); -var strip = require('../'); - -var dir = __dirname + '/cases/'; -fs.readdirSync(dir).forEach(function(testCase) { - if (/\.input\.json$/.test(testCase)) { - test(testCase, function() { - var stem = testCase.replace(/\.input\.json$/, '.'); - - function test(name, options) { - var input = fs.readFileSync(dir + testCase, 'utf8'); - input = lineJSON.parse(input); - - var result = strip(input, options); - expect(result).toMatchSnapshot(); - } - - test('unbuffered'); - test('buffered', {stripBuffered: true, stripUnbuffered: false}); - test('both', {stripBuffered: true}); - }); - } -}); - -var edir = __dirname + '/errors/'; -fs.readdirSync(edir).forEach(function(testCase) { - if (/\.input\.json$/.test(testCase)) { - test(testCase, function() { - var stem = testCase.replace(/\.input\.json$/, '.'); - - var input = fs.readFileSync(edir + testCase, 'utf8'); - input = lineJSON.parse(input); - - try { - strip(input); - throw new Error('Expected ' + testCase + ' to throw an exception.'); - } catch (ex) { - if (!ex || !ex.code || ex.code.indexOf('PUG:') !== 0) throw ex; - expect({ - msg: ex.msg, - code: ex.code, - line: ex.line, - }).toMatchSnapshot(); - } - }); - } -}); diff --git a/src/test-data/pug-walk/.gitignore b/src/test-data/pug-walk/.gitignore deleted file mode 100644 index b5a73bd..0000000 --- a/src/test-data/pug-walk/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -lib-cov -coverage -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz -pids -logs -results -npm-debug.log -node_modules diff --git a/src/test-data/pug-walk/.travis.yml b/src/test-data/pug-walk/.travis.yml deleted file mode 100644 index 7c816d2..0000000 --- a/src/test-data/pug-walk/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js -sudo: false - -node_js: - - "0.10" - - "0.12" - - "4" - - "6" - -after_success: - - npm run coverage - - npm i codecov - - codecov -f ./coverage/lcov.info - -notifications: - email: - on_success: never diff --git a/src/test-data/pug-walk/HISTORY.md b/src/test-data/pug-walk/HISTORY.md deleted file mode 100644 index 0892a9b..0000000 --- a/src/test-data/pug-walk/HISTORY.md +++ /dev/null @@ -1,4 +0,0 @@ -1.0.0 / 2016-08-22 -================== - - * Initial stable release diff --git a/src/test-data/pug-walk/LICENSE b/src/test-data/pug-walk/LICENSE deleted file mode 100644 index a1421ff..0000000 --- a/src/test-data/pug-walk/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2015 Forbes Lindesay - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/src/test-data/pug-walk/README.md b/src/test-data/pug-walk/README.md deleted file mode 100644 index 90da52e..0000000 --- a/src/test-data/pug-walk/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# pug-walk - -Walk and transform a Pug AST - -[![Build Status](https://img.shields.io/travis/pugjs/pug-walk/master.svg)](https://travis-ci.org/pugjs/pug-walk) -[![Dependencies Status](https://david-dm.org/pugjs/pug/status.svg?path=packages/pug-walk)](https://david-dm.org/pugjs/pug?path=packages/pug-walk) -[![DevDependencies Status](https://david-dm.org/pugjs/pug/dev-status.svg?path=packages/pug-walk)](https://david-dm.org/pugjs/pug?path=packages/pug-walk&type=dev) -[![npm version](https://img.shields.io/npm/v/pug-walk.svg)](https://www.npmjs.org/package/pug-walk) -[![Coverage Status](https://img.shields.io/codecov/c/github/pugjs/pug-walk/master.svg)](https://codecov.io/gh/pugjs/pug-walk/branch/master) - -## Installation - - npm install pug-walk - -## Usage - -```js -var walk = require('pug-walk'); -``` - -### `walk(ast, before, after, options)` - -Traverse and optionally transform a [Pug AST](https://github.com/pugjs/pug-ast-spec). - -`ast` is not cloned, so any changes done to it will be done directly on the AST provided. - -`before` and `after` are functions with the signature `(node, replace)`. `before` is called when a node is first seen, while `after` is called after the children of the node (if any) have already been traversed. - -The `replace` parameter is a function that can be used to replace the node in the AST. It takes either an object or an array as its only parameter. If an object is specified, the current node is replaced by the parameter in the AST. If an array is specified and the ancestor of the current node allows such an operation, the node is replaced by all of the nodes in the specified array. This way, you can remove and add new nodes adjacent to the current node. Whether the parent node allows array operation is indicated by the property `replace.arrayAllowed`, which is set to true when the parent is a Block and when the parent is a Include and the node is an IncludeFilter. - -If `before` returns `false`, the children of this node will not be traversed and left unchanged (unless `replace` has been called). Otherwise, the returned value of `before` is ignored. The returned value of `after` is always ignored. If `replace()` is called in `before()` with an array, and `before()` does not return `false`, the nodes in the array are still descended. - -`options` can contain the following properties: - -* `includeDependencies` (boolean): Walk the AST of a loaded dependent file (i.e., includes and extends). Defaults to `false`. -* `parents` (array): Nodes that are ancestors to the current `ast`. This option is used mainly internally, and users usually do not have to specify it. Defaults to `[]`. - -```js -var lex = require('pug-lexer'); -var parse = require('pug-parser'); - -// Changing content of all Text nodes -// ================================== - -var source = '.my-class foo'; -var dest = '.my-class bar'; - -var ast = parse(lex(source)); - -ast = walk(ast, function before(node, replace) { - if (node.type === 'Text') { - node.val = 'bar'; - - // Alternatively, you can replace the entire node - // rather than just the text. - // replace({ type: 'Text', val: 'bar', line: node.line }); - } -}, { - includeDependencies: true -}); - -assert.deepEqual(parse(lex(dest)), ast); - -// Convert all simple elements to text -// ============================================ - -var source = 'p abc #[strong NO]\nstrong on its own line'; -var dest = 'p abc #[| NO]\n| on its own line'; - -var ast = parse(lex(source)); - -ast = walk(ast, function before(node, replace) { - // Find all tags - if (node.type === 'Tag' && node.name === 'strong') { - var children = node.block.nodes; - - // Make sure that the Tag only has one child -- the text - if (children.length === 1 && children[0].type === 'Text') { - // Replace the Tag with the Text - replace({ type: 'Text', val: children[0].val, line: node.line }); - } - } -}, { - includeDependencies: true -}); - -assert.deepEqual(parse(lex(dest)), ast); - -// Flatten blocks -// ============== - -var ast = { - type: 'Block', - nodes: [ - { type: 'Text', val: 'a' }, - { - type: 'Block', - nodes: [ - { type: 'Text', val: 'b' }, - { - type: 'Block', - nodes: [ { type: 'Text', val: 'c' } ] - }, - { type: 'Text', val: 'd' } - ] - }, - { type: 'Text', val: 'e' } - ] -}; - -var dest = { - type: 'Block', - nodes: [ - { type: 'Text', val: 'a' }, - { type: 'Text', val: 'b' }, - { type: 'Text', val: 'c' }, - { type: 'Text', val: 'd' }, - { type: 'Text', val: 'e' } - ] -}; - -// We need to use `after` handler instead of `before` -// handler because we want to flatten the innermost -// blocks first before proceeding onto outer blocks. - -ast = walk(ast, null, function after(node, replace) { - if (node.type === 'Block' && replace.arrayAllowed) { - // Replace the block with its contents - replace(node.nodes); - } -}); - -assert.deepEqual(dest, ast); -``` - -## License - - MIT diff --git a/src/test-data/pug-walk/index.js b/src/test-data/pug-walk/index.js deleted file mode 100644 index 9972e4b..0000000 --- a/src/test-data/pug-walk/index.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -module.exports = walkAST; -function walkAST(ast, before, after, options) { - if (after && typeof after === 'object' && typeof options === 'undefined') { - options = after; - after = null; - } - options = options || {includeDependencies: false}; - var parents = (options.parents = options.parents || []); - - var replace = function replace(replacement) { - if (Array.isArray(replacement) && !replace.arrayAllowed) { - throw new Error( - 'replace() can only be called with an array if the last parent is a Block or NamedBlock' - ); - } - ast = replacement; - }; - replace.arrayAllowed = - parents[0] && - (/^(Named)?Block$/.test(parents[0].type) || - (parents[0].type === 'RawInclude' && ast.type === 'IncludeFilter')); - - if (before) { - var result = before(ast, replace); - if (result === false) { - return ast; - } else if (Array.isArray(ast)) { - // return right here to skip after() call on array - return walkAndMergeNodes(ast); - } - } - - parents.unshift(ast); - - switch (ast.type) { - case 'NamedBlock': - case 'Block': - ast.nodes = walkAndMergeNodes(ast.nodes); - break; - case 'Case': - case 'Filter': - case 'Mixin': - case 'Tag': - case 'InterpolatedTag': - case 'When': - case 'Code': - case 'While': - if (ast.block) { - ast.block = walkAST(ast.block, before, after, options); - } - break; - case 'Each': - if (ast.block) { - ast.block = walkAST(ast.block, before, after, options); - } - if (ast.alternate) { - ast.alternate = walkAST(ast.alternate, before, after, options); - } - break; - case 'EachOf': - if (ast.block) { - ast.block = walkAST(ast.block, before, after, options); - } - break; - case 'Conditional': - if (ast.consequent) { - ast.consequent = walkAST(ast.consequent, before, after, options); - } - if (ast.alternate) { - ast.alternate = walkAST(ast.alternate, before, after, options); - } - break; - case 'Include': - walkAST(ast.block, before, after, options); - walkAST(ast.file, before, after, options); - break; - case 'Extends': - walkAST(ast.file, before, after, options); - break; - case 'RawInclude': - ast.filters = walkAndMergeNodes(ast.filters); - walkAST(ast.file, before, after, options); - break; - case 'Attrs': - case 'BlockComment': - case 'Comment': - case 'Doctype': - case 'IncludeFilter': - case 'MixinBlock': - case 'YieldBlock': - case 'Text': - break; - case 'FileReference': - if (options.includeDependencies && ast.ast) { - walkAST(ast.ast, before, after, options); - } - break; - default: - throw new Error('Unexpected node type ' + ast.type); - break; - } - - parents.shift(); - - after && after(ast, replace); - return ast; - - function walkAndMergeNodes(nodes) { - return nodes.reduce(function(nodes, node) { - var result = walkAST(node, before, after, options); - if (Array.isArray(result)) { - return nodes.concat(result); - } else { - return nodes.concat([result]); - } - }, []); - } -} diff --git a/src/test-data/pug-walk/package.json b/src/test-data/pug-walk/package.json deleted file mode 100644 index 2f77584..0000000 --- a/src/test-data/pug-walk/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "pug-walk", - "version": "1.1.8", - "description": "Walk and transform a pug AST", - "keywords": [ - "pug" - ], - "devDependencies": { - "pug-lexer": "^4.1.0", - "pug-parser": "^5.0.1" - }, - "files": [ - "index.js" - ], - "repository": { - "type": "git", - "url": "https://github.com/pugjs/pug/tree/master/packages/pug-walk" - }, - "author": "ForbesLindesay", - "license": "MIT" -} diff --git a/src/test-data/pug-walk/test/index.test.js b/src/test-data/pug-walk/test/index.test.js deleted file mode 100644 index 70c4698..0000000 --- a/src/test-data/pug-walk/test/index.test.js +++ /dev/null @@ -1,178 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var lex = require('pug-lexer'); -var parse = require('pug-parser'); -var walk = require('../'); - -test('simple', function() { - var ast = walk( - parse(lex('.my-class foo')), - function before(node, replace) { - // called before walking the children of `node` - // to replace the node, call `replace(newNode)` - // return `false` to skip descending - if (node.type === 'Text') { - replace({ - type: 'Text', - val: 'bar', - line: node.line, - column: node.column, - }); - } - }, - function after(node, replace) { - // called before walking the children of `node` - // to replace the node, call `replace(newNode)` - } - ); - expect(ast).toEqual(parse(lex('.my-class bar'))); -}); - -describe('replace([])', function() { - test('block flattening', function() { - var called = []; - var ast = walk( - { - type: 'Block', - nodes: [ - { - type: 'Block', - nodes: [ - { - type: 'Block', - nodes: [ - { - type: 'Text', - val: 'a', - }, - { - type: 'Text', - val: 'b', - }, - ], - }, - { - type: 'Text', - val: 'c', - }, - ], - }, - { - type: 'Text', - val: 'd', - }, - ], - }, - function(node, replace) { - if (node.type === 'Text') { - called.push('before ' + node.val); - if (node.val === 'a') { - assert(replace.arrayAllowed, 'replace.arrayAllowed set wrongly'); - replace([ - { - type: 'Text', - val: 'e', - }, - { - type: 'Text', - val: 'f', - }, - ]); - } - } - }, - function(node, replace) { - if (node.type === 'Block' && replace.arrayAllowed) { - replace(node.nodes); - } else if (node.type === 'Text') { - called.push('after ' + node.val); - } - } - ); - - expect(ast).toEqual({ - type: 'Block', - nodes: [ - {type: 'Text', val: 'e'}, - {type: 'Text', val: 'f'}, - {type: 'Text', val: 'b'}, - {type: 'Text', val: 'c'}, - {type: 'Text', val: 'd'}, - ], - }); - - assert.deepEqual( - called, - [ - 'before a', - - 'before e', - 'after e', - - 'before f', - 'after f', - - 'before b', - 'after b', - - 'before c', - 'after c', - - 'before d', - 'after d', - ], - 'before() and after() called incorrectly: ' + JSON.stringify(called) - ); - }); - - test('adding include filters', function() { - var ast = walk(parse(lex('include:filter1:filter2 file')), function( - node, - replace - ) { - if (node.type === 'IncludeFilter') { - assert(replace.arrayAllowed); - if (node.name === 'filter1') { - var firstFilter = 'filter3'; - - replace([ - { - type: 'IncludeFilter', - name: firstFilter, - attrs: [], - line: node.line, - column: node.column, - }, - { - type: 'IncludeFilter', - name: 'filter4', - attrs: [], - line: node.line, - column: node.column + firstFilter.length + 1, - }, - ]); - } else if (node.name === 'filter2') { - replace([]); - } - } - }); - - expect(ast).toEqual(parse(lex('include:filter3:filter4 file'))); - }); - - test('fails when parent is not Block', function() { - walk(parse(lex('p content')), function(node, replace) { - if ( - node.type === 'Block' && - node.nodes[0] && - node.nodes[0].type === 'Text' - ) { - assert(!replace.arrayAllowed, 'replace.arrayAllowed set wrongly'); - assert.throws(function() { - replace([]); - }); - } - }); - }); -}); diff --git a/src/tests/check_list_test.zig b/src/tests/check_list_test.zig index d99f400..524a7e6 100644 --- a/src/tests/check_list_test.zig +++ b/src/tests/check_list_test.zig @@ -27,6 +27,10 @@ fn normalizeHtml(allocator: std.mem.Allocator, html: []const u8) ![]const u8 { const c = html[i]; if (c == '<') { + // Strip trailing whitespace before tags + while (result.items.len > 0 and (result.items[result.items.len - 1] == ' ' or result.items[result.items.len - 1] == '\t')) { + _ = result.pop(); + } in_tag = true; last_was_space = false; try result.append(allocator, c); @@ -34,11 +38,8 @@ fn normalizeHtml(allocator: std.mem.Allocator, html: []const u8) ![]const u8 { in_tag = false; last_was_space = false; try result.append(allocator, c); - } else if (c == '\n' or c == '\r') { - // Skip newlines - i += 1; - continue; - } else if (c == ' ' or c == '\t') { + } else if (c == '\n' or c == '\r' or c == ' ' or c == '\t') { + // Treat all whitespace (including newlines) uniformly if (in_tag) { // Preserve single space in tags for attribute separation if (!last_was_space) { diff --git a/src/tests/helper.zig b/src/tests/helper.zig index d37fa55..31d8e97 100644 --- a/src/tests/helper.zig +++ b/src/tests/helper.zig @@ -16,6 +16,10 @@ fn normalizeHtml(allocator: std.mem.Allocator, html: []const u8) ![]const u8 { const c = html[i]; if (c == '<') { + // Strip trailing whitespace before tags + while (result.items.len > 0 and (result.items[result.items.len - 1] == ' ' or result.items[result.items.len - 1] == '\t')) { + _ = result.pop(); + } in_tag = true; last_was_space = false; try result.append(allocator, c); @@ -23,11 +27,8 @@ fn normalizeHtml(allocator: std.mem.Allocator, html: []const u8) ![]const u8 { in_tag = false; last_was_space = false; try result.append(allocator, c); - } else if (c == '\n' or c == '\r') { - // Skip newlines - i += 1; - continue; - } else if (c == ' ' or c == '\t') { + } else if (c == '\n' or c == '\r' or c == ' ' or c == '\t') { + // Treat all whitespace (including newlines) uniformly if (in_tag) { // Preserve single space in tags for attribute separation if (!last_was_space) { diff --git a/src/run_playground.zig b/src/tests/run_playground.zig similarity index 100% rename from src/run_playground.zig rename to src/tests/run_playground.zig diff --git a/src/test-data/pug-attrs/index.test.js b/src/tests/sample_data/pug-attrs/index.test.js similarity index 100% rename from src/test-data/pug-attrs/index.test.js rename to src/tests/sample_data/pug-attrs/index.test.js diff --git a/src/test-data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap b/src/tests/sample_data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap similarity index 100% rename from src/test-data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap rename to src/tests/sample_data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap diff --git a/src/test-data/pug-filters/test/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug-filters/test/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug-filters/test/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug-filters/test/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap b/src/tests/sample_data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap similarity index 100% rename from src/test-data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap rename to src/tests/sample_data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap diff --git a/src/test-data/pug-filters/test/cases/filters-empty.input.json b/src/tests/sample_data/pug-filters/test/cases/filters-empty.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters-empty.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters-empty.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.cdata.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.cdata.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.cdata.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.cdata.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.coffeescript.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.coffeescript.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.coffeescript.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.coffeescript.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.custom.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.custom.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.custom.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.custom.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.include.custom.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.include.custom.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.include.custom.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.include.custom.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.include.custom.pug b/src/tests/sample_data/pug-filters/test/cases/filters.include.custom.pug similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.include.custom.pug rename to src/tests/sample_data/pug-filters/test/cases/filters.include.custom.pug diff --git a/src/test-data/pug-filters/test/cases/filters.include.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.include.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.include.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.include.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.inline.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.inline.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.inline.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.inline.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.less.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.less.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.less.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.less.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.markdown.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.markdown.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.markdown.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.markdown.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.nested.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.nested.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.nested.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.nested.input.json diff --git a/src/test-data/pug-filters/test/cases/filters.stylus.input.json b/src/tests/sample_data/pug-filters/test/cases/filters.stylus.input.json similarity index 100% rename from src/test-data/pug-filters/test/cases/filters.stylus.input.json rename to src/tests/sample_data/pug-filters/test/cases/filters.stylus.input.json diff --git a/src/test-data/pug-filters/test/cases/include-filter-coffee.coffee b/src/tests/sample_data/pug-filters/test/cases/include-filter-coffee.coffee similarity index 100% rename from src/test-data/pug-filters/test/cases/include-filter-coffee.coffee rename to src/tests/sample_data/pug-filters/test/cases/include-filter-coffee.coffee diff --git a/src/test-data/pug-filters/test/cases/some.md b/src/tests/sample_data/pug-filters/test/cases/some.md similarity index 100% rename from src/test-data/pug-filters/test/cases/some.md rename to src/tests/sample_data/pug-filters/test/cases/some.md diff --git a/src/test-data/pug-filters/test/custom-filters.js b/src/tests/sample_data/pug-filters/test/custom-filters.js similarity index 100% rename from src/test-data/pug-filters/test/custom-filters.js rename to src/tests/sample_data/pug-filters/test/custom-filters.js diff --git a/src/test-data/pug-filters/test/errors-src/dynamic-option.jade b/src/tests/sample_data/pug-filters/test/errors-src/dynamic-option.jade similarity index 100% rename from src/test-data/pug-filters/test/errors-src/dynamic-option.jade rename to src/tests/sample_data/pug-filters/test/errors-src/dynamic-option.jade diff --git a/src/test-data/pug-filters/test/errors/dynamic-option.input.json b/src/tests/sample_data/pug-filters/test/errors/dynamic-option.input.json similarity index 100% rename from src/test-data/pug-filters/test/errors/dynamic-option.input.json rename to src/tests/sample_data/pug-filters/test/errors/dynamic-option.input.json diff --git a/src/test-data/pug-filters/test/filter-aliases.test.js b/src/tests/sample_data/pug-filters/test/filter-aliases.test.js similarity index 100% rename from src/test-data/pug-filters/test/filter-aliases.test.js rename to src/tests/sample_data/pug-filters/test/filter-aliases.test.js diff --git a/src/test-data/pug-filters/test/index.test.js b/src/tests/sample_data/pug-filters/test/index.test.js similarity index 100% rename from src/test-data/pug-filters/test/index.test.js rename to src/tests/sample_data/pug-filters/test/index.test.js diff --git a/src/test-data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js b/src/tests/sample_data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js similarity index 100% rename from src/test-data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js rename to src/tests/sample_data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js diff --git a/src/test-data/pug-lexer/cases/attr-es2015.pug b/src/tests/sample_data/pug-lexer/cases/attr-es2015.pug similarity index 100% rename from src/test-data/pug-lexer/cases/attr-es2015.pug rename to src/tests/sample_data/pug-lexer/cases/attr-es2015.pug diff --git a/src/test-data/pug-lexer/cases/attrs-data.pug b/src/tests/sample_data/pug-lexer/cases/attrs-data.pug similarity index 100% rename from src/test-data/pug-lexer/cases/attrs-data.pug rename to src/tests/sample_data/pug-lexer/cases/attrs-data.pug diff --git a/src/test-data/pug-lexer/cases/attrs.js.pug b/src/tests/sample_data/pug-lexer/cases/attrs.js.pug similarity index 100% rename from src/test-data/pug-lexer/cases/attrs.js.pug rename to src/tests/sample_data/pug-lexer/cases/attrs.js.pug diff --git a/src/test-data/pug-lexer/cases/attrs.pug b/src/tests/sample_data/pug-lexer/cases/attrs.pug similarity index 100% rename from src/test-data/pug-lexer/cases/attrs.pug rename to src/tests/sample_data/pug-lexer/cases/attrs.pug diff --git a/src/test-data/pug-lexer/cases/attrs.unescaped.pug b/src/tests/sample_data/pug-lexer/cases/attrs.unescaped.pug similarity index 100% rename from src/test-data/pug-lexer/cases/attrs.unescaped.pug rename to src/tests/sample_data/pug-lexer/cases/attrs.unescaped.pug diff --git a/src/test-data/pug-lexer/cases/basic.pug b/src/tests/sample_data/pug-lexer/cases/basic.pug similarity index 100% rename from src/test-data/pug-lexer/cases/basic.pug rename to src/tests/sample_data/pug-lexer/cases/basic.pug diff --git a/src/test-data/pug-lexer/cases/blanks.pug b/src/tests/sample_data/pug-lexer/cases/blanks.pug similarity index 100% rename from src/test-data/pug-lexer/cases/blanks.pug rename to src/tests/sample_data/pug-lexer/cases/blanks.pug diff --git a/src/test-data/pug-lexer/cases/block-code.pug b/src/tests/sample_data/pug-lexer/cases/block-code.pug similarity index 100% rename from src/test-data/pug-lexer/cases/block-code.pug rename to src/tests/sample_data/pug-lexer/cases/block-code.pug diff --git a/src/test-data/pug-lexer/cases/block-expansion.pug b/src/tests/sample_data/pug-lexer/cases/block-expansion.pug similarity index 100% rename from src/test-data/pug-lexer/cases/block-expansion.pug rename to src/tests/sample_data/pug-lexer/cases/block-expansion.pug diff --git a/src/test-data/pug-lexer/cases/block-expansion.shorthands.pug b/src/tests/sample_data/pug-lexer/cases/block-expansion.shorthands.pug similarity index 100% rename from src/test-data/pug-lexer/cases/block-expansion.shorthands.pug rename to src/tests/sample_data/pug-lexer/cases/block-expansion.shorthands.pug diff --git a/src/test-data/pug-lexer/cases/blockquote.pug b/src/tests/sample_data/pug-lexer/cases/blockquote.pug similarity index 100% rename from src/test-data/pug-lexer/cases/blockquote.pug rename to src/tests/sample_data/pug-lexer/cases/blockquote.pug diff --git a/src/test-data/pug-lexer/cases/blocks-in-blocks.pug b/src/tests/sample_data/pug-lexer/cases/blocks-in-blocks.pug similarity index 100% rename from src/test-data/pug-lexer/cases/blocks-in-blocks.pug rename to src/tests/sample_data/pug-lexer/cases/blocks-in-blocks.pug diff --git a/src/test-data/pug-lexer/cases/blocks-in-if.pug b/src/tests/sample_data/pug-lexer/cases/blocks-in-if.pug similarity index 100% rename from src/test-data/pug-lexer/cases/blocks-in-if.pug rename to src/tests/sample_data/pug-lexer/cases/blocks-in-if.pug diff --git a/src/test-data/pug-lexer/cases/case-blocks.pug b/src/tests/sample_data/pug-lexer/cases/case-blocks.pug similarity index 100% rename from src/test-data/pug-lexer/cases/case-blocks.pug rename to src/tests/sample_data/pug-lexer/cases/case-blocks.pug diff --git a/src/test-data/pug-lexer/cases/case.pug b/src/tests/sample_data/pug-lexer/cases/case.pug similarity index 100% rename from src/test-data/pug-lexer/cases/case.pug rename to src/tests/sample_data/pug-lexer/cases/case.pug diff --git a/src/test-data/pug-lexer/cases/classes-empty.pug b/src/tests/sample_data/pug-lexer/cases/classes-empty.pug similarity index 100% rename from src/test-data/pug-lexer/cases/classes-empty.pug rename to src/tests/sample_data/pug-lexer/cases/classes-empty.pug diff --git a/src/test-data/pug-lexer/cases/classes.pug b/src/tests/sample_data/pug-lexer/cases/classes.pug similarity index 100% rename from src/test-data/pug-lexer/cases/classes.pug rename to src/tests/sample_data/pug-lexer/cases/classes.pug diff --git a/src/test-data/pug-lexer/cases/code.conditionals.pug b/src/tests/sample_data/pug-lexer/cases/code.conditionals.pug similarity index 100% rename from src/test-data/pug-lexer/cases/code.conditionals.pug rename to src/tests/sample_data/pug-lexer/cases/code.conditionals.pug diff --git a/src/test-data/pug-lexer/cases/code.escape.pug b/src/tests/sample_data/pug-lexer/cases/code.escape.pug similarity index 100% rename from src/test-data/pug-lexer/cases/code.escape.pug rename to src/tests/sample_data/pug-lexer/cases/code.escape.pug diff --git a/src/test-data/pug-lexer/cases/code.iteration.pug b/src/tests/sample_data/pug-lexer/cases/code.iteration.pug similarity index 100% rename from src/test-data/pug-lexer/cases/code.iteration.pug rename to src/tests/sample_data/pug-lexer/cases/code.iteration.pug diff --git a/src/test-data/pug-lexer/cases/code.pug b/src/tests/sample_data/pug-lexer/cases/code.pug similarity index 100% rename from src/test-data/pug-lexer/cases/code.pug rename to src/tests/sample_data/pug-lexer/cases/code.pug diff --git a/src/test-data/pug-lexer/cases/comments-in-case.pug b/src/tests/sample_data/pug-lexer/cases/comments-in-case.pug similarity index 100% rename from src/test-data/pug-lexer/cases/comments-in-case.pug rename to src/tests/sample_data/pug-lexer/cases/comments-in-case.pug diff --git a/src/test-data/pug-lexer/cases/comments.pug b/src/tests/sample_data/pug-lexer/cases/comments.pug similarity index 100% rename from src/test-data/pug-lexer/cases/comments.pug rename to src/tests/sample_data/pug-lexer/cases/comments.pug diff --git a/src/test-data/pug-lexer/cases/comments.source.pug b/src/tests/sample_data/pug-lexer/cases/comments.source.pug similarity index 100% rename from src/test-data/pug-lexer/cases/comments.source.pug rename to src/tests/sample_data/pug-lexer/cases/comments.source.pug diff --git a/src/test-data/pug-lexer/cases/doctype.custom.pug b/src/tests/sample_data/pug-lexer/cases/doctype.custom.pug similarity index 100% rename from src/test-data/pug-lexer/cases/doctype.custom.pug rename to src/tests/sample_data/pug-lexer/cases/doctype.custom.pug diff --git a/src/test-data/pug-lexer/cases/doctype.default.pug b/src/tests/sample_data/pug-lexer/cases/doctype.default.pug similarity index 100% rename from src/test-data/pug-lexer/cases/doctype.default.pug rename to src/tests/sample_data/pug-lexer/cases/doctype.default.pug diff --git a/src/test-data/pug-lexer/cases/doctype.keyword.pug b/src/tests/sample_data/pug-lexer/cases/doctype.keyword.pug similarity index 100% rename from src/test-data/pug-lexer/cases/doctype.keyword.pug rename to src/tests/sample_data/pug-lexer/cases/doctype.keyword.pug diff --git a/src/test-data/pug-lexer/cases/each.else.pug b/src/tests/sample_data/pug-lexer/cases/each.else.pug similarity index 100% rename from src/test-data/pug-lexer/cases/each.else.pug rename to src/tests/sample_data/pug-lexer/cases/each.else.pug diff --git a/src/test-data/pug-lexer/cases/escape-chars.pug b/src/tests/sample_data/pug-lexer/cases/escape-chars.pug similarity index 100% rename from src/test-data/pug-lexer/cases/escape-chars.pug rename to src/tests/sample_data/pug-lexer/cases/escape-chars.pug diff --git a/src/test-data/pug-lexer/cases/escape-test.pug b/src/tests/sample_data/pug-lexer/cases/escape-test.pug similarity index 100% rename from src/test-data/pug-lexer/cases/escape-test.pug rename to src/tests/sample_data/pug-lexer/cases/escape-test.pug diff --git a/src/test-data/pug-lexer/cases/escaping-class-attribute.pug b/src/tests/sample_data/pug-lexer/cases/escaping-class-attribute.pug similarity index 100% rename from src/test-data/pug-lexer/cases/escaping-class-attribute.pug rename to src/tests/sample_data/pug-lexer/cases/escaping-class-attribute.pug diff --git a/src/test-data/pug-lexer/cases/filter-in-include.pug b/src/tests/sample_data/pug-lexer/cases/filter-in-include.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filter-in-include.pug rename to src/tests/sample_data/pug-lexer/cases/filter-in-include.pug diff --git a/src/test-data/pug-lexer/cases/filters-empty.pug b/src/tests/sample_data/pug-lexer/cases/filters-empty.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters-empty.pug rename to src/tests/sample_data/pug-lexer/cases/filters-empty.pug diff --git a/src/test-data/pug-lexer/cases/filters.coffeescript.pug b/src/tests/sample_data/pug-lexer/cases/filters.coffeescript.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.coffeescript.pug rename to src/tests/sample_data/pug-lexer/cases/filters.coffeescript.pug diff --git a/src/test-data/pug-lexer/cases/filters.custom.pug b/src/tests/sample_data/pug-lexer/cases/filters.custom.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.custom.pug rename to src/tests/sample_data/pug-lexer/cases/filters.custom.pug diff --git a/src/test-data/pug-lexer/cases/filters.include.custom.pug b/src/tests/sample_data/pug-lexer/cases/filters.include.custom.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.include.custom.pug rename to src/tests/sample_data/pug-lexer/cases/filters.include.custom.pug diff --git a/src/test-data/pug-lexer/cases/filters.include.pug b/src/tests/sample_data/pug-lexer/cases/filters.include.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.include.pug rename to src/tests/sample_data/pug-lexer/cases/filters.include.pug diff --git a/src/test-data/pug-lexer/cases/filters.inline.pug b/src/tests/sample_data/pug-lexer/cases/filters.inline.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.inline.pug rename to src/tests/sample_data/pug-lexer/cases/filters.inline.pug diff --git a/src/test-data/pug-lexer/cases/filters.less.pug b/src/tests/sample_data/pug-lexer/cases/filters.less.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.less.pug rename to src/tests/sample_data/pug-lexer/cases/filters.less.pug diff --git a/src/test-data/pug-lexer/cases/filters.markdown.pug b/src/tests/sample_data/pug-lexer/cases/filters.markdown.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.markdown.pug rename to src/tests/sample_data/pug-lexer/cases/filters.markdown.pug diff --git a/src/test-data/pug-lexer/cases/filters.nested.pug b/src/tests/sample_data/pug-lexer/cases/filters.nested.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.nested.pug rename to src/tests/sample_data/pug-lexer/cases/filters.nested.pug diff --git a/src/test-data/pug-lexer/cases/filters.stylus.pug b/src/tests/sample_data/pug-lexer/cases/filters.stylus.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.stylus.pug rename to src/tests/sample_data/pug-lexer/cases/filters.stylus.pug diff --git a/src/test-data/pug-lexer/cases/filters.verbatim.pug b/src/tests/sample_data/pug-lexer/cases/filters.verbatim.pug similarity index 100% rename from src/test-data/pug-lexer/cases/filters.verbatim.pug rename to src/tests/sample_data/pug-lexer/cases/filters.verbatim.pug diff --git a/src/test-data/pug-lexer/cases/html.pug b/src/tests/sample_data/pug-lexer/cases/html.pug similarity index 100% rename from src/test-data/pug-lexer/cases/html.pug rename to src/tests/sample_data/pug-lexer/cases/html.pug diff --git a/src/test-data/pug-lexer/cases/html5.pug b/src/tests/sample_data/pug-lexer/cases/html5.pug similarity index 100% rename from src/test-data/pug-lexer/cases/html5.pug rename to src/tests/sample_data/pug-lexer/cases/html5.pug diff --git a/src/test-data/pug-lexer/cases/include-extends-from-root.pug b/src/tests/sample_data/pug-lexer/cases/include-extends-from-root.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-extends-from-root.pug rename to src/tests/sample_data/pug-lexer/cases/include-extends-from-root.pug diff --git a/src/test-data/pug-lexer/cases/include-extends-of-common-template.pug b/src/tests/sample_data/pug-lexer/cases/include-extends-of-common-template.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-extends-of-common-template.pug rename to src/tests/sample_data/pug-lexer/cases/include-extends-of-common-template.pug diff --git a/src/test-data/pug-lexer/cases/include-extends-relative.pug b/src/tests/sample_data/pug-lexer/cases/include-extends-relative.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-extends-relative.pug rename to src/tests/sample_data/pug-lexer/cases/include-extends-relative.pug diff --git a/src/test-data/pug-lexer/cases/include-only-text-body.pug b/src/tests/sample_data/pug-lexer/cases/include-only-text-body.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-only-text-body.pug rename to src/tests/sample_data/pug-lexer/cases/include-only-text-body.pug diff --git a/src/test-data/pug-lexer/cases/include-only-text.pug b/src/tests/sample_data/pug-lexer/cases/include-only-text.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-only-text.pug rename to src/tests/sample_data/pug-lexer/cases/include-only-text.pug diff --git a/src/test-data/pug-lexer/cases/include-with-text-head.pug b/src/tests/sample_data/pug-lexer/cases/include-with-text-head.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-with-text-head.pug rename to src/tests/sample_data/pug-lexer/cases/include-with-text-head.pug diff --git a/src/test-data/pug-lexer/cases/include-with-text.pug b/src/tests/sample_data/pug-lexer/cases/include-with-text.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include-with-text.pug rename to src/tests/sample_data/pug-lexer/cases/include-with-text.pug diff --git a/src/test-data/pug-lexer/cases/include.script.pug b/src/tests/sample_data/pug-lexer/cases/include.script.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include.script.pug rename to src/tests/sample_data/pug-lexer/cases/include.script.pug diff --git a/src/test-data/pug-lexer/cases/include.yield.nested.pug b/src/tests/sample_data/pug-lexer/cases/include.yield.nested.pug similarity index 100% rename from src/test-data/pug-lexer/cases/include.yield.nested.pug rename to src/tests/sample_data/pug-lexer/cases/include.yield.nested.pug diff --git a/src/test-data/pug-lexer/cases/includes-with-ext-js.pug b/src/tests/sample_data/pug-lexer/cases/includes-with-ext-js.pug similarity index 100% rename from src/test-data/pug-lexer/cases/includes-with-ext-js.pug rename to src/tests/sample_data/pug-lexer/cases/includes-with-ext-js.pug diff --git a/src/test-data/pug-lexer/cases/includes.pug b/src/tests/sample_data/pug-lexer/cases/includes.pug similarity index 100% rename from src/test-data/pug-lexer/cases/includes.pug rename to src/tests/sample_data/pug-lexer/cases/includes.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.alert-dialog.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.alert-dialog.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.alert-dialog.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.alert-dialog.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.defaults.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.defaults.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.defaults.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.defaults.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.extend.include.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.extend.include.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.extend.include.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.extend.include.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.extend.mixins.block.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.extend.mixins.block.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.extend.mixins.block.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.extend.mixins.block.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.extend.mixins.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.extend.mixins.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.extend.mixins.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.extend.mixins.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.extend.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.extend.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.extend.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.extend.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.extend.recursive.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.extend.recursive.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.extend.recursive.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.extend.recursive.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.extend.whitespace.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.extend.whitespace.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.extend.whitespace.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.extend.whitespace.pug diff --git a/src/test-data/pug-lexer/cases/inheritance.pug b/src/tests/sample_data/pug-lexer/cases/inheritance.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inheritance.pug rename to src/tests/sample_data/pug-lexer/cases/inheritance.pug diff --git a/src/test-data/pug-lexer/cases/inline-block-comment.pug b/src/tests/sample_data/pug-lexer/cases/inline-block-comment.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inline-block-comment.pug rename to src/tests/sample_data/pug-lexer/cases/inline-block-comment.pug diff --git a/src/test-data/pug-lexer/cases/inline-tag.pug b/src/tests/sample_data/pug-lexer/cases/inline-tag.pug similarity index 100% rename from src/test-data/pug-lexer/cases/inline-tag.pug rename to src/tests/sample_data/pug-lexer/cases/inline-tag.pug diff --git a/src/test-data/pug-lexer/cases/intepolated-elements.pug b/src/tests/sample_data/pug-lexer/cases/intepolated-elements.pug similarity index 100% rename from src/test-data/pug-lexer/cases/intepolated-elements.pug rename to src/tests/sample_data/pug-lexer/cases/intepolated-elements.pug diff --git a/src/test-data/pug-lexer/cases/interpolated-mixin.pug b/src/tests/sample_data/pug-lexer/cases/interpolated-mixin.pug similarity index 100% rename from src/test-data/pug-lexer/cases/interpolated-mixin.pug rename to src/tests/sample_data/pug-lexer/cases/interpolated-mixin.pug diff --git a/src/test-data/pug-lexer/cases/interpolation.escape.pug b/src/tests/sample_data/pug-lexer/cases/interpolation.escape.pug similarity index 100% rename from src/test-data/pug-lexer/cases/interpolation.escape.pug rename to src/tests/sample_data/pug-lexer/cases/interpolation.escape.pug diff --git a/src/test-data/pug-lexer/cases/javascript-new-lines.js b/src/tests/sample_data/pug-lexer/cases/javascript-new-lines.js similarity index 100% rename from src/test-data/pug-lexer/cases/javascript-new-lines.js rename to src/tests/sample_data/pug-lexer/cases/javascript-new-lines.js diff --git a/src/test-data/pug-lexer/cases/layout.append.pug b/src/tests/sample_data/pug-lexer/cases/layout.append.pug similarity index 100% rename from src/test-data/pug-lexer/cases/layout.append.pug rename to src/tests/sample_data/pug-lexer/cases/layout.append.pug diff --git a/src/test-data/pug-lexer/cases/layout.append.without-block.pug b/src/tests/sample_data/pug-lexer/cases/layout.append.without-block.pug similarity index 100% rename from src/test-data/pug-lexer/cases/layout.append.without-block.pug rename to src/tests/sample_data/pug-lexer/cases/layout.append.without-block.pug diff --git a/src/test-data/pug-lexer/cases/layout.multi.append.prepend.block.pug b/src/tests/sample_data/pug-lexer/cases/layout.multi.append.prepend.block.pug similarity index 100% rename from src/test-data/pug-lexer/cases/layout.multi.append.prepend.block.pug rename to src/tests/sample_data/pug-lexer/cases/layout.multi.append.prepend.block.pug diff --git a/src/test-data/pug-lexer/cases/layout.prepend.pug b/src/tests/sample_data/pug-lexer/cases/layout.prepend.pug similarity index 100% rename from src/test-data/pug-lexer/cases/layout.prepend.pug rename to src/tests/sample_data/pug-lexer/cases/layout.prepend.pug diff --git a/src/test-data/pug-lexer/cases/layout.prepend.without-block.pug b/src/tests/sample_data/pug-lexer/cases/layout.prepend.without-block.pug similarity index 100% rename from src/test-data/pug-lexer/cases/layout.prepend.without-block.pug rename to src/tests/sample_data/pug-lexer/cases/layout.prepend.without-block.pug diff --git a/src/test-data/pug-lexer/cases/mixin-at-end-of-file.pug b/src/tests/sample_data/pug-lexer/cases/mixin-at-end-of-file.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin-at-end-of-file.pug rename to src/tests/sample_data/pug-lexer/cases/mixin-at-end-of-file.pug diff --git a/src/test-data/pug-lexer/cases/mixin-block-with-space.pug b/src/tests/sample_data/pug-lexer/cases/mixin-block-with-space.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin-block-with-space.pug rename to src/tests/sample_data/pug-lexer/cases/mixin-block-with-space.pug diff --git a/src/test-data/pug-lexer/cases/mixin-hoist.pug b/src/tests/sample_data/pug-lexer/cases/mixin-hoist.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin-hoist.pug rename to src/tests/sample_data/pug-lexer/cases/mixin-hoist.pug diff --git a/src/test-data/pug-lexer/cases/mixin-via-include.pug b/src/tests/sample_data/pug-lexer/cases/mixin-via-include.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin-via-include.pug rename to src/tests/sample_data/pug-lexer/cases/mixin-via-include.pug diff --git a/src/test-data/pug-lexer/cases/mixin.attrs.pug b/src/tests/sample_data/pug-lexer/cases/mixin.attrs.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin.attrs.pug rename to src/tests/sample_data/pug-lexer/cases/mixin.attrs.pug diff --git a/src/test-data/pug-lexer/cases/mixin.block-tag-behaviour.pug b/src/tests/sample_data/pug-lexer/cases/mixin.block-tag-behaviour.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin.block-tag-behaviour.pug rename to src/tests/sample_data/pug-lexer/cases/mixin.block-tag-behaviour.pug diff --git a/src/test-data/pug-lexer/cases/mixin.blocks.pug b/src/tests/sample_data/pug-lexer/cases/mixin.blocks.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin.blocks.pug rename to src/tests/sample_data/pug-lexer/cases/mixin.blocks.pug diff --git a/src/test-data/pug-lexer/cases/mixin.merge.pug b/src/tests/sample_data/pug-lexer/cases/mixin.merge.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixin.merge.pug rename to src/tests/sample_data/pug-lexer/cases/mixin.merge.pug diff --git a/src/test-data/pug-lexer/cases/mixins-unused.pug b/src/tests/sample_data/pug-lexer/cases/mixins-unused.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixins-unused.pug rename to src/tests/sample_data/pug-lexer/cases/mixins-unused.pug diff --git a/src/test-data/pug-lexer/cases/mixins.pug b/src/tests/sample_data/pug-lexer/cases/mixins.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixins.pug rename to src/tests/sample_data/pug-lexer/cases/mixins.pug diff --git a/src/test-data/pug-lexer/cases/mixins.rest-args.pug b/src/tests/sample_data/pug-lexer/cases/mixins.rest-args.pug similarity index 100% rename from src/test-data/pug-lexer/cases/mixins.rest-args.pug rename to src/tests/sample_data/pug-lexer/cases/mixins.rest-args.pug diff --git a/src/test-data/pug-lexer/cases/namespaces.pug b/src/tests/sample_data/pug-lexer/cases/namespaces.pug similarity index 100% rename from src/test-data/pug-lexer/cases/namespaces.pug rename to src/tests/sample_data/pug-lexer/cases/namespaces.pug diff --git a/src/test-data/pug-lexer/cases/nesting.pug b/src/tests/sample_data/pug-lexer/cases/nesting.pug similarity index 100% rename from src/test-data/pug-lexer/cases/nesting.pug rename to src/tests/sample_data/pug-lexer/cases/nesting.pug diff --git a/src/test-data/pug-lexer/cases/pipeless-comments.pug b/src/tests/sample_data/pug-lexer/cases/pipeless-comments.pug similarity index 100% rename from src/test-data/pug-lexer/cases/pipeless-comments.pug rename to src/tests/sample_data/pug-lexer/cases/pipeless-comments.pug diff --git a/src/test-data/pug-lexer/cases/pipeless-filters.pug b/src/tests/sample_data/pug-lexer/cases/pipeless-filters.pug similarity index 100% rename from src/test-data/pug-lexer/cases/pipeless-filters.pug rename to src/tests/sample_data/pug-lexer/cases/pipeless-filters.pug diff --git a/src/test-data/pug-lexer/cases/pipeless-tag.pug b/src/tests/sample_data/pug-lexer/cases/pipeless-tag.pug similarity index 100% rename from src/test-data/pug-lexer/cases/pipeless-tag.pug rename to src/tests/sample_data/pug-lexer/cases/pipeless-tag.pug diff --git a/src/test-data/pug-lexer/cases/pre.pug b/src/tests/sample_data/pug-lexer/cases/pre.pug similarity index 100% rename from src/test-data/pug-lexer/cases/pre.pug rename to src/tests/sample_data/pug-lexer/cases/pre.pug diff --git a/src/test-data/pug-lexer/cases/quotes.pug b/src/tests/sample_data/pug-lexer/cases/quotes.pug similarity index 100% rename from src/test-data/pug-lexer/cases/quotes.pug rename to src/tests/sample_data/pug-lexer/cases/quotes.pug diff --git a/src/test-data/pug-lexer/cases/regression.1794.pug b/src/tests/sample_data/pug-lexer/cases/regression.1794.pug similarity index 100% rename from src/test-data/pug-lexer/cases/regression.1794.pug rename to src/tests/sample_data/pug-lexer/cases/regression.1794.pug diff --git a/src/test-data/pug-lexer/cases/regression.784.pug b/src/tests/sample_data/pug-lexer/cases/regression.784.pug similarity index 100% rename from src/test-data/pug-lexer/cases/regression.784.pug rename to src/tests/sample_data/pug-lexer/cases/regression.784.pug diff --git a/src/test-data/pug-lexer/cases/script.whitespace.pug b/src/tests/sample_data/pug-lexer/cases/script.whitespace.pug similarity index 100% rename from src/test-data/pug-lexer/cases/script.whitespace.pug rename to src/tests/sample_data/pug-lexer/cases/script.whitespace.pug diff --git a/src/test-data/pug-lexer/cases/scripts.non-js.pug b/src/tests/sample_data/pug-lexer/cases/scripts.non-js.pug similarity index 100% rename from src/test-data/pug-lexer/cases/scripts.non-js.pug rename to src/tests/sample_data/pug-lexer/cases/scripts.non-js.pug diff --git a/src/test-data/pug-lexer/cases/scripts.pug b/src/tests/sample_data/pug-lexer/cases/scripts.pug similarity index 100% rename from src/test-data/pug-lexer/cases/scripts.pug rename to src/tests/sample_data/pug-lexer/cases/scripts.pug diff --git a/src/test-data/pug-lexer/cases/self-closing-html.pug b/src/tests/sample_data/pug-lexer/cases/self-closing-html.pug similarity index 100% rename from src/test-data/pug-lexer/cases/self-closing-html.pug rename to src/tests/sample_data/pug-lexer/cases/self-closing-html.pug diff --git a/src/test-data/pug-lexer/cases/single-period.pug b/src/tests/sample_data/pug-lexer/cases/single-period.pug similarity index 100% rename from src/test-data/pug-lexer/cases/single-period.pug rename to src/tests/sample_data/pug-lexer/cases/single-period.pug diff --git a/src/test-data/pug-lexer/cases/source.pug b/src/tests/sample_data/pug-lexer/cases/source.pug similarity index 100% rename from src/test-data/pug-lexer/cases/source.pug rename to src/tests/sample_data/pug-lexer/cases/source.pug diff --git a/src/test-data/pug-lexer/cases/styles.pug b/src/tests/sample_data/pug-lexer/cases/styles.pug similarity index 100% rename from src/test-data/pug-lexer/cases/styles.pug rename to src/tests/sample_data/pug-lexer/cases/styles.pug diff --git a/src/test-data/pug-lexer/cases/tag-blocks.pug b/src/tests/sample_data/pug-lexer/cases/tag-blocks.pug similarity index 100% rename from src/test-data/pug-lexer/cases/tag-blocks.pug rename to src/tests/sample_data/pug-lexer/cases/tag-blocks.pug diff --git a/src/test-data/pug-lexer/cases/tag.interpolation.pug b/src/tests/sample_data/pug-lexer/cases/tag.interpolation.pug similarity index 100% rename from src/test-data/pug-lexer/cases/tag.interpolation.pug rename to src/tests/sample_data/pug-lexer/cases/tag.interpolation.pug diff --git a/src/test-data/pug-lexer/cases/tags.self-closing.pug b/src/tests/sample_data/pug-lexer/cases/tags.self-closing.pug similarity index 100% rename from src/test-data/pug-lexer/cases/tags.self-closing.pug rename to src/tests/sample_data/pug-lexer/cases/tags.self-closing.pug diff --git a/src/test-data/pug-lexer/cases/template.pug b/src/tests/sample_data/pug-lexer/cases/template.pug similarity index 100% rename from src/test-data/pug-lexer/cases/template.pug rename to src/tests/sample_data/pug-lexer/cases/template.pug diff --git a/src/test-data/pug-lexer/cases/text-block.pug b/src/tests/sample_data/pug-lexer/cases/text-block.pug similarity index 100% rename from src/test-data/pug-lexer/cases/text-block.pug rename to src/tests/sample_data/pug-lexer/cases/text-block.pug diff --git a/src/test-data/pug-lexer/cases/text.pug b/src/tests/sample_data/pug-lexer/cases/text.pug similarity index 100% rename from src/test-data/pug-lexer/cases/text.pug rename to src/tests/sample_data/pug-lexer/cases/text.pug diff --git a/src/test-data/pug-lexer/cases/utf8bom.pug b/src/tests/sample_data/pug-lexer/cases/utf8bom.pug similarity index 100% rename from src/test-data/pug-lexer/cases/utf8bom.pug rename to src/tests/sample_data/pug-lexer/cases/utf8bom.pug diff --git a/src/test-data/pug-lexer/cases/vars.pug b/src/tests/sample_data/pug-lexer/cases/vars.pug similarity index 100% rename from src/test-data/pug-lexer/cases/vars.pug rename to src/tests/sample_data/pug-lexer/cases/vars.pug diff --git a/src/test-data/pug-lexer/cases/while.pug b/src/tests/sample_data/pug-lexer/cases/while.pug similarity index 100% rename from src/test-data/pug-lexer/cases/while.pug rename to src/tests/sample_data/pug-lexer/cases/while.pug diff --git a/src/test-data/pug-lexer/cases/xml.pug b/src/tests/sample_data/pug-lexer/cases/xml.pug similarity index 100% rename from src/test-data/pug-lexer/cases/xml.pug rename to src/tests/sample_data/pug-lexer/cases/xml.pug diff --git a/src/test-data/pug-lexer/cases/yield-before-conditional-head.pug b/src/tests/sample_data/pug-lexer/cases/yield-before-conditional-head.pug similarity index 100% rename from src/test-data/pug-lexer/cases/yield-before-conditional-head.pug rename to src/tests/sample_data/pug-lexer/cases/yield-before-conditional-head.pug diff --git a/src/test-data/pug-lexer/cases/yield-before-conditional.pug b/src/tests/sample_data/pug-lexer/cases/yield-before-conditional.pug similarity index 100% rename from src/test-data/pug-lexer/cases/yield-before-conditional.pug rename to src/tests/sample_data/pug-lexer/cases/yield-before-conditional.pug diff --git a/src/test-data/pug-lexer/cases/yield-head.pug b/src/tests/sample_data/pug-lexer/cases/yield-head.pug similarity index 100% rename from src/test-data/pug-lexer/cases/yield-head.pug rename to src/tests/sample_data/pug-lexer/cases/yield-head.pug diff --git a/src/test-data/pug-lexer/cases/yield-title-head.pug b/src/tests/sample_data/pug-lexer/cases/yield-title-head.pug similarity index 100% rename from src/test-data/pug-lexer/cases/yield-title-head.pug rename to src/tests/sample_data/pug-lexer/cases/yield-title-head.pug diff --git a/src/test-data/pug-lexer/cases/yield-title.pug b/src/tests/sample_data/pug-lexer/cases/yield-title.pug similarity index 100% rename from src/test-data/pug-lexer/cases/yield-title.pug rename to src/tests/sample_data/pug-lexer/cases/yield-title.pug diff --git a/src/test-data/pug-lexer/cases/yield.pug b/src/tests/sample_data/pug-lexer/cases/yield.pug similarity index 100% rename from src/test-data/pug-lexer/cases/yield.pug rename to src/tests/sample_data/pug-lexer/cases/yield.pug diff --git a/src/test-data/pug-lexer/errors/attribute-invalid-expression.pug b/src/tests/sample_data/pug-lexer/errors/attribute-invalid-expression.pug similarity index 100% rename from src/test-data/pug-lexer/errors/attribute-invalid-expression.pug rename to src/tests/sample_data/pug-lexer/errors/attribute-invalid-expression.pug diff --git a/src/test-data/pug-lexer/errors/case-with-invalid-expression.pug b/src/tests/sample_data/pug-lexer/errors/case-with-invalid-expression.pug similarity index 100% rename from src/test-data/pug-lexer/errors/case-with-invalid-expression.pug rename to src/tests/sample_data/pug-lexer/errors/case-with-invalid-expression.pug diff --git a/src/test-data/pug-lexer/errors/case-with-no-expression.pug b/src/tests/sample_data/pug-lexer/errors/case-with-no-expression.pug similarity index 100% rename from src/test-data/pug-lexer/errors/case-with-no-expression.pug rename to src/tests/sample_data/pug-lexer/errors/case-with-no-expression.pug diff --git a/src/test-data/pug-lexer/errors/default-with-expression.pug b/src/tests/sample_data/pug-lexer/errors/default-with-expression.pug similarity index 100% rename from src/test-data/pug-lexer/errors/default-with-expression.pug rename to src/tests/sample_data/pug-lexer/errors/default-with-expression.pug diff --git a/src/test-data/pug-lexer/errors/else-with-condition.pug b/src/tests/sample_data/pug-lexer/errors/else-with-condition.pug similarity index 100% rename from src/test-data/pug-lexer/errors/else-with-condition.pug rename to src/tests/sample_data/pug-lexer/errors/else-with-condition.pug diff --git a/src/test-data/pug-lexer/errors/extends-no-path.pug b/src/tests/sample_data/pug-lexer/errors/extends-no-path.pug similarity index 100% rename from src/test-data/pug-lexer/errors/extends-no-path.pug rename to src/tests/sample_data/pug-lexer/errors/extends-no-path.pug diff --git a/src/test-data/pug-lexer/errors/include-filter-no-path-2.pug b/src/tests/sample_data/pug-lexer/errors/include-filter-no-path-2.pug similarity index 100% rename from src/test-data/pug-lexer/errors/include-filter-no-path-2.pug rename to src/tests/sample_data/pug-lexer/errors/include-filter-no-path-2.pug diff --git a/src/test-data/pug-lexer/errors/include-filter-no-path.pug b/src/tests/sample_data/pug-lexer/errors/include-filter-no-path.pug similarity index 100% rename from src/test-data/pug-lexer/errors/include-filter-no-path.pug rename to src/tests/sample_data/pug-lexer/errors/include-filter-no-path.pug diff --git a/src/test-data/pug-lexer/errors/include-filter-no-space.pug b/src/tests/sample_data/pug-lexer/errors/include-filter-no-space.pug similarity index 100% rename from src/test-data/pug-lexer/errors/include-filter-no-space.pug rename to src/tests/sample_data/pug-lexer/errors/include-filter-no-space.pug diff --git a/src/test-data/pug-lexer/errors/include-no-path.pug b/src/tests/sample_data/pug-lexer/errors/include-no-path.pug similarity index 100% rename from src/test-data/pug-lexer/errors/include-no-path.pug rename to src/tests/sample_data/pug-lexer/errors/include-no-path.pug diff --git a/src/test-data/pug-lexer/errors/inconsistent-indentation.pug b/src/tests/sample_data/pug-lexer/errors/inconsistent-indentation.pug similarity index 100% rename from src/test-data/pug-lexer/errors/inconsistent-indentation.pug rename to src/tests/sample_data/pug-lexer/errors/inconsistent-indentation.pug diff --git a/src/test-data/pug-lexer/errors/interpolated-call.pug b/src/tests/sample_data/pug-lexer/errors/interpolated-call.pug similarity index 100% rename from src/test-data/pug-lexer/errors/interpolated-call.pug rename to src/tests/sample_data/pug-lexer/errors/interpolated-call.pug diff --git a/src/test-data/pug-lexer/errors/invalid-class-name-1.pug b/src/tests/sample_data/pug-lexer/errors/invalid-class-name-1.pug similarity index 100% rename from src/test-data/pug-lexer/errors/invalid-class-name-1.pug rename to src/tests/sample_data/pug-lexer/errors/invalid-class-name-1.pug diff --git a/src/test-data/pug-lexer/errors/invalid-class-name-2.pug b/src/tests/sample_data/pug-lexer/errors/invalid-class-name-2.pug similarity index 100% rename from src/test-data/pug-lexer/errors/invalid-class-name-2.pug rename to src/tests/sample_data/pug-lexer/errors/invalid-class-name-2.pug diff --git a/src/test-data/pug-lexer/errors/invalid-class-name-3.pug b/src/tests/sample_data/pug-lexer/errors/invalid-class-name-3.pug similarity index 100% rename from src/test-data/pug-lexer/errors/invalid-class-name-3.pug rename to src/tests/sample_data/pug-lexer/errors/invalid-class-name-3.pug diff --git a/src/test-data/pug-lexer/errors/invalid-id.pug b/src/tests/sample_data/pug-lexer/errors/invalid-id.pug similarity index 100% rename from src/test-data/pug-lexer/errors/invalid-id.pug rename to src/tests/sample_data/pug-lexer/errors/invalid-id.pug diff --git a/src/test-data/pug-lexer/errors/malformed-each.pug b/src/tests/sample_data/pug-lexer/errors/malformed-each.pug similarity index 100% rename from src/test-data/pug-lexer/errors/malformed-each.pug rename to src/tests/sample_data/pug-lexer/errors/malformed-each.pug diff --git a/src/test-data/pug-lexer/errors/malformed-extend.pug b/src/tests/sample_data/pug-lexer/errors/malformed-extend.pug similarity index 100% rename from src/test-data/pug-lexer/errors/malformed-extend.pug rename to src/tests/sample_data/pug-lexer/errors/malformed-extend.pug diff --git a/src/test-data/pug-lexer/errors/malformed-include.pug b/src/tests/sample_data/pug-lexer/errors/malformed-include.pug similarity index 100% rename from src/test-data/pug-lexer/errors/malformed-include.pug rename to src/tests/sample_data/pug-lexer/errors/malformed-include.pug diff --git a/src/test-data/pug-lexer/errors/mismatched-inline-tag.pug b/src/tests/sample_data/pug-lexer/errors/mismatched-inline-tag.pug similarity index 100% rename from src/test-data/pug-lexer/errors/mismatched-inline-tag.pug rename to src/tests/sample_data/pug-lexer/errors/mismatched-inline-tag.pug diff --git a/src/test-data/pug-lexer/errors/mismatched-tag-interpolation.pug b/src/tests/sample_data/pug-lexer/errors/mismatched-tag-interpolation.pug similarity index 100% rename from src/test-data/pug-lexer/errors/mismatched-tag-interpolation.pug rename to src/tests/sample_data/pug-lexer/errors/mismatched-tag-interpolation.pug diff --git a/src/test-data/pug-lexer/errors/multi-line-interpolation.pug b/src/tests/sample_data/pug-lexer/errors/multi-line-interpolation.pug similarity index 100% rename from src/test-data/pug-lexer/errors/multi-line-interpolation.pug rename to src/tests/sample_data/pug-lexer/errors/multi-line-interpolation.pug diff --git a/src/test-data/pug-lexer/errors/old-prefixed-each.pug b/src/tests/sample_data/pug-lexer/errors/old-prefixed-each.pug similarity index 100% rename from src/test-data/pug-lexer/errors/old-prefixed-each.pug rename to src/tests/sample_data/pug-lexer/errors/old-prefixed-each.pug diff --git a/src/test-data/pug-lexer/errors/open-interpolation.pug b/src/tests/sample_data/pug-lexer/errors/open-interpolation.pug similarity index 100% rename from src/test-data/pug-lexer/errors/open-interpolation.pug rename to src/tests/sample_data/pug-lexer/errors/open-interpolation.pug diff --git a/src/test-data/pug-lexer/errors/when-with-no-expression.pug b/src/tests/sample_data/pug-lexer/errors/when-with-no-expression.pug similarity index 100% rename from src/test-data/pug-lexer/errors/when-with-no-expression.pug rename to src/tests/sample_data/pug-lexer/errors/when-with-no-expression.pug diff --git a/src/test-data/pug-lexer/errors/while-with-no-expression.pug b/src/tests/sample_data/pug-lexer/errors/while-with-no-expression.pug similarity index 100% rename from src/test-data/pug-lexer/errors/while-with-no-expression.pug rename to src/tests/sample_data/pug-lexer/errors/while-with-no-expression.pug diff --git a/src/test-data/pug-linker/test/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug-linker/test/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug-linker/test/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug-linker/test/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/1794-extends.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/1794-extends.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/1794-extends.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/1794-extends.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/1794-include.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/1794-include.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/1794-include.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/1794-include.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/dialog.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/dialog.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/dialog.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/dialog.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/empty-block.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/empty-block.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/empty-block.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/empty-block.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/escapes.html b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/escapes.html similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/escapes.html rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/escapes.html diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-relative.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-relative.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/extends-relative.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/extends-relative.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/includable.js b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/includable.js similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/includable.js rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/includable.js diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/include-from-root.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/include-from-root.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/include-from-root.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/include-from-root.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/layout.include.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/layout.include.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/layout.include.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/layout.include.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/layout.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/layout.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/layout.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/layout.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/mixins.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/mixins.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/mixins.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/mixins.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/pet.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/pet.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/pet.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/pet.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/smile.html b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/smile.html similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/smile.html rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/smile.html diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/window.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/window.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/window.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/window.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/yield-nested.pug b/src/tests/sample_data/pug-linker/test/cases-src/auxiliary/yield-nested.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/auxiliary/yield-nested.pug rename to src/tests/sample_data/pug-linker/test/cases-src/auxiliary/yield-nested.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-extends-from-root.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-extends-from-root.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-extends-from-root.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-extends-from-root.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-extends-of-common-template.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-extends-of-common-template.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-extends-of-common-template.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-extends-of-common-template.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-extends-relative.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-extends-relative.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-extends-relative.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-extends-relative.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-filter-coffee.coffee b/src/tests/sample_data/pug-linker/test/cases-src/include-filter-coffee.coffee similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-filter-coffee.coffee rename to src/tests/sample_data/pug-linker/test/cases-src/include-filter-coffee.coffee diff --git a/src/test-data/pug-linker/test/cases-src/include-filter-stylus.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-filter-stylus.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-filter-stylus.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-filter-stylus.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-filter.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-filter.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-filter.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-filter.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-only-text-body.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-only-text-body.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-only-text-body.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-only-text-body.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-only-text.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-only-text.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-only-text.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-only-text.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-with-text-head.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-with-text-head.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-with-text-head.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-with-text-head.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-with-text.pug b/src/tests/sample_data/pug-linker/test/cases-src/include-with-text.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include-with-text.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include-with-text.pug diff --git a/src/test-data/pug-linker/test/cases-src/include.script.pug b/src/tests/sample_data/pug-linker/test/cases-src/include.script.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include.script.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include.script.pug diff --git a/src/test-data/pug-linker/test/cases-src/include.yield.nested.pug b/src/tests/sample_data/pug-linker/test/cases-src/include.yield.nested.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/include.yield.nested.pug rename to src/tests/sample_data/pug-linker/test/cases-src/include.yield.nested.pug diff --git a/src/test-data/pug-linker/test/cases-src/includes-with-ext-js.pug b/src/tests/sample_data/pug-linker/test/cases-src/includes-with-ext-js.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/includes-with-ext-js.pug rename to src/tests/sample_data/pug-linker/test/cases-src/includes-with-ext-js.pug diff --git a/src/test-data/pug-linker/test/cases-src/includes.pug b/src/tests/sample_data/pug-linker/test/cases-src/includes.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/includes.pug rename to src/tests/sample_data/pug-linker/test/cases-src/includes.pug diff --git a/src/test-data/pug-linker/test/cases-src/javascript-new-lines.js b/src/tests/sample_data/pug-linker/test/cases-src/javascript-new-lines.js similarity index 100% rename from src/test-data/pug-linker/test/cases-src/javascript-new-lines.js rename to src/tests/sample_data/pug-linker/test/cases-src/javascript-new-lines.js diff --git a/src/test-data/pug-linker/test/cases-src/layout.append.pug b/src/tests/sample_data/pug-linker/test/cases-src/layout.append.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/layout.append.pug rename to src/tests/sample_data/pug-linker/test/cases-src/layout.append.pug diff --git a/src/test-data/pug-linker/test/cases-src/layout.append.without-block.pug b/src/tests/sample_data/pug-linker/test/cases-src/layout.append.without-block.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/layout.append.without-block.pug rename to src/tests/sample_data/pug-linker/test/cases-src/layout.append.without-block.pug diff --git a/src/test-data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug b/src/tests/sample_data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug rename to src/tests/sample_data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug diff --git a/src/test-data/pug-linker/test/cases-src/layout.prepend.pug b/src/tests/sample_data/pug-linker/test/cases-src/layout.prepend.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/layout.prepend.pug rename to src/tests/sample_data/pug-linker/test/cases-src/layout.prepend.pug diff --git a/src/test-data/pug-linker/test/cases-src/layout.prepend.without-block.pug b/src/tests/sample_data/pug-linker/test/cases-src/layout.prepend.without-block.pug similarity index 100% rename from src/test-data/pug-linker/test/cases-src/layout.prepend.without-block.pug rename to src/tests/sample_data/pug-linker/test/cases-src/layout.prepend.without-block.pug diff --git a/src/test-data/pug-linker/test/cases-src/some-included.styl b/src/tests/sample_data/pug-linker/test/cases-src/some-included.styl similarity index 100% rename from src/test-data/pug-linker/test/cases-src/some-included.styl rename to src/tests/sample_data/pug-linker/test/cases-src/some-included.styl diff --git a/src/test-data/pug-linker/test/cases-src/some.md b/src/tests/sample_data/pug-linker/test/cases-src/some.md similarity index 100% rename from src/test-data/pug-linker/test/cases-src/some.md rename to src/tests/sample_data/pug-linker/test/cases-src/some.md diff --git a/src/test-data/pug-linker/test/cases-src/some.styl b/src/tests/sample_data/pug-linker/test/cases-src/some.styl similarity index 100% rename from src/test-data/pug-linker/test/cases-src/some.styl rename to src/tests/sample_data/pug-linker/test/cases-src/some.styl diff --git a/src/test-data/pug-linker/test/cases/include-extends-from-root.input.json b/src/tests/sample_data/pug-linker/test/cases/include-extends-from-root.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-extends-from-root.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-extends-from-root.input.json diff --git a/src/test-data/pug-linker/test/cases/include-extends-of-common-template.input.json b/src/tests/sample_data/pug-linker/test/cases/include-extends-of-common-template.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-extends-of-common-template.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-extends-of-common-template.input.json diff --git a/src/test-data/pug-linker/test/cases/include-extends-relative.input.json b/src/tests/sample_data/pug-linker/test/cases/include-extends-relative.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-extends-relative.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-extends-relative.input.json diff --git a/src/test-data/pug-linker/test/cases/include-filter-stylus.input.json b/src/tests/sample_data/pug-linker/test/cases/include-filter-stylus.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-filter-stylus.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-filter-stylus.input.json diff --git a/src/test-data/pug-linker/test/cases/include-filter.input.json b/src/tests/sample_data/pug-linker/test/cases/include-filter.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-filter.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-filter.input.json diff --git a/src/test-data/pug-linker/test/cases/include-only-text-body.input.json b/src/tests/sample_data/pug-linker/test/cases/include-only-text-body.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-only-text-body.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-only-text-body.input.json diff --git a/src/test-data/pug-linker/test/cases/include-only-text.input.json b/src/tests/sample_data/pug-linker/test/cases/include-only-text.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-only-text.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-only-text.input.json diff --git a/src/test-data/pug-linker/test/cases/include-with-text-head.input.json b/src/tests/sample_data/pug-linker/test/cases/include-with-text-head.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-with-text-head.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-with-text-head.input.json diff --git a/src/test-data/pug-linker/test/cases/include-with-text.input.json b/src/tests/sample_data/pug-linker/test/cases/include-with-text.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include-with-text.input.json rename to src/tests/sample_data/pug-linker/test/cases/include-with-text.input.json diff --git a/src/test-data/pug-linker/test/cases/include.script.input.json b/src/tests/sample_data/pug-linker/test/cases/include.script.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include.script.input.json rename to src/tests/sample_data/pug-linker/test/cases/include.script.input.json diff --git a/src/test-data/pug-linker/test/cases/include.yield.nested.input.json b/src/tests/sample_data/pug-linker/test/cases/include.yield.nested.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/include.yield.nested.input.json rename to src/tests/sample_data/pug-linker/test/cases/include.yield.nested.input.json diff --git a/src/test-data/pug-linker/test/cases/includes-with-ext-js.input.json b/src/tests/sample_data/pug-linker/test/cases/includes-with-ext-js.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/includes-with-ext-js.input.json rename to src/tests/sample_data/pug-linker/test/cases/includes-with-ext-js.input.json diff --git a/src/test-data/pug-linker/test/cases/includes.input.json b/src/tests/sample_data/pug-linker/test/cases/includes.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/includes.input.json rename to src/tests/sample_data/pug-linker/test/cases/includes.input.json diff --git a/src/test-data/pug-linker/test/cases/layout.append.input.json b/src/tests/sample_data/pug-linker/test/cases/layout.append.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/layout.append.input.json rename to src/tests/sample_data/pug-linker/test/cases/layout.append.input.json diff --git a/src/test-data/pug-linker/test/cases/layout.append.without-block.input.json b/src/tests/sample_data/pug-linker/test/cases/layout.append.without-block.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/layout.append.without-block.input.json rename to src/tests/sample_data/pug-linker/test/cases/layout.append.without-block.input.json diff --git a/src/test-data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json b/src/tests/sample_data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json rename to src/tests/sample_data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json diff --git a/src/test-data/pug-linker/test/cases/layout.prepend.input.json b/src/tests/sample_data/pug-linker/test/cases/layout.prepend.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/layout.prepend.input.json rename to src/tests/sample_data/pug-linker/test/cases/layout.prepend.input.json diff --git a/src/test-data/pug-linker/test/cases/layout.prepend.without-block.input.json b/src/tests/sample_data/pug-linker/test/cases/layout.prepend.without-block.input.json similarity index 100% rename from src/test-data/pug-linker/test/cases/layout.prepend.without-block.input.json rename to src/tests/sample_data/pug-linker/test/cases/layout.prepend.without-block.input.json diff --git a/src/test-data/pug-linker/test/errors-src/child-with-tags.pug b/src/tests/sample_data/pug-linker/test/errors-src/child-with-tags.pug similarity index 100% rename from src/test-data/pug-linker/test/errors-src/child-with-tags.pug rename to src/tests/sample_data/pug-linker/test/errors-src/child-with-tags.pug diff --git a/src/test-data/pug-linker/test/errors-src/extends-not-first.pug b/src/tests/sample_data/pug-linker/test/errors-src/extends-not-first.pug similarity index 100% rename from src/test-data/pug-linker/test/errors-src/extends-not-first.pug rename to src/tests/sample_data/pug-linker/test/errors-src/extends-not-first.pug diff --git a/src/test-data/pug-linker/test/errors-src/unexpected-block.pug b/src/tests/sample_data/pug-linker/test/errors-src/unexpected-block.pug similarity index 100% rename from src/test-data/pug-linker/test/errors-src/unexpected-block.pug rename to src/tests/sample_data/pug-linker/test/errors-src/unexpected-block.pug diff --git a/src/test-data/pug-linker/test/errors/child-with-tags.input.json b/src/tests/sample_data/pug-linker/test/errors/child-with-tags.input.json similarity index 100% rename from src/test-data/pug-linker/test/errors/child-with-tags.input.json rename to src/tests/sample_data/pug-linker/test/errors/child-with-tags.input.json diff --git a/src/test-data/pug-linker/test/errors/extends-not-first.input.json b/src/tests/sample_data/pug-linker/test/errors/extends-not-first.input.json similarity index 100% rename from src/test-data/pug-linker/test/errors/extends-not-first.input.json rename to src/tests/sample_data/pug-linker/test/errors/extends-not-first.input.json diff --git a/src/test-data/pug-linker/test/errors/unexpected-block.input.json b/src/tests/sample_data/pug-linker/test/errors/unexpected-block.input.json similarity index 100% rename from src/test-data/pug-linker/test/errors/unexpected-block.input.json rename to src/tests/sample_data/pug-linker/test/errors/unexpected-block.input.json diff --git a/src/test-data/pug-linker/test/fixtures/append-without-block/app-layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/append-without-block/app-layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append-without-block/app-layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/append-without-block/app-layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/append-without-block/layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/append-without-block/layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append-without-block/layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/append-without-block/layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/append-without-block/page.pug b/src/tests/sample_data/pug-linker/test/fixtures/append-without-block/page.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append-without-block/page.pug rename to src/tests/sample_data/pug-linker/test/fixtures/append-without-block/page.pug diff --git a/src/test-data/pug-linker/test/fixtures/append/app-layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/append/app-layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append/app-layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/append/app-layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/append/layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/append/layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append/layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/append/layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/append/page.html b/src/tests/sample_data/pug-linker/test/fixtures/append/page.html similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append/page.html rename to src/tests/sample_data/pug-linker/test/fixtures/append/page.html diff --git a/src/test-data/pug-linker/test/fixtures/append/page.pug b/src/tests/sample_data/pug-linker/test/fixtures/append/page.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/append/page.pug rename to src/tests/sample_data/pug-linker/test/fixtures/append/page.pug diff --git a/src/test-data/pug-linker/test/fixtures/empty.pug b/src/tests/sample_data/pug-linker/test/fixtures/empty.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/empty.pug rename to src/tests/sample_data/pug-linker/test/fixtures/empty.pug diff --git a/src/test-data/pug-linker/test/fixtures/layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/mixins.pug b/src/tests/sample_data/pug-linker/test/fixtures/mixins.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/mixins.pug rename to src/tests/sample_data/pug-linker/test/fixtures/mixins.pug diff --git a/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug b/src/tests/sample_data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug rename to src/tests/sample_data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug diff --git a/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug b/src/tests/sample_data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug rename to src/tests/sample_data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend-without-block/layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.html b/src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/page.html similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend-without-block/page.html rename to src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/page.html diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.pug b/src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/page.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend-without-block/page.pug rename to src/tests/sample_data/pug-linker/test/fixtures/prepend-without-block/page.pug diff --git a/src/test-data/pug-linker/test/fixtures/prepend/app-layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/prepend/app-layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend/app-layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/prepend/app-layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/prepend/layout.pug b/src/tests/sample_data/pug-linker/test/fixtures/prepend/layout.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend/layout.pug rename to src/tests/sample_data/pug-linker/test/fixtures/prepend/layout.pug diff --git a/src/test-data/pug-linker/test/fixtures/prepend/page.html b/src/tests/sample_data/pug-linker/test/fixtures/prepend/page.html similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend/page.html rename to src/tests/sample_data/pug-linker/test/fixtures/prepend/page.html diff --git a/src/test-data/pug-linker/test/fixtures/prepend/page.pug b/src/tests/sample_data/pug-linker/test/fixtures/prepend/page.pug similarity index 100% rename from src/test-data/pug-linker/test/fixtures/prepend/page.pug rename to src/tests/sample_data/pug-linker/test/fixtures/prepend/page.pug diff --git a/src/test-data/pug-linker/test/index.test.js b/src/tests/sample_data/pug-linker/test/index.test.js similarity index 100% rename from src/test-data/pug-linker/test/index.test.js rename to src/tests/sample_data/pug-linker/test/index.test.js diff --git a/src/test-data/pug-linker/test/special-cases-src/extending-empty.pug b/src/tests/sample_data/pug-linker/test/special-cases-src/extending-empty.pug similarity index 100% rename from src/test-data/pug-linker/test/special-cases-src/extending-empty.pug rename to src/tests/sample_data/pug-linker/test/special-cases-src/extending-empty.pug diff --git a/src/test-data/pug-linker/test/special-cases-src/extending-include.pug b/src/tests/sample_data/pug-linker/test/special-cases-src/extending-include.pug similarity index 100% rename from src/test-data/pug-linker/test/special-cases-src/extending-include.pug rename to src/tests/sample_data/pug-linker/test/special-cases-src/extending-include.pug diff --git a/src/test-data/pug-linker/test/special-cases-src/root-mixin.pug b/src/tests/sample_data/pug-linker/test/special-cases-src/root-mixin.pug similarity index 100% rename from src/test-data/pug-linker/test/special-cases-src/root-mixin.pug rename to src/tests/sample_data/pug-linker/test/special-cases-src/root-mixin.pug diff --git a/src/test-data/pug-linker/test/special-cases/extending-empty.input.json b/src/tests/sample_data/pug-linker/test/special-cases/extending-empty.input.json similarity index 100% rename from src/test-data/pug-linker/test/special-cases/extending-empty.input.json rename to src/tests/sample_data/pug-linker/test/special-cases/extending-empty.input.json diff --git a/src/test-data/pug-linker/test/special-cases/extending-include.input.json b/src/tests/sample_data/pug-linker/test/special-cases/extending-include.input.json similarity index 100% rename from src/test-data/pug-linker/test/special-cases/extending-include.input.json rename to src/tests/sample_data/pug-linker/test/special-cases/extending-include.input.json diff --git a/src/test-data/pug-linker/test/special-cases/root-mixin.input.json b/src/tests/sample_data/pug-linker/test/special-cases/root-mixin.input.json similarity index 100% rename from src/test-data/pug-linker/test/special-cases/root-mixin.input.json rename to src/tests/sample_data/pug-linker/test/special-cases/root-mixin.input.json diff --git a/src/test-data/pug-load/test/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug-load/test/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug-load/test/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug-load/test/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug-load/test/bar.pug b/src/tests/sample_data/pug-load/test/bar.pug similarity index 100% rename from src/test-data/pug-load/test/bar.pug rename to src/tests/sample_data/pug-load/test/bar.pug diff --git a/src/test-data/pug-load/test/bing.pug b/src/tests/sample_data/pug-load/test/bing.pug similarity index 100% rename from src/test-data/pug-load/test/bing.pug rename to src/tests/sample_data/pug-load/test/bing.pug diff --git a/src/test-data/pug-load/test/foo.pug b/src/tests/sample_data/pug-load/test/foo.pug similarity index 100% rename from src/test-data/pug-load/test/foo.pug rename to src/tests/sample_data/pug-load/test/foo.pug diff --git a/src/test-data/pug-load/test/index.test.js b/src/tests/sample_data/pug-load/test/index.test.js similarity index 100% rename from src/test-data/pug-load/test/index.test.js rename to src/tests/sample_data/pug-load/test/index.test.js diff --git a/src/test-data/pug-load/test/script.js b/src/tests/sample_data/pug-load/test/script.js similarity index 100% rename from src/test-data/pug-load/test/script.js rename to src/tests/sample_data/pug-load/test/script.js diff --git a/src/test-data/pug-parser/cases/attr-es2015.tokens.json b/src/tests/sample_data/pug-parser/cases/attr-es2015.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/attr-es2015.tokens.json rename to src/tests/sample_data/pug-parser/cases/attr-es2015.tokens.json diff --git a/src/test-data/pug-parser/cases/attrs-data.tokens.json b/src/tests/sample_data/pug-parser/cases/attrs-data.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/attrs-data.tokens.json rename to src/tests/sample_data/pug-parser/cases/attrs-data.tokens.json diff --git a/src/test-data/pug-parser/cases/attrs.js.tokens.json b/src/tests/sample_data/pug-parser/cases/attrs.js.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/attrs.js.tokens.json rename to src/tests/sample_data/pug-parser/cases/attrs.js.tokens.json diff --git a/src/test-data/pug-parser/cases/attrs.tokens.json b/src/tests/sample_data/pug-parser/cases/attrs.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/attrs.tokens.json rename to src/tests/sample_data/pug-parser/cases/attrs.tokens.json diff --git a/src/test-data/pug-parser/cases/attrs.unescaped.tokens.json b/src/tests/sample_data/pug-parser/cases/attrs.unescaped.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/attrs.unescaped.tokens.json rename to src/tests/sample_data/pug-parser/cases/attrs.unescaped.tokens.json diff --git a/src/test-data/pug-parser/cases/basic.tokens.json b/src/tests/sample_data/pug-parser/cases/basic.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/basic.tokens.json rename to src/tests/sample_data/pug-parser/cases/basic.tokens.json diff --git a/src/test-data/pug-parser/cases/blanks.tokens.json b/src/tests/sample_data/pug-parser/cases/blanks.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/blanks.tokens.json rename to src/tests/sample_data/pug-parser/cases/blanks.tokens.json diff --git a/src/test-data/pug-parser/cases/block-code.tokens.json b/src/tests/sample_data/pug-parser/cases/block-code.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/block-code.tokens.json rename to src/tests/sample_data/pug-parser/cases/block-code.tokens.json diff --git a/src/test-data/pug-parser/cases/block-expansion.shorthands.tokens.json b/src/tests/sample_data/pug-parser/cases/block-expansion.shorthands.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/block-expansion.shorthands.tokens.json rename to src/tests/sample_data/pug-parser/cases/block-expansion.shorthands.tokens.json diff --git a/src/test-data/pug-parser/cases/block-expansion.tokens.json b/src/tests/sample_data/pug-parser/cases/block-expansion.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/block-expansion.tokens.json rename to src/tests/sample_data/pug-parser/cases/block-expansion.tokens.json diff --git a/src/test-data/pug-parser/cases/blockquote.tokens.json b/src/tests/sample_data/pug-parser/cases/blockquote.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/blockquote.tokens.json rename to src/tests/sample_data/pug-parser/cases/blockquote.tokens.json diff --git a/src/test-data/pug-parser/cases/blocks-in-blocks.tokens.json b/src/tests/sample_data/pug-parser/cases/blocks-in-blocks.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/blocks-in-blocks.tokens.json rename to src/tests/sample_data/pug-parser/cases/blocks-in-blocks.tokens.json diff --git a/src/test-data/pug-parser/cases/blocks-in-if.tokens.json b/src/tests/sample_data/pug-parser/cases/blocks-in-if.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/blocks-in-if.tokens.json rename to src/tests/sample_data/pug-parser/cases/blocks-in-if.tokens.json diff --git a/src/test-data/pug-parser/cases/case-blocks.tokens.json b/src/tests/sample_data/pug-parser/cases/case-blocks.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/case-blocks.tokens.json rename to src/tests/sample_data/pug-parser/cases/case-blocks.tokens.json diff --git a/src/test-data/pug-parser/cases/case.tokens.json b/src/tests/sample_data/pug-parser/cases/case.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/case.tokens.json rename to src/tests/sample_data/pug-parser/cases/case.tokens.json diff --git a/src/test-data/pug-parser/cases/classes-empty.tokens.json b/src/tests/sample_data/pug-parser/cases/classes-empty.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/classes-empty.tokens.json rename to src/tests/sample_data/pug-parser/cases/classes-empty.tokens.json diff --git a/src/test-data/pug-parser/cases/classes.tokens.json b/src/tests/sample_data/pug-parser/cases/classes.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/classes.tokens.json rename to src/tests/sample_data/pug-parser/cases/classes.tokens.json diff --git a/src/test-data/pug-parser/cases/code.conditionals.tokens.json b/src/tests/sample_data/pug-parser/cases/code.conditionals.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/code.conditionals.tokens.json rename to src/tests/sample_data/pug-parser/cases/code.conditionals.tokens.json diff --git a/src/test-data/pug-parser/cases/code.escape.tokens.json b/src/tests/sample_data/pug-parser/cases/code.escape.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/code.escape.tokens.json rename to src/tests/sample_data/pug-parser/cases/code.escape.tokens.json diff --git a/src/test-data/pug-parser/cases/code.iteration.tokens.json b/src/tests/sample_data/pug-parser/cases/code.iteration.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/code.iteration.tokens.json rename to src/tests/sample_data/pug-parser/cases/code.iteration.tokens.json diff --git a/src/test-data/pug-parser/cases/code.tokens.json b/src/tests/sample_data/pug-parser/cases/code.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/code.tokens.json rename to src/tests/sample_data/pug-parser/cases/code.tokens.json diff --git a/src/test-data/pug-parser/cases/comments-in-case.tokens.json b/src/tests/sample_data/pug-parser/cases/comments-in-case.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/comments-in-case.tokens.json rename to src/tests/sample_data/pug-parser/cases/comments-in-case.tokens.json diff --git a/src/test-data/pug-parser/cases/comments.source.tokens.json b/src/tests/sample_data/pug-parser/cases/comments.source.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/comments.source.tokens.json rename to src/tests/sample_data/pug-parser/cases/comments.source.tokens.json diff --git a/src/test-data/pug-parser/cases/comments.tokens.json b/src/tests/sample_data/pug-parser/cases/comments.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/comments.tokens.json rename to src/tests/sample_data/pug-parser/cases/comments.tokens.json diff --git a/src/test-data/pug-parser/cases/doctype.custom.tokens.json b/src/tests/sample_data/pug-parser/cases/doctype.custom.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/doctype.custom.tokens.json rename to src/tests/sample_data/pug-parser/cases/doctype.custom.tokens.json diff --git a/src/test-data/pug-parser/cases/doctype.default.tokens.json b/src/tests/sample_data/pug-parser/cases/doctype.default.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/doctype.default.tokens.json rename to src/tests/sample_data/pug-parser/cases/doctype.default.tokens.json diff --git a/src/test-data/pug-parser/cases/doctype.keyword.tokens.json b/src/tests/sample_data/pug-parser/cases/doctype.keyword.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/doctype.keyword.tokens.json rename to src/tests/sample_data/pug-parser/cases/doctype.keyword.tokens.json diff --git a/src/test-data/pug-parser/cases/each.else.tokens.json b/src/tests/sample_data/pug-parser/cases/each.else.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/each.else.tokens.json rename to src/tests/sample_data/pug-parser/cases/each.else.tokens.json diff --git a/src/test-data/pug-parser/cases/escape-chars.tokens.json b/src/tests/sample_data/pug-parser/cases/escape-chars.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/escape-chars.tokens.json rename to src/tests/sample_data/pug-parser/cases/escape-chars.tokens.json diff --git a/src/test-data/pug-parser/cases/escape-test.tokens.json b/src/tests/sample_data/pug-parser/cases/escape-test.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/escape-test.tokens.json rename to src/tests/sample_data/pug-parser/cases/escape-test.tokens.json diff --git a/src/test-data/pug-parser/cases/escaping-class-attribute.tokens.json b/src/tests/sample_data/pug-parser/cases/escaping-class-attribute.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/escaping-class-attribute.tokens.json rename to src/tests/sample_data/pug-parser/cases/escaping-class-attribute.tokens.json diff --git a/src/test-data/pug-parser/cases/filter-in-include.tokens.json b/src/tests/sample_data/pug-parser/cases/filter-in-include.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filter-in-include.tokens.json rename to src/tests/sample_data/pug-parser/cases/filter-in-include.tokens.json diff --git a/src/test-data/pug-parser/cases/filters-empty.tokens.json b/src/tests/sample_data/pug-parser/cases/filters-empty.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters-empty.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters-empty.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.coffeescript.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.coffeescript.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.coffeescript.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.coffeescript.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.custom.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.custom.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.custom.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.custom.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.include.custom.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.include.custom.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.include.custom.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.include.custom.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.include.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.include.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.include.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.include.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.inline.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.inline.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.inline.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.inline.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.less.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.less.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.less.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.less.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.markdown.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.markdown.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.markdown.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.markdown.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.nested.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.nested.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.nested.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.nested.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.stylus.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.stylus.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.stylus.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.stylus.tokens.json diff --git a/src/test-data/pug-parser/cases/filters.verbatim.tokens.json b/src/tests/sample_data/pug-parser/cases/filters.verbatim.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/filters.verbatim.tokens.json rename to src/tests/sample_data/pug-parser/cases/filters.verbatim.tokens.json diff --git a/src/test-data/pug-parser/cases/html.tokens.json b/src/tests/sample_data/pug-parser/cases/html.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/html.tokens.json rename to src/tests/sample_data/pug-parser/cases/html.tokens.json diff --git a/src/test-data/pug-parser/cases/html5.tokens.json b/src/tests/sample_data/pug-parser/cases/html5.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/html5.tokens.json rename to src/tests/sample_data/pug-parser/cases/html5.tokens.json diff --git a/src/test-data/pug-parser/cases/include-extends-from-root.tokens.json b/src/tests/sample_data/pug-parser/cases/include-extends-from-root.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-extends-from-root.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-extends-from-root.tokens.json diff --git a/src/test-data/pug-parser/cases/include-extends-of-common-template.tokens.json b/src/tests/sample_data/pug-parser/cases/include-extends-of-common-template.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-extends-of-common-template.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-extends-of-common-template.tokens.json diff --git a/src/test-data/pug-parser/cases/include-extends-relative.tokens.json b/src/tests/sample_data/pug-parser/cases/include-extends-relative.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-extends-relative.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-extends-relative.tokens.json diff --git a/src/test-data/pug-parser/cases/include-only-text-body.tokens.json b/src/tests/sample_data/pug-parser/cases/include-only-text-body.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-only-text-body.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-only-text-body.tokens.json diff --git a/src/test-data/pug-parser/cases/include-only-text.tokens.json b/src/tests/sample_data/pug-parser/cases/include-only-text.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-only-text.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-only-text.tokens.json diff --git a/src/test-data/pug-parser/cases/include-with-text-head.tokens.json b/src/tests/sample_data/pug-parser/cases/include-with-text-head.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-with-text-head.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-with-text-head.tokens.json diff --git a/src/test-data/pug-parser/cases/include-with-text.tokens.json b/src/tests/sample_data/pug-parser/cases/include-with-text.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include-with-text.tokens.json rename to src/tests/sample_data/pug-parser/cases/include-with-text.tokens.json diff --git a/src/test-data/pug-parser/cases/include.script.tokens.json b/src/tests/sample_data/pug-parser/cases/include.script.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include.script.tokens.json rename to src/tests/sample_data/pug-parser/cases/include.script.tokens.json diff --git a/src/test-data/pug-parser/cases/include.yield.nested.tokens.json b/src/tests/sample_data/pug-parser/cases/include.yield.nested.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/include.yield.nested.tokens.json rename to src/tests/sample_data/pug-parser/cases/include.yield.nested.tokens.json diff --git a/src/test-data/pug-parser/cases/includes-with-ext-js.tokens.json b/src/tests/sample_data/pug-parser/cases/includes-with-ext-js.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/includes-with-ext-js.tokens.json rename to src/tests/sample_data/pug-parser/cases/includes-with-ext-js.tokens.json diff --git a/src/test-data/pug-parser/cases/includes.tokens.json b/src/tests/sample_data/pug-parser/cases/includes.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/includes.tokens.json rename to src/tests/sample_data/pug-parser/cases/includes.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.alert-dialog.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.alert-dialog.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.alert-dialog.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.alert-dialog.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.defaults.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.defaults.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.defaults.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.defaults.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.extend.include.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.extend.include.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.extend.include.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.extend.include.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.extend.mixins.block.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.extend.mixins.block.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.extend.mixins.block.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.extend.mixins.block.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.extend.mixins.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.extend.mixins.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.extend.mixins.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.extend.mixins.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.extend.recursive.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.extend.recursive.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.extend.recursive.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.extend.recursive.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.extend.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.extend.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.extend.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.extend.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.extend.whitespace.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.extend.whitespace.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.extend.whitespace.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.extend.whitespace.tokens.json diff --git a/src/test-data/pug-parser/cases/inheritance.tokens.json b/src/tests/sample_data/pug-parser/cases/inheritance.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inheritance.tokens.json rename to src/tests/sample_data/pug-parser/cases/inheritance.tokens.json diff --git a/src/test-data/pug-parser/cases/inline-block-comment.tokens.json b/src/tests/sample_data/pug-parser/cases/inline-block-comment.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inline-block-comment.tokens.json rename to src/tests/sample_data/pug-parser/cases/inline-block-comment.tokens.json diff --git a/src/test-data/pug-parser/cases/inline-tag.tokens.json b/src/tests/sample_data/pug-parser/cases/inline-tag.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/inline-tag.tokens.json rename to src/tests/sample_data/pug-parser/cases/inline-tag.tokens.json diff --git a/src/test-data/pug-parser/cases/intepolated-elements.tokens.json b/src/tests/sample_data/pug-parser/cases/intepolated-elements.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/intepolated-elements.tokens.json rename to src/tests/sample_data/pug-parser/cases/intepolated-elements.tokens.json diff --git a/src/test-data/pug-parser/cases/interpolated-mixin.tokens.json b/src/tests/sample_data/pug-parser/cases/interpolated-mixin.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/interpolated-mixin.tokens.json rename to src/tests/sample_data/pug-parser/cases/interpolated-mixin.tokens.json diff --git a/src/test-data/pug-parser/cases/interpolation.escape.tokens.json b/src/tests/sample_data/pug-parser/cases/interpolation.escape.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/interpolation.escape.tokens.json rename to src/tests/sample_data/pug-parser/cases/interpolation.escape.tokens.json diff --git a/src/test-data/pug-parser/cases/layout.append.tokens.json b/src/tests/sample_data/pug-parser/cases/layout.append.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/layout.append.tokens.json rename to src/tests/sample_data/pug-parser/cases/layout.append.tokens.json diff --git a/src/test-data/pug-parser/cases/layout.append.without-block.tokens.json b/src/tests/sample_data/pug-parser/cases/layout.append.without-block.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/layout.append.without-block.tokens.json rename to src/tests/sample_data/pug-parser/cases/layout.append.without-block.tokens.json diff --git a/src/test-data/pug-parser/cases/layout.multi.append.prepend.block.tokens.json b/src/tests/sample_data/pug-parser/cases/layout.multi.append.prepend.block.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/layout.multi.append.prepend.block.tokens.json rename to src/tests/sample_data/pug-parser/cases/layout.multi.append.prepend.block.tokens.json diff --git a/src/test-data/pug-parser/cases/layout.prepend.tokens.json b/src/tests/sample_data/pug-parser/cases/layout.prepend.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/layout.prepend.tokens.json rename to src/tests/sample_data/pug-parser/cases/layout.prepend.tokens.json diff --git a/src/test-data/pug-parser/cases/layout.prepend.without-block.tokens.json b/src/tests/sample_data/pug-parser/cases/layout.prepend.without-block.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/layout.prepend.without-block.tokens.json rename to src/tests/sample_data/pug-parser/cases/layout.prepend.without-block.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin-at-end-of-file.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin-at-end-of-file.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin-at-end-of-file.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin-at-end-of-file.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin-block-with-space.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin-block-with-space.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin-block-with-space.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin-block-with-space.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin-hoist.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin-hoist.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin-hoist.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin-hoist.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin-via-include.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin-via-include.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin-via-include.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin-via-include.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin.attrs.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin.attrs.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin.attrs.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin.attrs.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin.block-tag-behaviour.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin.block-tag-behaviour.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin.block-tag-behaviour.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin.block-tag-behaviour.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin.blocks.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin.blocks.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin.blocks.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin.blocks.tokens.json diff --git a/src/test-data/pug-parser/cases/mixin.merge.tokens.json b/src/tests/sample_data/pug-parser/cases/mixin.merge.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixin.merge.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixin.merge.tokens.json diff --git a/src/test-data/pug-parser/cases/mixins-unused.tokens.json b/src/tests/sample_data/pug-parser/cases/mixins-unused.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixins-unused.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixins-unused.tokens.json diff --git a/src/test-data/pug-parser/cases/mixins.rest-args.tokens.json b/src/tests/sample_data/pug-parser/cases/mixins.rest-args.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixins.rest-args.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixins.rest-args.tokens.json diff --git a/src/test-data/pug-parser/cases/mixins.tokens.json b/src/tests/sample_data/pug-parser/cases/mixins.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/mixins.tokens.json rename to src/tests/sample_data/pug-parser/cases/mixins.tokens.json diff --git a/src/test-data/pug-parser/cases/namespaces.tokens.json b/src/tests/sample_data/pug-parser/cases/namespaces.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/namespaces.tokens.json rename to src/tests/sample_data/pug-parser/cases/namespaces.tokens.json diff --git a/src/test-data/pug-parser/cases/nesting.tokens.json b/src/tests/sample_data/pug-parser/cases/nesting.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/nesting.tokens.json rename to src/tests/sample_data/pug-parser/cases/nesting.tokens.json diff --git a/src/test-data/pug-parser/cases/pipeless-comments.tokens.json b/src/tests/sample_data/pug-parser/cases/pipeless-comments.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/pipeless-comments.tokens.json rename to src/tests/sample_data/pug-parser/cases/pipeless-comments.tokens.json diff --git a/src/test-data/pug-parser/cases/pipeless-filters.tokens.json b/src/tests/sample_data/pug-parser/cases/pipeless-filters.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/pipeless-filters.tokens.json rename to src/tests/sample_data/pug-parser/cases/pipeless-filters.tokens.json diff --git a/src/test-data/pug-parser/cases/pipeless-tag.tokens.json b/src/tests/sample_data/pug-parser/cases/pipeless-tag.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/pipeless-tag.tokens.json rename to src/tests/sample_data/pug-parser/cases/pipeless-tag.tokens.json diff --git a/src/test-data/pug-parser/cases/pre.tokens.json b/src/tests/sample_data/pug-parser/cases/pre.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/pre.tokens.json rename to src/tests/sample_data/pug-parser/cases/pre.tokens.json diff --git a/src/test-data/pug-parser/cases/quotes.tokens.json b/src/tests/sample_data/pug-parser/cases/quotes.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/quotes.tokens.json rename to src/tests/sample_data/pug-parser/cases/quotes.tokens.json diff --git a/src/test-data/pug-parser/cases/regression.1794.tokens.json b/src/tests/sample_data/pug-parser/cases/regression.1794.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/regression.1794.tokens.json rename to src/tests/sample_data/pug-parser/cases/regression.1794.tokens.json diff --git a/src/test-data/pug-parser/cases/regression.784.tokens.json b/src/tests/sample_data/pug-parser/cases/regression.784.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/regression.784.tokens.json rename to src/tests/sample_data/pug-parser/cases/regression.784.tokens.json diff --git a/src/test-data/pug-parser/cases/script.whitespace.tokens.json b/src/tests/sample_data/pug-parser/cases/script.whitespace.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/script.whitespace.tokens.json rename to src/tests/sample_data/pug-parser/cases/script.whitespace.tokens.json diff --git a/src/test-data/pug-parser/cases/scripts.non-js.tokens.json b/src/tests/sample_data/pug-parser/cases/scripts.non-js.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/scripts.non-js.tokens.json rename to src/tests/sample_data/pug-parser/cases/scripts.non-js.tokens.json diff --git a/src/test-data/pug-parser/cases/scripts.tokens.json b/src/tests/sample_data/pug-parser/cases/scripts.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/scripts.tokens.json rename to src/tests/sample_data/pug-parser/cases/scripts.tokens.json diff --git a/src/test-data/pug-parser/cases/self-closing-html.tokens.json b/src/tests/sample_data/pug-parser/cases/self-closing-html.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/self-closing-html.tokens.json rename to src/tests/sample_data/pug-parser/cases/self-closing-html.tokens.json diff --git a/src/test-data/pug-parser/cases/single-period.tokens.json b/src/tests/sample_data/pug-parser/cases/single-period.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/single-period.tokens.json rename to src/tests/sample_data/pug-parser/cases/single-period.tokens.json diff --git a/src/test-data/pug-parser/cases/source.tokens.json b/src/tests/sample_data/pug-parser/cases/source.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/source.tokens.json rename to src/tests/sample_data/pug-parser/cases/source.tokens.json diff --git a/src/test-data/pug-parser/cases/styles.tokens.json b/src/tests/sample_data/pug-parser/cases/styles.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/styles.tokens.json rename to src/tests/sample_data/pug-parser/cases/styles.tokens.json diff --git a/src/test-data/pug-parser/cases/tag-blocks.tokens.json b/src/tests/sample_data/pug-parser/cases/tag-blocks.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/tag-blocks.tokens.json rename to src/tests/sample_data/pug-parser/cases/tag-blocks.tokens.json diff --git a/src/test-data/pug-parser/cases/tag.interpolation.tokens.json b/src/tests/sample_data/pug-parser/cases/tag.interpolation.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/tag.interpolation.tokens.json rename to src/tests/sample_data/pug-parser/cases/tag.interpolation.tokens.json diff --git a/src/test-data/pug-parser/cases/tags.self-closing.tokens.json b/src/tests/sample_data/pug-parser/cases/tags.self-closing.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/tags.self-closing.tokens.json rename to src/tests/sample_data/pug-parser/cases/tags.self-closing.tokens.json diff --git a/src/test-data/pug-parser/cases/template.tokens.json b/src/tests/sample_data/pug-parser/cases/template.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/template.tokens.json rename to src/tests/sample_data/pug-parser/cases/template.tokens.json diff --git a/src/test-data/pug-parser/cases/text-block.tokens.json b/src/tests/sample_data/pug-parser/cases/text-block.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/text-block.tokens.json rename to src/tests/sample_data/pug-parser/cases/text-block.tokens.json diff --git a/src/test-data/pug-parser/cases/text.tokens.json b/src/tests/sample_data/pug-parser/cases/text.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/text.tokens.json rename to src/tests/sample_data/pug-parser/cases/text.tokens.json diff --git a/src/test-data/pug-parser/cases/utf8bom.tokens.json b/src/tests/sample_data/pug-parser/cases/utf8bom.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/utf8bom.tokens.json rename to src/tests/sample_data/pug-parser/cases/utf8bom.tokens.json diff --git a/src/test-data/pug-parser/cases/vars.tokens.json b/src/tests/sample_data/pug-parser/cases/vars.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/vars.tokens.json rename to src/tests/sample_data/pug-parser/cases/vars.tokens.json diff --git a/src/test-data/pug-parser/cases/while.tokens.json b/src/tests/sample_data/pug-parser/cases/while.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/while.tokens.json rename to src/tests/sample_data/pug-parser/cases/while.tokens.json diff --git a/src/test-data/pug-parser/cases/xml.tokens.json b/src/tests/sample_data/pug-parser/cases/xml.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/xml.tokens.json rename to src/tests/sample_data/pug-parser/cases/xml.tokens.json diff --git a/src/test-data/pug-parser/cases/yield-before-conditional-head.tokens.json b/src/tests/sample_data/pug-parser/cases/yield-before-conditional-head.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/yield-before-conditional-head.tokens.json rename to src/tests/sample_data/pug-parser/cases/yield-before-conditional-head.tokens.json diff --git a/src/test-data/pug-parser/cases/yield-before-conditional.tokens.json b/src/tests/sample_data/pug-parser/cases/yield-before-conditional.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/yield-before-conditional.tokens.json rename to src/tests/sample_data/pug-parser/cases/yield-before-conditional.tokens.json diff --git a/src/test-data/pug-parser/cases/yield-head.tokens.json b/src/tests/sample_data/pug-parser/cases/yield-head.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/yield-head.tokens.json rename to src/tests/sample_data/pug-parser/cases/yield-head.tokens.json diff --git a/src/test-data/pug-parser/cases/yield-title-head.tokens.json b/src/tests/sample_data/pug-parser/cases/yield-title-head.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/yield-title-head.tokens.json rename to src/tests/sample_data/pug-parser/cases/yield-title-head.tokens.json diff --git a/src/test-data/pug-parser/cases/yield-title.tokens.json b/src/tests/sample_data/pug-parser/cases/yield-title.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/yield-title.tokens.json rename to src/tests/sample_data/pug-parser/cases/yield-title.tokens.json diff --git a/src/test-data/pug-parser/cases/yield.tokens.json b/src/tests/sample_data/pug-parser/cases/yield.tokens.json similarity index 100% rename from src/test-data/pug-parser/cases/yield.tokens.json rename to src/tests/sample_data/pug-parser/cases/yield.tokens.json diff --git a/src/test-data/pug-runtime/test/index.test.js b/src/tests/sample_data/pug-runtime/test/index.test.js similarity index 100% rename from src/test-data/pug-runtime/test/index.test.js rename to src/tests/sample_data/pug-runtime/test/index.test.js diff --git a/src/test-data/pug/examples/README.md b/src/tests/sample_data/pug/examples/README.md similarity index 100% rename from src/test-data/pug/examples/README.md rename to src/tests/sample_data/pug/examples/README.md diff --git a/src/test-data/pug/examples/attributes.js b/src/tests/sample_data/pug/examples/attributes.js similarity index 100% rename from src/test-data/pug/examples/attributes.js rename to src/tests/sample_data/pug/examples/attributes.js diff --git a/src/test-data/pug/examples/attributes.pug b/src/tests/sample_data/pug/examples/attributes.pug similarity index 100% rename from src/test-data/pug/examples/attributes.pug rename to src/tests/sample_data/pug/examples/attributes.pug diff --git a/src/test-data/pug/examples/code.js b/src/tests/sample_data/pug/examples/code.js similarity index 100% rename from src/test-data/pug/examples/code.js rename to src/tests/sample_data/pug/examples/code.js diff --git a/src/test-data/pug/examples/code.pug b/src/tests/sample_data/pug/examples/code.pug similarity index 100% rename from src/test-data/pug/examples/code.pug rename to src/tests/sample_data/pug/examples/code.pug diff --git a/src/test-data/pug/examples/dynamicscript.js b/src/tests/sample_data/pug/examples/dynamicscript.js similarity index 100% rename from src/test-data/pug/examples/dynamicscript.js rename to src/tests/sample_data/pug/examples/dynamicscript.js diff --git a/src/test-data/pug/examples/dynamicscript.pug b/src/tests/sample_data/pug/examples/dynamicscript.pug similarity index 100% rename from src/test-data/pug/examples/dynamicscript.pug rename to src/tests/sample_data/pug/examples/dynamicscript.pug diff --git a/src/test-data/pug/examples/each.js b/src/tests/sample_data/pug/examples/each.js similarity index 100% rename from src/test-data/pug/examples/each.js rename to src/tests/sample_data/pug/examples/each.js diff --git a/src/test-data/pug/examples/each.pug b/src/tests/sample_data/pug/examples/each.pug similarity index 100% rename from src/test-data/pug/examples/each.pug rename to src/tests/sample_data/pug/examples/each.pug diff --git a/src/test-data/pug/examples/extend-layout.pug b/src/tests/sample_data/pug/examples/extend-layout.pug similarity index 100% rename from src/test-data/pug/examples/extend-layout.pug rename to src/tests/sample_data/pug/examples/extend-layout.pug diff --git a/src/test-data/pug/examples/extend.js b/src/tests/sample_data/pug/examples/extend.js similarity index 100% rename from src/test-data/pug/examples/extend.js rename to src/tests/sample_data/pug/examples/extend.js diff --git a/src/test-data/pug/examples/extend.pug b/src/tests/sample_data/pug/examples/extend.pug similarity index 100% rename from src/test-data/pug/examples/extend.pug rename to src/tests/sample_data/pug/examples/extend.pug diff --git a/src/test-data/pug/examples/form.js b/src/tests/sample_data/pug/examples/form.js similarity index 100% rename from src/test-data/pug/examples/form.js rename to src/tests/sample_data/pug/examples/form.js diff --git a/src/test-data/pug/examples/form.pug b/src/tests/sample_data/pug/examples/form.pug similarity index 100% rename from src/test-data/pug/examples/form.pug rename to src/tests/sample_data/pug/examples/form.pug diff --git a/src/test-data/pug/examples/includes.js b/src/tests/sample_data/pug/examples/includes.js similarity index 100% rename from src/test-data/pug/examples/includes.js rename to src/tests/sample_data/pug/examples/includes.js diff --git a/src/test-data/pug/examples/includes.pug b/src/tests/sample_data/pug/examples/includes.pug similarity index 100% rename from src/test-data/pug/examples/includes.pug rename to src/tests/sample_data/pug/examples/includes.pug diff --git a/src/test-data/pug/examples/includes/foot.pug b/src/tests/sample_data/pug/examples/includes/foot.pug similarity index 100% rename from src/test-data/pug/examples/includes/foot.pug rename to src/tests/sample_data/pug/examples/includes/foot.pug diff --git a/src/test-data/pug/examples/includes/head.pug b/src/tests/sample_data/pug/examples/includes/head.pug similarity index 100% rename from src/test-data/pug/examples/includes/head.pug rename to src/tests/sample_data/pug/examples/includes/head.pug diff --git a/src/test-data/pug/examples/includes/scripts.pug b/src/tests/sample_data/pug/examples/includes/scripts.pug similarity index 100% rename from src/test-data/pug/examples/includes/scripts.pug rename to src/tests/sample_data/pug/examples/includes/scripts.pug diff --git a/src/test-data/pug/examples/includes/style.css b/src/tests/sample_data/pug/examples/includes/style.css similarity index 100% rename from src/test-data/pug/examples/includes/style.css rename to src/tests/sample_data/pug/examples/includes/style.css diff --git a/src/test-data/pug/examples/layout-debug.js b/src/tests/sample_data/pug/examples/layout-debug.js similarity index 100% rename from src/test-data/pug/examples/layout-debug.js rename to src/tests/sample_data/pug/examples/layout-debug.js diff --git a/src/test-data/pug/examples/layout.js b/src/tests/sample_data/pug/examples/layout.js similarity index 100% rename from src/test-data/pug/examples/layout.js rename to src/tests/sample_data/pug/examples/layout.js diff --git a/src/test-data/pug/examples/layout.pug b/src/tests/sample_data/pug/examples/layout.pug similarity index 100% rename from src/test-data/pug/examples/layout.pug rename to src/tests/sample_data/pug/examples/layout.pug diff --git a/src/test-data/pug/examples/mixins.js b/src/tests/sample_data/pug/examples/mixins.js similarity index 100% rename from src/test-data/pug/examples/mixins.js rename to src/tests/sample_data/pug/examples/mixins.js diff --git a/src/test-data/pug/examples/mixins.pug b/src/tests/sample_data/pug/examples/mixins.pug similarity index 100% rename from src/test-data/pug/examples/mixins.pug rename to src/tests/sample_data/pug/examples/mixins.pug diff --git a/src/test-data/pug/examples/mixins/dialog.pug b/src/tests/sample_data/pug/examples/mixins/dialog.pug similarity index 100% rename from src/test-data/pug/examples/mixins/dialog.pug rename to src/tests/sample_data/pug/examples/mixins/dialog.pug diff --git a/src/test-data/pug/examples/mixins/profile.pug b/src/tests/sample_data/pug/examples/mixins/profile.pug similarity index 100% rename from src/test-data/pug/examples/mixins/profile.pug rename to src/tests/sample_data/pug/examples/mixins/profile.pug diff --git a/src/test-data/pug/examples/pet.pug b/src/tests/sample_data/pug/examples/pet.pug similarity index 100% rename from src/test-data/pug/examples/pet.pug rename to src/tests/sample_data/pug/examples/pet.pug diff --git a/src/test-data/pug/examples/rss.js b/src/tests/sample_data/pug/examples/rss.js similarity index 100% rename from src/test-data/pug/examples/rss.js rename to src/tests/sample_data/pug/examples/rss.js diff --git a/src/test-data/pug/examples/rss.pug b/src/tests/sample_data/pug/examples/rss.pug similarity index 100% rename from src/test-data/pug/examples/rss.pug rename to src/tests/sample_data/pug/examples/rss.pug diff --git a/src/test-data/pug/examples/text.js b/src/tests/sample_data/pug/examples/text.js similarity index 100% rename from src/test-data/pug/examples/text.js rename to src/tests/sample_data/pug/examples/text.js diff --git a/src/test-data/pug/examples/text.pug b/src/tests/sample_data/pug/examples/text.pug similarity index 100% rename from src/test-data/pug/examples/text.pug rename to src/tests/sample_data/pug/examples/text.pug diff --git a/src/test-data/pug/examples/whitespace.js b/src/tests/sample_data/pug/examples/whitespace.js similarity index 100% rename from src/test-data/pug/examples/whitespace.js rename to src/tests/sample_data/pug/examples/whitespace.js diff --git a/src/test-data/pug/examples/whitespace.pug b/src/tests/sample_data/pug/examples/whitespace.pug similarity index 100% rename from src/test-data/pug/examples/whitespace.pug rename to src/tests/sample_data/pug/examples/whitespace.pug diff --git a/src/test-data/pug/test/README.md b/src/tests/sample_data/pug/test/README.md similarity index 100% rename from src/test-data/pug/test/README.md rename to src/tests/sample_data/pug/test/README.md diff --git a/src/test-data/pug/test/__snapshots__/pug.test.js.snap b/src/tests/sample_data/pug/test/__snapshots__/pug.test.js.snap similarity index 100% rename from src/test-data/pug/test/__snapshots__/pug.test.js.snap rename to src/tests/sample_data/pug/test/__snapshots__/pug.test.js.snap diff --git a/src/test-data/pug/test/anti-cases/attrs.unescaped.pug b/src/tests/sample_data/pug/test/anti-cases/attrs.unescaped.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/attrs.unescaped.pug rename to src/tests/sample_data/pug/test/anti-cases/attrs.unescaped.pug diff --git a/src/test-data/pug/test/anti-cases/case-when.pug b/src/tests/sample_data/pug/test/anti-cases/case-when.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/case-when.pug rename to src/tests/sample_data/pug/test/anti-cases/case-when.pug diff --git a/src/test-data/pug/test/anti-cases/case-without-with.pug b/src/tests/sample_data/pug/test/anti-cases/case-without-with.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/case-without-with.pug rename to src/tests/sample_data/pug/test/anti-cases/case-without-with.pug diff --git a/src/test-data/pug/test/anti-cases/else-condition.pug b/src/tests/sample_data/pug/test/anti-cases/else-condition.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/else-condition.pug rename to src/tests/sample_data/pug/test/anti-cases/else-condition.pug diff --git a/src/test-data/pug/test/anti-cases/else-without-if.pug b/src/tests/sample_data/pug/test/anti-cases/else-without-if.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/else-without-if.pug rename to src/tests/sample_data/pug/test/anti-cases/else-without-if.pug diff --git a/src/test-data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug b/src/tests/sample_data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug rename to src/tests/sample_data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug diff --git a/src/test-data/pug/test/anti-cases/key-char-ending-badly.pug b/src/tests/sample_data/pug/test/anti-cases/key-char-ending-badly.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/key-char-ending-badly.pug rename to src/tests/sample_data/pug/test/anti-cases/key-char-ending-badly.pug diff --git a/src/test-data/pug/test/anti-cases/key-ending-badly.pug b/src/tests/sample_data/pug/test/anti-cases/key-ending-badly.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/key-ending-badly.pug rename to src/tests/sample_data/pug/test/anti-cases/key-ending-badly.pug diff --git a/src/test-data/pug/test/anti-cases/mismatched-inline-tag.pug b/src/tests/sample_data/pug/test/anti-cases/mismatched-inline-tag.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/mismatched-inline-tag.pug rename to src/tests/sample_data/pug/test/anti-cases/mismatched-inline-tag.pug diff --git a/src/test-data/pug/test/anti-cases/mixin-args-syntax-error.pug b/src/tests/sample_data/pug/test/anti-cases/mixin-args-syntax-error.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/mixin-args-syntax-error.pug rename to src/tests/sample_data/pug/test/anti-cases/mixin-args-syntax-error.pug diff --git a/src/test-data/pug/test/anti-cases/mixins-blocks-with-bodies.pug b/src/tests/sample_data/pug/test/anti-cases/mixins-blocks-with-bodies.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/mixins-blocks-with-bodies.pug rename to src/tests/sample_data/pug/test/anti-cases/mixins-blocks-with-bodies.pug diff --git a/src/test-data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug b/src/tests/sample_data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug rename to src/tests/sample_data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug diff --git a/src/test-data/pug/test/anti-cases/non-existant-filter.pug b/src/tests/sample_data/pug/test/anti-cases/non-existant-filter.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/non-existant-filter.pug rename to src/tests/sample_data/pug/test/anti-cases/non-existant-filter.pug diff --git a/src/test-data/pug/test/anti-cases/non-mixin-block.pug b/src/tests/sample_data/pug/test/anti-cases/non-mixin-block.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/non-mixin-block.pug rename to src/tests/sample_data/pug/test/anti-cases/non-mixin-block.pug diff --git a/src/test-data/pug/test/anti-cases/open-brace-in-attributes.pug b/src/tests/sample_data/pug/test/anti-cases/open-brace-in-attributes.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/open-brace-in-attributes.pug rename to src/tests/sample_data/pug/test/anti-cases/open-brace-in-attributes.pug diff --git a/src/test-data/pug/test/anti-cases/readme.md b/src/tests/sample_data/pug/test/anti-cases/readme.md similarity index 100% rename from src/test-data/pug/test/anti-cases/readme.md rename to src/tests/sample_data/pug/test/anti-cases/readme.md diff --git a/src/test-data/pug/test/anti-cases/self-closing-tag-with-block.pug b/src/tests/sample_data/pug/test/anti-cases/self-closing-tag-with-block.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/self-closing-tag-with-block.pug rename to src/tests/sample_data/pug/test/anti-cases/self-closing-tag-with-block.pug diff --git a/src/test-data/pug/test/anti-cases/self-closing-tag-with-body.pug b/src/tests/sample_data/pug/test/anti-cases/self-closing-tag-with-body.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/self-closing-tag-with-body.pug rename to src/tests/sample_data/pug/test/anti-cases/self-closing-tag-with-body.pug diff --git a/src/test-data/pug/test/anti-cases/self-closing-tag-with-code.pug b/src/tests/sample_data/pug/test/anti-cases/self-closing-tag-with-code.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/self-closing-tag-with-code.pug rename to src/tests/sample_data/pug/test/anti-cases/self-closing-tag-with-code.pug diff --git a/src/test-data/pug/test/anti-cases/tabs-and-spaces.pug b/src/tests/sample_data/pug/test/anti-cases/tabs-and-spaces.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/tabs-and-spaces.pug rename to src/tests/sample_data/pug/test/anti-cases/tabs-and-spaces.pug diff --git a/src/test-data/pug/test/anti-cases/unclosed-interpolated-call.pug b/src/tests/sample_data/pug/test/anti-cases/unclosed-interpolated-call.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/unclosed-interpolated-call.pug rename to src/tests/sample_data/pug/test/anti-cases/unclosed-interpolated-call.pug diff --git a/src/test-data/pug/test/anti-cases/unclosed-interpolated-tag.pug b/src/tests/sample_data/pug/test/anti-cases/unclosed-interpolated-tag.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/unclosed-interpolated-tag.pug rename to src/tests/sample_data/pug/test/anti-cases/unclosed-interpolated-tag.pug diff --git a/src/test-data/pug/test/anti-cases/unclosed-interpolation.pug b/src/tests/sample_data/pug/test/anti-cases/unclosed-interpolation.pug similarity index 100% rename from src/test-data/pug/test/anti-cases/unclosed-interpolation.pug rename to src/tests/sample_data/pug/test/anti-cases/unclosed-interpolation.pug diff --git a/src/test-data/pug/test/browser/index.html b/src/tests/sample_data/pug/test/browser/index.html similarity index 100% rename from src/test-data/pug/test/browser/index.html rename to src/tests/sample_data/pug/test/browser/index.html diff --git a/src/test-data/pug/test/browser/index.pug b/src/tests/sample_data/pug/test/browser/index.pug similarity index 100% rename from src/test-data/pug/test/browser/index.pug rename to src/tests/sample_data/pug/test/browser/index.pug diff --git a/src/test-data/pug/test/cases-es2015/attr.html b/src/tests/sample_data/pug/test/cases-es2015/attr.html similarity index 100% rename from src/test-data/pug/test/cases-es2015/attr.html rename to src/tests/sample_data/pug/test/cases-es2015/attr.html diff --git a/src/test-data/pug/test/cases-es2015/attr.pug b/src/tests/sample_data/pug/test/cases-es2015/attr.pug similarity index 100% rename from src/test-data/pug/test/cases-es2015/attr.pug rename to src/tests/sample_data/pug/test/cases-es2015/attr.pug diff --git a/src/test-data/pug/test/cases/attrs-data.html b/src/tests/sample_data/pug/test/cases/attrs-data.html similarity index 100% rename from src/test-data/pug/test/cases/attrs-data.html rename to src/tests/sample_data/pug/test/cases/attrs-data.html diff --git a/src/test-data/pug/test/cases/attrs-data.pug b/src/tests/sample_data/pug/test/cases/attrs-data.pug similarity index 100% rename from src/test-data/pug/test/cases/attrs-data.pug rename to src/tests/sample_data/pug/test/cases/attrs-data.pug diff --git a/src/test-data/pug/test/cases/attrs.colon.html b/src/tests/sample_data/pug/test/cases/attrs.colon.html similarity index 100% rename from src/test-data/pug/test/cases/attrs.colon.html rename to src/tests/sample_data/pug/test/cases/attrs.colon.html diff --git a/src/test-data/pug/test/cases/attrs.colon.pug b/src/tests/sample_data/pug/test/cases/attrs.colon.pug similarity index 100% rename from src/test-data/pug/test/cases/attrs.colon.pug rename to src/tests/sample_data/pug/test/cases/attrs.colon.pug diff --git a/src/test-data/pug/test/cases/attrs.html b/src/tests/sample_data/pug/test/cases/attrs.html similarity index 100% rename from src/test-data/pug/test/cases/attrs.html rename to src/tests/sample_data/pug/test/cases/attrs.html diff --git a/src/test-data/pug/test/cases/attrs.js.html b/src/tests/sample_data/pug/test/cases/attrs.js.html similarity index 100% rename from src/test-data/pug/test/cases/attrs.js.html rename to src/tests/sample_data/pug/test/cases/attrs.js.html diff --git a/src/test-data/pug/test/cases/attrs.js.pug b/src/tests/sample_data/pug/test/cases/attrs.js.pug similarity index 100% rename from src/test-data/pug/test/cases/attrs.js.pug rename to src/tests/sample_data/pug/test/cases/attrs.js.pug diff --git a/src/test-data/pug/test/cases/attrs.pug b/src/tests/sample_data/pug/test/cases/attrs.pug similarity index 100% rename from src/test-data/pug/test/cases/attrs.pug rename to src/tests/sample_data/pug/test/cases/attrs.pug diff --git a/src/test-data/pug/test/cases/attrs.unescaped.html b/src/tests/sample_data/pug/test/cases/attrs.unescaped.html similarity index 100% rename from src/test-data/pug/test/cases/attrs.unescaped.html rename to src/tests/sample_data/pug/test/cases/attrs.unescaped.html diff --git a/src/test-data/pug/test/cases/attrs.unescaped.pug b/src/tests/sample_data/pug/test/cases/attrs.unescaped.pug similarity index 100% rename from src/test-data/pug/test/cases/attrs.unescaped.pug rename to src/tests/sample_data/pug/test/cases/attrs.unescaped.pug diff --git a/src/test-data/pug/test/cases/auxiliary/1794-extends.pug b/src/tests/sample_data/pug/test/cases/auxiliary/1794-extends.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/1794-extends.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/1794-extends.pug diff --git a/src/test-data/pug/test/cases/auxiliary/1794-include.pug b/src/tests/sample_data/pug/test/cases/auxiliary/1794-include.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/1794-include.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/1794-include.pug diff --git a/src/test-data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug b/src/tests/sample_data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug diff --git a/src/test-data/pug/test/cases/auxiliary/dialog.pug b/src/tests/sample_data/pug/test/cases/auxiliary/dialog.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/dialog.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/dialog.pug diff --git a/src/test-data/pug/test/cases/auxiliary/empty-block.pug b/src/tests/sample_data/pug/test/cases/auxiliary/empty-block.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/empty-block.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/empty-block.pug diff --git a/src/test-data/pug/test/cases/auxiliary/escapes.html b/src/tests/sample_data/pug/test/cases/auxiliary/escapes.html similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/escapes.html rename to src/tests/sample_data/pug/test/cases/auxiliary/escapes.html diff --git a/src/test-data/pug/test/cases/auxiliary/extends-empty-block-1.pug b/src/tests/sample_data/pug/test/cases/auxiliary/extends-empty-block-1.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/extends-empty-block-1.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/extends-empty-block-1.pug diff --git a/src/test-data/pug/test/cases/auxiliary/extends-empty-block-2.pug b/src/tests/sample_data/pug/test/cases/auxiliary/extends-empty-block-2.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/extends-empty-block-2.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/extends-empty-block-2.pug diff --git a/src/test-data/pug/test/cases/auxiliary/extends-from-root.pug b/src/tests/sample_data/pug/test/cases/auxiliary/extends-from-root.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/extends-from-root.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/extends-from-root.pug diff --git a/src/test-data/pug/test/cases/auxiliary/extends-relative.pug b/src/tests/sample_data/pug/test/cases/auxiliary/extends-relative.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/extends-relative.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/extends-relative.pug diff --git a/src/test-data/pug/test/cases/auxiliary/filter-in-include.pug b/src/tests/sample_data/pug/test/cases/auxiliary/filter-in-include.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/filter-in-include.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/filter-in-include.pug diff --git a/src/test-data/pug/test/cases/auxiliary/includable.js b/src/tests/sample_data/pug/test/cases/auxiliary/includable.js similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/includable.js rename to src/tests/sample_data/pug/test/cases/auxiliary/includable.js diff --git a/src/test-data/pug/test/cases/auxiliary/include-from-root.pug b/src/tests/sample_data/pug/test/cases/auxiliary/include-from-root.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/include-from-root.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/include-from-root.pug diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug b/src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug b/src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug b/src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug b/src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug diff --git a/src/test-data/pug/test/cases/auxiliary/layout.include.pug b/src/tests/sample_data/pug/test/cases/auxiliary/layout.include.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/layout.include.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/layout.include.pug diff --git a/src/test-data/pug/test/cases/auxiliary/layout.pug b/src/tests/sample_data/pug/test/cases/auxiliary/layout.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/layout.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/layout.pug diff --git a/src/test-data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug b/src/tests/sample_data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug diff --git a/src/test-data/pug/test/cases/auxiliary/mixins.pug b/src/tests/sample_data/pug/test/cases/auxiliary/mixins.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/mixins.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/mixins.pug diff --git a/src/test-data/pug/test/cases/auxiliary/pet.pug b/src/tests/sample_data/pug/test/cases/auxiliary/pet.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/pet.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/pet.pug diff --git a/src/test-data/pug/test/cases/auxiliary/smile.html b/src/tests/sample_data/pug/test/cases/auxiliary/smile.html similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/smile.html rename to src/tests/sample_data/pug/test/cases/auxiliary/smile.html diff --git a/src/test-data/pug/test/cases/auxiliary/window.pug b/src/tests/sample_data/pug/test/cases/auxiliary/window.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/window.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/window.pug diff --git a/src/test-data/pug/test/cases/auxiliary/yield-nested.pug b/src/tests/sample_data/pug/test/cases/auxiliary/yield-nested.pug similarity index 100% rename from src/test-data/pug/test/cases/auxiliary/yield-nested.pug rename to src/tests/sample_data/pug/test/cases/auxiliary/yield-nested.pug diff --git a/src/test-data/pug/test/cases/basic.html b/src/tests/sample_data/pug/test/cases/basic.html similarity index 100% rename from src/test-data/pug/test/cases/basic.html rename to src/tests/sample_data/pug/test/cases/basic.html diff --git a/src/test-data/pug/test/cases/basic.pug b/src/tests/sample_data/pug/test/cases/basic.pug similarity index 100% rename from src/test-data/pug/test/cases/basic.pug rename to src/tests/sample_data/pug/test/cases/basic.pug diff --git a/src/test-data/pug/test/cases/blanks.html b/src/tests/sample_data/pug/test/cases/blanks.html similarity index 100% rename from src/test-data/pug/test/cases/blanks.html rename to src/tests/sample_data/pug/test/cases/blanks.html diff --git a/src/test-data/pug/test/cases/blanks.pug b/src/tests/sample_data/pug/test/cases/blanks.pug similarity index 100% rename from src/test-data/pug/test/cases/blanks.pug rename to src/tests/sample_data/pug/test/cases/blanks.pug diff --git a/src/test-data/pug/test/cases/block-code.html b/src/tests/sample_data/pug/test/cases/block-code.html similarity index 100% rename from src/test-data/pug/test/cases/block-code.html rename to src/tests/sample_data/pug/test/cases/block-code.html diff --git a/src/test-data/pug/test/cases/block-code.pug b/src/tests/sample_data/pug/test/cases/block-code.pug similarity index 100% rename from src/test-data/pug/test/cases/block-code.pug rename to src/tests/sample_data/pug/test/cases/block-code.pug diff --git a/src/test-data/pug/test/cases/block-expansion.html b/src/tests/sample_data/pug/test/cases/block-expansion.html similarity index 100% rename from src/test-data/pug/test/cases/block-expansion.html rename to src/tests/sample_data/pug/test/cases/block-expansion.html diff --git a/src/test-data/pug/test/cases/block-expansion.pug b/src/tests/sample_data/pug/test/cases/block-expansion.pug similarity index 100% rename from src/test-data/pug/test/cases/block-expansion.pug rename to src/tests/sample_data/pug/test/cases/block-expansion.pug diff --git a/src/test-data/pug/test/cases/block-expansion.shorthands.html b/src/tests/sample_data/pug/test/cases/block-expansion.shorthands.html similarity index 100% rename from src/test-data/pug/test/cases/block-expansion.shorthands.html rename to src/tests/sample_data/pug/test/cases/block-expansion.shorthands.html diff --git a/src/test-data/pug/test/cases/block-expansion.shorthands.pug b/src/tests/sample_data/pug/test/cases/block-expansion.shorthands.pug similarity index 100% rename from src/test-data/pug/test/cases/block-expansion.shorthands.pug rename to src/tests/sample_data/pug/test/cases/block-expansion.shorthands.pug diff --git a/src/test-data/pug/test/cases/blockquote.html b/src/tests/sample_data/pug/test/cases/blockquote.html similarity index 100% rename from src/test-data/pug/test/cases/blockquote.html rename to src/tests/sample_data/pug/test/cases/blockquote.html diff --git a/src/test-data/pug/test/cases/blockquote.pug b/src/tests/sample_data/pug/test/cases/blockquote.pug similarity index 100% rename from src/test-data/pug/test/cases/blockquote.pug rename to src/tests/sample_data/pug/test/cases/blockquote.pug diff --git a/src/test-data/pug/test/cases/blocks-in-blocks.html b/src/tests/sample_data/pug/test/cases/blocks-in-blocks.html similarity index 100% rename from src/test-data/pug/test/cases/blocks-in-blocks.html rename to src/tests/sample_data/pug/test/cases/blocks-in-blocks.html diff --git a/src/test-data/pug/test/cases/blocks-in-blocks.pug b/src/tests/sample_data/pug/test/cases/blocks-in-blocks.pug similarity index 100% rename from src/test-data/pug/test/cases/blocks-in-blocks.pug rename to src/tests/sample_data/pug/test/cases/blocks-in-blocks.pug diff --git a/src/test-data/pug/test/cases/blocks-in-if.html b/src/tests/sample_data/pug/test/cases/blocks-in-if.html similarity index 100% rename from src/test-data/pug/test/cases/blocks-in-if.html rename to src/tests/sample_data/pug/test/cases/blocks-in-if.html diff --git a/src/test-data/pug/test/cases/blocks-in-if.pug b/src/tests/sample_data/pug/test/cases/blocks-in-if.pug similarity index 100% rename from src/test-data/pug/test/cases/blocks-in-if.pug rename to src/tests/sample_data/pug/test/cases/blocks-in-if.pug diff --git a/src/test-data/pug/test/cases/case-blocks.html b/src/tests/sample_data/pug/test/cases/case-blocks.html similarity index 100% rename from src/test-data/pug/test/cases/case-blocks.html rename to src/tests/sample_data/pug/test/cases/case-blocks.html diff --git a/src/test-data/pug/test/cases/case-blocks.pug b/src/tests/sample_data/pug/test/cases/case-blocks.pug similarity index 100% rename from src/test-data/pug/test/cases/case-blocks.pug rename to src/tests/sample_data/pug/test/cases/case-blocks.pug diff --git a/src/test-data/pug/test/cases/case.html b/src/tests/sample_data/pug/test/cases/case.html similarity index 100% rename from src/test-data/pug/test/cases/case.html rename to src/tests/sample_data/pug/test/cases/case.html diff --git a/src/test-data/pug/test/cases/case.pug b/src/tests/sample_data/pug/test/cases/case.pug similarity index 100% rename from src/test-data/pug/test/cases/case.pug rename to src/tests/sample_data/pug/test/cases/case.pug diff --git a/src/test-data/pug/test/cases/classes-empty.html b/src/tests/sample_data/pug/test/cases/classes-empty.html similarity index 100% rename from src/test-data/pug/test/cases/classes-empty.html rename to src/tests/sample_data/pug/test/cases/classes-empty.html diff --git a/src/test-data/pug/test/cases/classes-empty.pug b/src/tests/sample_data/pug/test/cases/classes-empty.pug similarity index 100% rename from src/test-data/pug/test/cases/classes-empty.pug rename to src/tests/sample_data/pug/test/cases/classes-empty.pug diff --git a/src/test-data/pug/test/cases/classes.html b/src/tests/sample_data/pug/test/cases/classes.html similarity index 100% rename from src/test-data/pug/test/cases/classes.html rename to src/tests/sample_data/pug/test/cases/classes.html diff --git a/src/test-data/pug/test/cases/classes.pug b/src/tests/sample_data/pug/test/cases/classes.pug similarity index 100% rename from src/test-data/pug/test/cases/classes.pug rename to src/tests/sample_data/pug/test/cases/classes.pug diff --git a/src/test-data/pug/test/cases/code.conditionals.html b/src/tests/sample_data/pug/test/cases/code.conditionals.html similarity index 100% rename from src/test-data/pug/test/cases/code.conditionals.html rename to src/tests/sample_data/pug/test/cases/code.conditionals.html diff --git a/src/test-data/pug/test/cases/code.conditionals.pug b/src/tests/sample_data/pug/test/cases/code.conditionals.pug similarity index 100% rename from src/test-data/pug/test/cases/code.conditionals.pug rename to src/tests/sample_data/pug/test/cases/code.conditionals.pug diff --git a/src/test-data/pug/test/cases/code.escape.html b/src/tests/sample_data/pug/test/cases/code.escape.html similarity index 100% rename from src/test-data/pug/test/cases/code.escape.html rename to src/tests/sample_data/pug/test/cases/code.escape.html diff --git a/src/test-data/pug/test/cases/code.escape.pug b/src/tests/sample_data/pug/test/cases/code.escape.pug similarity index 100% rename from src/test-data/pug/test/cases/code.escape.pug rename to src/tests/sample_data/pug/test/cases/code.escape.pug diff --git a/src/test-data/pug/test/cases/code.html b/src/tests/sample_data/pug/test/cases/code.html similarity index 100% rename from src/test-data/pug/test/cases/code.html rename to src/tests/sample_data/pug/test/cases/code.html diff --git a/src/test-data/pug/test/cases/code.iteration.html b/src/tests/sample_data/pug/test/cases/code.iteration.html similarity index 100% rename from src/test-data/pug/test/cases/code.iteration.html rename to src/tests/sample_data/pug/test/cases/code.iteration.html diff --git a/src/test-data/pug/test/cases/code.iteration.pug b/src/tests/sample_data/pug/test/cases/code.iteration.pug similarity index 100% rename from src/test-data/pug/test/cases/code.iteration.pug rename to src/tests/sample_data/pug/test/cases/code.iteration.pug diff --git a/src/test-data/pug/test/cases/code.pug b/src/tests/sample_data/pug/test/cases/code.pug similarity index 100% rename from src/test-data/pug/test/cases/code.pug rename to src/tests/sample_data/pug/test/cases/code.pug diff --git a/src/test-data/pug/test/cases/comments-in-case.html b/src/tests/sample_data/pug/test/cases/comments-in-case.html similarity index 100% rename from src/test-data/pug/test/cases/comments-in-case.html rename to src/tests/sample_data/pug/test/cases/comments-in-case.html diff --git a/src/test-data/pug/test/cases/comments-in-case.pug b/src/tests/sample_data/pug/test/cases/comments-in-case.pug similarity index 100% rename from src/test-data/pug/test/cases/comments-in-case.pug rename to src/tests/sample_data/pug/test/cases/comments-in-case.pug diff --git a/src/test-data/pug/test/cases/comments.html b/src/tests/sample_data/pug/test/cases/comments.html similarity index 100% rename from src/test-data/pug/test/cases/comments.html rename to src/tests/sample_data/pug/test/cases/comments.html diff --git a/src/test-data/pug/test/cases/comments.pug b/src/tests/sample_data/pug/test/cases/comments.pug similarity index 100% rename from src/test-data/pug/test/cases/comments.pug rename to src/tests/sample_data/pug/test/cases/comments.pug diff --git a/src/test-data/pug/test/cases/comments.source.html b/src/tests/sample_data/pug/test/cases/comments.source.html similarity index 100% rename from src/test-data/pug/test/cases/comments.source.html rename to src/tests/sample_data/pug/test/cases/comments.source.html diff --git a/src/test-data/pug/test/cases/comments.source.pug b/src/tests/sample_data/pug/test/cases/comments.source.pug similarity index 100% rename from src/test-data/pug/test/cases/comments.source.pug rename to src/tests/sample_data/pug/test/cases/comments.source.pug diff --git a/src/test-data/pug/test/cases/doctype.custom.html b/src/tests/sample_data/pug/test/cases/doctype.custom.html similarity index 100% rename from src/test-data/pug/test/cases/doctype.custom.html rename to src/tests/sample_data/pug/test/cases/doctype.custom.html diff --git a/src/test-data/pug/test/cases/doctype.custom.pug b/src/tests/sample_data/pug/test/cases/doctype.custom.pug similarity index 100% rename from src/test-data/pug/test/cases/doctype.custom.pug rename to src/tests/sample_data/pug/test/cases/doctype.custom.pug diff --git a/src/test-data/pug/test/cases/doctype.default.html b/src/tests/sample_data/pug/test/cases/doctype.default.html similarity index 100% rename from src/test-data/pug/test/cases/doctype.default.html rename to src/tests/sample_data/pug/test/cases/doctype.default.html diff --git a/src/test-data/pug/test/cases/doctype.default.pug b/src/tests/sample_data/pug/test/cases/doctype.default.pug similarity index 100% rename from src/test-data/pug/test/cases/doctype.default.pug rename to src/tests/sample_data/pug/test/cases/doctype.default.pug diff --git a/src/test-data/pug/test/cases/doctype.keyword.html b/src/tests/sample_data/pug/test/cases/doctype.keyword.html similarity index 100% rename from src/test-data/pug/test/cases/doctype.keyword.html rename to src/tests/sample_data/pug/test/cases/doctype.keyword.html diff --git a/src/test-data/pug/test/cases/doctype.keyword.pug b/src/tests/sample_data/pug/test/cases/doctype.keyword.pug similarity index 100% rename from src/test-data/pug/test/cases/doctype.keyword.pug rename to src/tests/sample_data/pug/test/cases/doctype.keyword.pug diff --git a/src/test-data/pug/test/cases/each.else.html b/src/tests/sample_data/pug/test/cases/each.else.html similarity index 100% rename from src/test-data/pug/test/cases/each.else.html rename to src/tests/sample_data/pug/test/cases/each.else.html diff --git a/src/test-data/pug/test/cases/each.else.pug b/src/tests/sample_data/pug/test/cases/each.else.pug similarity index 100% rename from src/test-data/pug/test/cases/each.else.pug rename to src/tests/sample_data/pug/test/cases/each.else.pug diff --git a/src/test-data/pug/test/cases/escape-chars.html b/src/tests/sample_data/pug/test/cases/escape-chars.html similarity index 100% rename from src/test-data/pug/test/cases/escape-chars.html rename to src/tests/sample_data/pug/test/cases/escape-chars.html diff --git a/src/test-data/pug/test/cases/escape-chars.pug b/src/tests/sample_data/pug/test/cases/escape-chars.pug similarity index 100% rename from src/test-data/pug/test/cases/escape-chars.pug rename to src/tests/sample_data/pug/test/cases/escape-chars.pug diff --git a/src/test-data/pug/test/cases/escape-test.html b/src/tests/sample_data/pug/test/cases/escape-test.html similarity index 100% rename from src/test-data/pug/test/cases/escape-test.html rename to src/tests/sample_data/pug/test/cases/escape-test.html diff --git a/src/test-data/pug/test/cases/escape-test.pug b/src/tests/sample_data/pug/test/cases/escape-test.pug similarity index 100% rename from src/test-data/pug/test/cases/escape-test.pug rename to src/tests/sample_data/pug/test/cases/escape-test.pug diff --git a/src/test-data/pug/test/cases/escaping-class-attribute.html b/src/tests/sample_data/pug/test/cases/escaping-class-attribute.html similarity index 100% rename from src/test-data/pug/test/cases/escaping-class-attribute.html rename to src/tests/sample_data/pug/test/cases/escaping-class-attribute.html diff --git a/src/test-data/pug/test/cases/escaping-class-attribute.pug b/src/tests/sample_data/pug/test/cases/escaping-class-attribute.pug similarity index 100% rename from src/test-data/pug/test/cases/escaping-class-attribute.pug rename to src/tests/sample_data/pug/test/cases/escaping-class-attribute.pug diff --git a/src/test-data/pug/test/cases/filter-in-include.html b/src/tests/sample_data/pug/test/cases/filter-in-include.html similarity index 100% rename from src/test-data/pug/test/cases/filter-in-include.html rename to src/tests/sample_data/pug/test/cases/filter-in-include.html diff --git a/src/test-data/pug/test/cases/filter-in-include.pug b/src/tests/sample_data/pug/test/cases/filter-in-include.pug similarity index 100% rename from src/test-data/pug/test/cases/filter-in-include.pug rename to src/tests/sample_data/pug/test/cases/filter-in-include.pug diff --git a/src/test-data/pug/test/cases/filters-empty.html b/src/tests/sample_data/pug/test/cases/filters-empty.html similarity index 100% rename from src/test-data/pug/test/cases/filters-empty.html rename to src/tests/sample_data/pug/test/cases/filters-empty.html diff --git a/src/test-data/pug/test/cases/filters-empty.pug b/src/tests/sample_data/pug/test/cases/filters-empty.pug similarity index 100% rename from src/test-data/pug/test/cases/filters-empty.pug rename to src/tests/sample_data/pug/test/cases/filters-empty.pug diff --git a/src/test-data/pug/test/cases/filters.coffeescript.html b/src/tests/sample_data/pug/test/cases/filters.coffeescript.html similarity index 100% rename from src/test-data/pug/test/cases/filters.coffeescript.html rename to src/tests/sample_data/pug/test/cases/filters.coffeescript.html diff --git a/src/test-data/pug/test/cases/filters.coffeescript.pug b/src/tests/sample_data/pug/test/cases/filters.coffeescript.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.coffeescript.pug rename to src/tests/sample_data/pug/test/cases/filters.coffeescript.pug diff --git a/src/test-data/pug/test/cases/filters.custom.html b/src/tests/sample_data/pug/test/cases/filters.custom.html similarity index 100% rename from src/test-data/pug/test/cases/filters.custom.html rename to src/tests/sample_data/pug/test/cases/filters.custom.html diff --git a/src/test-data/pug/test/cases/filters.custom.pug b/src/tests/sample_data/pug/test/cases/filters.custom.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.custom.pug rename to src/tests/sample_data/pug/test/cases/filters.custom.pug diff --git a/src/test-data/pug/test/cases/filters.include.custom.html b/src/tests/sample_data/pug/test/cases/filters.include.custom.html similarity index 100% rename from src/test-data/pug/test/cases/filters.include.custom.html rename to src/tests/sample_data/pug/test/cases/filters.include.custom.html diff --git a/src/test-data/pug/test/cases/filters.include.custom.pug b/src/tests/sample_data/pug/test/cases/filters.include.custom.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.include.custom.pug rename to src/tests/sample_data/pug/test/cases/filters.include.custom.pug diff --git a/src/test-data/pug/test/cases/filters.include.html b/src/tests/sample_data/pug/test/cases/filters.include.html similarity index 100% rename from src/test-data/pug/test/cases/filters.include.html rename to src/tests/sample_data/pug/test/cases/filters.include.html diff --git a/src/test-data/pug/test/cases/filters.include.pug b/src/tests/sample_data/pug/test/cases/filters.include.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.include.pug rename to src/tests/sample_data/pug/test/cases/filters.include.pug diff --git a/src/test-data/pug/test/cases/filters.inline.html b/src/tests/sample_data/pug/test/cases/filters.inline.html similarity index 100% rename from src/test-data/pug/test/cases/filters.inline.html rename to src/tests/sample_data/pug/test/cases/filters.inline.html diff --git a/src/test-data/pug/test/cases/filters.inline.pug b/src/tests/sample_data/pug/test/cases/filters.inline.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.inline.pug rename to src/tests/sample_data/pug/test/cases/filters.inline.pug diff --git a/src/test-data/pug/test/cases/filters.less.html b/src/tests/sample_data/pug/test/cases/filters.less.html similarity index 100% rename from src/test-data/pug/test/cases/filters.less.html rename to src/tests/sample_data/pug/test/cases/filters.less.html diff --git a/src/test-data/pug/test/cases/filters.less.pug b/src/tests/sample_data/pug/test/cases/filters.less.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.less.pug rename to src/tests/sample_data/pug/test/cases/filters.less.pug diff --git a/src/test-data/pug/test/cases/filters.markdown.html b/src/tests/sample_data/pug/test/cases/filters.markdown.html similarity index 100% rename from src/test-data/pug/test/cases/filters.markdown.html rename to src/tests/sample_data/pug/test/cases/filters.markdown.html diff --git a/src/test-data/pug/test/cases/filters.markdown.pug b/src/tests/sample_data/pug/test/cases/filters.markdown.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.markdown.pug rename to src/tests/sample_data/pug/test/cases/filters.markdown.pug diff --git a/src/test-data/pug/test/cases/filters.nested.html b/src/tests/sample_data/pug/test/cases/filters.nested.html similarity index 100% rename from src/test-data/pug/test/cases/filters.nested.html rename to src/tests/sample_data/pug/test/cases/filters.nested.html diff --git a/src/test-data/pug/test/cases/filters.nested.pug b/src/tests/sample_data/pug/test/cases/filters.nested.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.nested.pug rename to src/tests/sample_data/pug/test/cases/filters.nested.pug diff --git a/src/test-data/pug/test/cases/filters.stylus.html b/src/tests/sample_data/pug/test/cases/filters.stylus.html similarity index 100% rename from src/test-data/pug/test/cases/filters.stylus.html rename to src/tests/sample_data/pug/test/cases/filters.stylus.html diff --git a/src/test-data/pug/test/cases/filters.stylus.pug b/src/tests/sample_data/pug/test/cases/filters.stylus.pug similarity index 100% rename from src/test-data/pug/test/cases/filters.stylus.pug rename to src/tests/sample_data/pug/test/cases/filters.stylus.pug diff --git a/src/test-data/pug/test/cases/html.html b/src/tests/sample_data/pug/test/cases/html.html similarity index 100% rename from src/test-data/pug/test/cases/html.html rename to src/tests/sample_data/pug/test/cases/html.html diff --git a/src/test-data/pug/test/cases/html.pug b/src/tests/sample_data/pug/test/cases/html.pug similarity index 100% rename from src/test-data/pug/test/cases/html.pug rename to src/tests/sample_data/pug/test/cases/html.pug diff --git a/src/test-data/pug/test/cases/html5.html b/src/tests/sample_data/pug/test/cases/html5.html similarity index 100% rename from src/test-data/pug/test/cases/html5.html rename to src/tests/sample_data/pug/test/cases/html5.html diff --git a/src/test-data/pug/test/cases/html5.pug b/src/tests/sample_data/pug/test/cases/html5.pug similarity index 100% rename from src/test-data/pug/test/cases/html5.pug rename to src/tests/sample_data/pug/test/cases/html5.pug diff --git a/src/test-data/pug/test/cases/include-extends-from-root.html b/src/tests/sample_data/pug/test/cases/include-extends-from-root.html similarity index 100% rename from src/test-data/pug/test/cases/include-extends-from-root.html rename to src/tests/sample_data/pug/test/cases/include-extends-from-root.html diff --git a/src/test-data/pug/test/cases/include-extends-from-root.pug b/src/tests/sample_data/pug/test/cases/include-extends-from-root.pug similarity index 100% rename from src/test-data/pug/test/cases/include-extends-from-root.pug rename to src/tests/sample_data/pug/test/cases/include-extends-from-root.pug diff --git a/src/test-data/pug/test/cases/include-extends-of-common-template.html b/src/tests/sample_data/pug/test/cases/include-extends-of-common-template.html similarity index 100% rename from src/test-data/pug/test/cases/include-extends-of-common-template.html rename to src/tests/sample_data/pug/test/cases/include-extends-of-common-template.html diff --git a/src/test-data/pug/test/cases/include-extends-of-common-template.pug b/src/tests/sample_data/pug/test/cases/include-extends-of-common-template.pug similarity index 100% rename from src/test-data/pug/test/cases/include-extends-of-common-template.pug rename to src/tests/sample_data/pug/test/cases/include-extends-of-common-template.pug diff --git a/src/test-data/pug/test/cases/include-extends-relative.html b/src/tests/sample_data/pug/test/cases/include-extends-relative.html similarity index 100% rename from src/test-data/pug/test/cases/include-extends-relative.html rename to src/tests/sample_data/pug/test/cases/include-extends-relative.html diff --git a/src/test-data/pug/test/cases/include-extends-relative.pug b/src/tests/sample_data/pug/test/cases/include-extends-relative.pug similarity index 100% rename from src/test-data/pug/test/cases/include-extends-relative.pug rename to src/tests/sample_data/pug/test/cases/include-extends-relative.pug diff --git a/src/test-data/pug/test/cases/include-filter-coffee.coffee b/src/tests/sample_data/pug/test/cases/include-filter-coffee.coffee similarity index 100% rename from src/test-data/pug/test/cases/include-filter-coffee.coffee rename to src/tests/sample_data/pug/test/cases/include-filter-coffee.coffee diff --git a/src/test-data/pug/test/cases/include-only-text-body.html b/src/tests/sample_data/pug/test/cases/include-only-text-body.html similarity index 100% rename from src/test-data/pug/test/cases/include-only-text-body.html rename to src/tests/sample_data/pug/test/cases/include-only-text-body.html diff --git a/src/test-data/pug/test/cases/include-only-text-body.pug b/src/tests/sample_data/pug/test/cases/include-only-text-body.pug similarity index 100% rename from src/test-data/pug/test/cases/include-only-text-body.pug rename to src/tests/sample_data/pug/test/cases/include-only-text-body.pug diff --git a/src/test-data/pug/test/cases/include-only-text.html b/src/tests/sample_data/pug/test/cases/include-only-text.html similarity index 100% rename from src/test-data/pug/test/cases/include-only-text.html rename to src/tests/sample_data/pug/test/cases/include-only-text.html diff --git a/src/test-data/pug/test/cases/include-only-text.pug b/src/tests/sample_data/pug/test/cases/include-only-text.pug similarity index 100% rename from src/test-data/pug/test/cases/include-only-text.pug rename to src/tests/sample_data/pug/test/cases/include-only-text.pug diff --git a/src/test-data/pug/test/cases/include-with-text-head.html b/src/tests/sample_data/pug/test/cases/include-with-text-head.html similarity index 100% rename from src/test-data/pug/test/cases/include-with-text-head.html rename to src/tests/sample_data/pug/test/cases/include-with-text-head.html diff --git a/src/test-data/pug/test/cases/include-with-text-head.pug b/src/tests/sample_data/pug/test/cases/include-with-text-head.pug similarity index 100% rename from src/test-data/pug/test/cases/include-with-text-head.pug rename to src/tests/sample_data/pug/test/cases/include-with-text-head.pug diff --git a/src/test-data/pug/test/cases/include-with-text.html b/src/tests/sample_data/pug/test/cases/include-with-text.html similarity index 100% rename from src/test-data/pug/test/cases/include-with-text.html rename to src/tests/sample_data/pug/test/cases/include-with-text.html diff --git a/src/test-data/pug/test/cases/include-with-text.pug b/src/tests/sample_data/pug/test/cases/include-with-text.pug similarity index 100% rename from src/test-data/pug/test/cases/include-with-text.pug rename to src/tests/sample_data/pug/test/cases/include-with-text.pug diff --git a/src/test-data/pug/test/cases/include.script.html b/src/tests/sample_data/pug/test/cases/include.script.html similarity index 100% rename from src/test-data/pug/test/cases/include.script.html rename to src/tests/sample_data/pug/test/cases/include.script.html diff --git a/src/test-data/pug/test/cases/include.script.pug b/src/tests/sample_data/pug/test/cases/include.script.pug similarity index 100% rename from src/test-data/pug/test/cases/include.script.pug rename to src/tests/sample_data/pug/test/cases/include.script.pug diff --git a/src/test-data/pug/test/cases/include.yield.nested.html b/src/tests/sample_data/pug/test/cases/include.yield.nested.html similarity index 100% rename from src/test-data/pug/test/cases/include.yield.nested.html rename to src/tests/sample_data/pug/test/cases/include.yield.nested.html diff --git a/src/test-data/pug/test/cases/include.yield.nested.pug b/src/tests/sample_data/pug/test/cases/include.yield.nested.pug similarity index 100% rename from src/test-data/pug/test/cases/include.yield.nested.pug rename to src/tests/sample_data/pug/test/cases/include.yield.nested.pug diff --git a/src/test-data/pug/test/cases/includes-with-ext-js.html b/src/tests/sample_data/pug/test/cases/includes-with-ext-js.html similarity index 100% rename from src/test-data/pug/test/cases/includes-with-ext-js.html rename to src/tests/sample_data/pug/test/cases/includes-with-ext-js.html diff --git a/src/test-data/pug/test/cases/includes-with-ext-js.pug b/src/tests/sample_data/pug/test/cases/includes-with-ext-js.pug similarity index 100% rename from src/test-data/pug/test/cases/includes-with-ext-js.pug rename to src/tests/sample_data/pug/test/cases/includes-with-ext-js.pug diff --git a/src/test-data/pug/test/cases/includes.html b/src/tests/sample_data/pug/test/cases/includes.html similarity index 100% rename from src/test-data/pug/test/cases/includes.html rename to src/tests/sample_data/pug/test/cases/includes.html diff --git a/src/test-data/pug/test/cases/includes.pug b/src/tests/sample_data/pug/test/cases/includes.pug similarity index 100% rename from src/test-data/pug/test/cases/includes.pug rename to src/tests/sample_data/pug/test/cases/includes.pug diff --git a/src/test-data/pug/test/cases/inheritance.alert-dialog.html b/src/tests/sample_data/pug/test/cases/inheritance.alert-dialog.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.alert-dialog.html rename to src/tests/sample_data/pug/test/cases/inheritance.alert-dialog.html diff --git a/src/test-data/pug/test/cases/inheritance.alert-dialog.pug b/src/tests/sample_data/pug/test/cases/inheritance.alert-dialog.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.alert-dialog.pug rename to src/tests/sample_data/pug/test/cases/inheritance.alert-dialog.pug diff --git a/src/test-data/pug/test/cases/inheritance.defaults.html b/src/tests/sample_data/pug/test/cases/inheritance.defaults.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.defaults.html rename to src/tests/sample_data/pug/test/cases/inheritance.defaults.html diff --git a/src/test-data/pug/test/cases/inheritance.defaults.pug b/src/tests/sample_data/pug/test/cases/inheritance.defaults.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.defaults.pug rename to src/tests/sample_data/pug/test/cases/inheritance.defaults.pug diff --git a/src/test-data/pug/test/cases/inheritance.extend.html b/src/tests/sample_data/pug/test/cases/inheritance.extend.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.html rename to src/tests/sample_data/pug/test/cases/inheritance.extend.html diff --git a/src/test-data/pug/test/cases/inheritance.extend.include.html b/src/tests/sample_data/pug/test/cases/inheritance.extend.include.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.include.html rename to src/tests/sample_data/pug/test/cases/inheritance.extend.include.html diff --git a/src/test-data/pug/test/cases/inheritance.extend.include.pug b/src/tests/sample_data/pug/test/cases/inheritance.extend.include.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.include.pug rename to src/tests/sample_data/pug/test/cases/inheritance.extend.include.pug diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.block.html b/src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.block.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.mixins.block.html rename to src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.block.html diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.block.pug b/src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.block.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.mixins.block.pug rename to src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.block.pug diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.html b/src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.mixins.html rename to src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.html diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.pug b/src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.mixins.pug rename to src/tests/sample_data/pug/test/cases/inheritance.extend.mixins.pug diff --git a/src/test-data/pug/test/cases/inheritance.extend.pug b/src/tests/sample_data/pug/test/cases/inheritance.extend.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.pug rename to src/tests/sample_data/pug/test/cases/inheritance.extend.pug diff --git a/src/test-data/pug/test/cases/inheritance.extend.recursive.html b/src/tests/sample_data/pug/test/cases/inheritance.extend.recursive.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.recursive.html rename to src/tests/sample_data/pug/test/cases/inheritance.extend.recursive.html diff --git a/src/test-data/pug/test/cases/inheritance.extend.recursive.pug b/src/tests/sample_data/pug/test/cases/inheritance.extend.recursive.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.recursive.pug rename to src/tests/sample_data/pug/test/cases/inheritance.extend.recursive.pug diff --git a/src/test-data/pug/test/cases/inheritance.extend.whitespace.html b/src/tests/sample_data/pug/test/cases/inheritance.extend.whitespace.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.whitespace.html rename to src/tests/sample_data/pug/test/cases/inheritance.extend.whitespace.html diff --git a/src/test-data/pug/test/cases/inheritance.extend.whitespace.pug b/src/tests/sample_data/pug/test/cases/inheritance.extend.whitespace.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.extend.whitespace.pug rename to src/tests/sample_data/pug/test/cases/inheritance.extend.whitespace.pug diff --git a/src/test-data/pug/test/cases/inheritance.html b/src/tests/sample_data/pug/test/cases/inheritance.html similarity index 100% rename from src/test-data/pug/test/cases/inheritance.html rename to src/tests/sample_data/pug/test/cases/inheritance.html diff --git a/src/test-data/pug/test/cases/inheritance.pug b/src/tests/sample_data/pug/test/cases/inheritance.pug similarity index 100% rename from src/test-data/pug/test/cases/inheritance.pug rename to src/tests/sample_data/pug/test/cases/inheritance.pug diff --git a/src/test-data/pug/test/cases/inline-tag.html b/src/tests/sample_data/pug/test/cases/inline-tag.html similarity index 100% rename from src/test-data/pug/test/cases/inline-tag.html rename to src/tests/sample_data/pug/test/cases/inline-tag.html diff --git a/src/test-data/pug/test/cases/inline-tag.pug b/src/tests/sample_data/pug/test/cases/inline-tag.pug similarity index 100% rename from src/test-data/pug/test/cases/inline-tag.pug rename to src/tests/sample_data/pug/test/cases/inline-tag.pug diff --git a/src/test-data/pug/test/cases/intepolated-elements.html b/src/tests/sample_data/pug/test/cases/intepolated-elements.html similarity index 100% rename from src/test-data/pug/test/cases/intepolated-elements.html rename to src/tests/sample_data/pug/test/cases/intepolated-elements.html diff --git a/src/test-data/pug/test/cases/intepolated-elements.pug b/src/tests/sample_data/pug/test/cases/intepolated-elements.pug similarity index 100% rename from src/test-data/pug/test/cases/intepolated-elements.pug rename to src/tests/sample_data/pug/test/cases/intepolated-elements.pug diff --git a/src/test-data/pug/test/cases/interpolated-mixin.html b/src/tests/sample_data/pug/test/cases/interpolated-mixin.html similarity index 100% rename from src/test-data/pug/test/cases/interpolated-mixin.html rename to src/tests/sample_data/pug/test/cases/interpolated-mixin.html diff --git a/src/test-data/pug/test/cases/interpolated-mixin.pug b/src/tests/sample_data/pug/test/cases/interpolated-mixin.pug similarity index 100% rename from src/test-data/pug/test/cases/interpolated-mixin.pug rename to src/tests/sample_data/pug/test/cases/interpolated-mixin.pug diff --git a/src/test-data/pug/test/cases/interpolation.escape.html b/src/tests/sample_data/pug/test/cases/interpolation.escape.html similarity index 100% rename from src/test-data/pug/test/cases/interpolation.escape.html rename to src/tests/sample_data/pug/test/cases/interpolation.escape.html diff --git a/src/test-data/pug/test/cases/interpolation.escape.pug b/src/tests/sample_data/pug/test/cases/interpolation.escape.pug similarity index 100% rename from src/test-data/pug/test/cases/interpolation.escape.pug rename to src/tests/sample_data/pug/test/cases/interpolation.escape.pug diff --git a/src/test-data/pug/test/cases/javascript-new-lines.js b/src/tests/sample_data/pug/test/cases/javascript-new-lines.js similarity index 100% rename from src/test-data/pug/test/cases/javascript-new-lines.js rename to src/tests/sample_data/pug/test/cases/javascript-new-lines.js diff --git a/src/test-data/pug/test/cases/layout.append.html b/src/tests/sample_data/pug/test/cases/layout.append.html similarity index 100% rename from src/test-data/pug/test/cases/layout.append.html rename to src/tests/sample_data/pug/test/cases/layout.append.html diff --git a/src/test-data/pug/test/cases/layout.append.pug b/src/tests/sample_data/pug/test/cases/layout.append.pug similarity index 100% rename from src/test-data/pug/test/cases/layout.append.pug rename to src/tests/sample_data/pug/test/cases/layout.append.pug diff --git a/src/test-data/pug/test/cases/layout.append.without-block.html b/src/tests/sample_data/pug/test/cases/layout.append.without-block.html similarity index 100% rename from src/test-data/pug/test/cases/layout.append.without-block.html rename to src/tests/sample_data/pug/test/cases/layout.append.without-block.html diff --git a/src/test-data/pug/test/cases/layout.append.without-block.pug b/src/tests/sample_data/pug/test/cases/layout.append.without-block.pug similarity index 100% rename from src/test-data/pug/test/cases/layout.append.without-block.pug rename to src/tests/sample_data/pug/test/cases/layout.append.without-block.pug diff --git a/src/test-data/pug/test/cases/layout.multi.append.prepend.block.html b/src/tests/sample_data/pug/test/cases/layout.multi.append.prepend.block.html similarity index 100% rename from src/test-data/pug/test/cases/layout.multi.append.prepend.block.html rename to src/tests/sample_data/pug/test/cases/layout.multi.append.prepend.block.html diff --git a/src/test-data/pug/test/cases/layout.multi.append.prepend.block.pug b/src/tests/sample_data/pug/test/cases/layout.multi.append.prepend.block.pug similarity index 100% rename from src/test-data/pug/test/cases/layout.multi.append.prepend.block.pug rename to src/tests/sample_data/pug/test/cases/layout.multi.append.prepend.block.pug diff --git a/src/test-data/pug/test/cases/layout.prepend.html b/src/tests/sample_data/pug/test/cases/layout.prepend.html similarity index 100% rename from src/test-data/pug/test/cases/layout.prepend.html rename to src/tests/sample_data/pug/test/cases/layout.prepend.html diff --git a/src/test-data/pug/test/cases/layout.prepend.pug b/src/tests/sample_data/pug/test/cases/layout.prepend.pug similarity index 100% rename from src/test-data/pug/test/cases/layout.prepend.pug rename to src/tests/sample_data/pug/test/cases/layout.prepend.pug diff --git a/src/test-data/pug/test/cases/layout.prepend.without-block.html b/src/tests/sample_data/pug/test/cases/layout.prepend.without-block.html similarity index 100% rename from src/test-data/pug/test/cases/layout.prepend.without-block.html rename to src/tests/sample_data/pug/test/cases/layout.prepend.without-block.html diff --git a/src/test-data/pug/test/cases/layout.prepend.without-block.pug b/src/tests/sample_data/pug/test/cases/layout.prepend.without-block.pug similarity index 100% rename from src/test-data/pug/test/cases/layout.prepend.without-block.pug rename to src/tests/sample_data/pug/test/cases/layout.prepend.without-block.pug diff --git a/src/test-data/pug/test/cases/mixin-at-end-of-file.html b/src/tests/sample_data/pug/test/cases/mixin-at-end-of-file.html similarity index 100% rename from src/test-data/pug/test/cases/mixin-at-end-of-file.html rename to src/tests/sample_data/pug/test/cases/mixin-at-end-of-file.html diff --git a/src/test-data/pug/test/cases/mixin-at-end-of-file.pug b/src/tests/sample_data/pug/test/cases/mixin-at-end-of-file.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin-at-end-of-file.pug rename to src/tests/sample_data/pug/test/cases/mixin-at-end-of-file.pug diff --git a/src/test-data/pug/test/cases/mixin-block-with-space.html b/src/tests/sample_data/pug/test/cases/mixin-block-with-space.html similarity index 100% rename from src/test-data/pug/test/cases/mixin-block-with-space.html rename to src/tests/sample_data/pug/test/cases/mixin-block-with-space.html diff --git a/src/test-data/pug/test/cases/mixin-block-with-space.pug b/src/tests/sample_data/pug/test/cases/mixin-block-with-space.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin-block-with-space.pug rename to src/tests/sample_data/pug/test/cases/mixin-block-with-space.pug diff --git a/src/test-data/pug/test/cases/mixin-hoist.html b/src/tests/sample_data/pug/test/cases/mixin-hoist.html similarity index 100% rename from src/test-data/pug/test/cases/mixin-hoist.html rename to src/tests/sample_data/pug/test/cases/mixin-hoist.html diff --git a/src/test-data/pug/test/cases/mixin-hoist.pug b/src/tests/sample_data/pug/test/cases/mixin-hoist.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin-hoist.pug rename to src/tests/sample_data/pug/test/cases/mixin-hoist.pug diff --git a/src/test-data/pug/test/cases/mixin-via-include.html b/src/tests/sample_data/pug/test/cases/mixin-via-include.html similarity index 100% rename from src/test-data/pug/test/cases/mixin-via-include.html rename to src/tests/sample_data/pug/test/cases/mixin-via-include.html diff --git a/src/test-data/pug/test/cases/mixin-via-include.pug b/src/tests/sample_data/pug/test/cases/mixin-via-include.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin-via-include.pug rename to src/tests/sample_data/pug/test/cases/mixin-via-include.pug diff --git a/src/test-data/pug/test/cases/mixin.attrs.html b/src/tests/sample_data/pug/test/cases/mixin.attrs.html similarity index 100% rename from src/test-data/pug/test/cases/mixin.attrs.html rename to src/tests/sample_data/pug/test/cases/mixin.attrs.html diff --git a/src/test-data/pug/test/cases/mixin.attrs.pug b/src/tests/sample_data/pug/test/cases/mixin.attrs.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin.attrs.pug rename to src/tests/sample_data/pug/test/cases/mixin.attrs.pug diff --git a/src/test-data/pug/test/cases/mixin.block-tag-behaviour.html b/src/tests/sample_data/pug/test/cases/mixin.block-tag-behaviour.html similarity index 100% rename from src/test-data/pug/test/cases/mixin.block-tag-behaviour.html rename to src/tests/sample_data/pug/test/cases/mixin.block-tag-behaviour.html diff --git a/src/test-data/pug/test/cases/mixin.block-tag-behaviour.pug b/src/tests/sample_data/pug/test/cases/mixin.block-tag-behaviour.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin.block-tag-behaviour.pug rename to src/tests/sample_data/pug/test/cases/mixin.block-tag-behaviour.pug diff --git a/src/test-data/pug/test/cases/mixin.blocks.html b/src/tests/sample_data/pug/test/cases/mixin.blocks.html similarity index 100% rename from src/test-data/pug/test/cases/mixin.blocks.html rename to src/tests/sample_data/pug/test/cases/mixin.blocks.html diff --git a/src/test-data/pug/test/cases/mixin.blocks.pug b/src/tests/sample_data/pug/test/cases/mixin.blocks.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin.blocks.pug rename to src/tests/sample_data/pug/test/cases/mixin.blocks.pug diff --git a/src/test-data/pug/test/cases/mixin.merge.html b/src/tests/sample_data/pug/test/cases/mixin.merge.html similarity index 100% rename from src/test-data/pug/test/cases/mixin.merge.html rename to src/tests/sample_data/pug/test/cases/mixin.merge.html diff --git a/src/test-data/pug/test/cases/mixin.merge.pug b/src/tests/sample_data/pug/test/cases/mixin.merge.pug similarity index 100% rename from src/test-data/pug/test/cases/mixin.merge.pug rename to src/tests/sample_data/pug/test/cases/mixin.merge.pug diff --git a/src/test-data/pug/test/cases/mixins-unused.html b/src/tests/sample_data/pug/test/cases/mixins-unused.html similarity index 100% rename from src/test-data/pug/test/cases/mixins-unused.html rename to src/tests/sample_data/pug/test/cases/mixins-unused.html diff --git a/src/test-data/pug/test/cases/mixins-unused.pug b/src/tests/sample_data/pug/test/cases/mixins-unused.pug similarity index 100% rename from src/test-data/pug/test/cases/mixins-unused.pug rename to src/tests/sample_data/pug/test/cases/mixins-unused.pug diff --git a/src/test-data/pug/test/cases/mixins.html b/src/tests/sample_data/pug/test/cases/mixins.html similarity index 100% rename from src/test-data/pug/test/cases/mixins.html rename to src/tests/sample_data/pug/test/cases/mixins.html diff --git a/src/test-data/pug/test/cases/mixins.pug b/src/tests/sample_data/pug/test/cases/mixins.pug similarity index 100% rename from src/test-data/pug/test/cases/mixins.pug rename to src/tests/sample_data/pug/test/cases/mixins.pug diff --git a/src/test-data/pug/test/cases/mixins.rest-args.html b/src/tests/sample_data/pug/test/cases/mixins.rest-args.html similarity index 100% rename from src/test-data/pug/test/cases/mixins.rest-args.html rename to src/tests/sample_data/pug/test/cases/mixins.rest-args.html diff --git a/src/test-data/pug/test/cases/mixins.rest-args.pug b/src/tests/sample_data/pug/test/cases/mixins.rest-args.pug similarity index 100% rename from src/test-data/pug/test/cases/mixins.rest-args.pug rename to src/tests/sample_data/pug/test/cases/mixins.rest-args.pug diff --git a/src/test-data/pug/test/cases/namespaces.html b/src/tests/sample_data/pug/test/cases/namespaces.html similarity index 100% rename from src/test-data/pug/test/cases/namespaces.html rename to src/tests/sample_data/pug/test/cases/namespaces.html diff --git a/src/test-data/pug/test/cases/namespaces.pug b/src/tests/sample_data/pug/test/cases/namespaces.pug similarity index 100% rename from src/test-data/pug/test/cases/namespaces.pug rename to src/tests/sample_data/pug/test/cases/namespaces.pug diff --git a/src/test-data/pug/test/cases/nesting.html b/src/tests/sample_data/pug/test/cases/nesting.html similarity index 100% rename from src/test-data/pug/test/cases/nesting.html rename to src/tests/sample_data/pug/test/cases/nesting.html diff --git a/src/test-data/pug/test/cases/nesting.pug b/src/tests/sample_data/pug/test/cases/nesting.pug similarity index 100% rename from src/test-data/pug/test/cases/nesting.pug rename to src/tests/sample_data/pug/test/cases/nesting.pug diff --git a/src/test-data/pug/test/cases/pipeless-comments.html b/src/tests/sample_data/pug/test/cases/pipeless-comments.html similarity index 100% rename from src/test-data/pug/test/cases/pipeless-comments.html rename to src/tests/sample_data/pug/test/cases/pipeless-comments.html diff --git a/src/test-data/pug/test/cases/pipeless-comments.pug b/src/tests/sample_data/pug/test/cases/pipeless-comments.pug similarity index 100% rename from src/test-data/pug/test/cases/pipeless-comments.pug rename to src/tests/sample_data/pug/test/cases/pipeless-comments.pug diff --git a/src/test-data/pug/test/cases/pipeless-filters.html b/src/tests/sample_data/pug/test/cases/pipeless-filters.html similarity index 100% rename from src/test-data/pug/test/cases/pipeless-filters.html rename to src/tests/sample_data/pug/test/cases/pipeless-filters.html diff --git a/src/test-data/pug/test/cases/pipeless-filters.pug b/src/tests/sample_data/pug/test/cases/pipeless-filters.pug similarity index 100% rename from src/test-data/pug/test/cases/pipeless-filters.pug rename to src/tests/sample_data/pug/test/cases/pipeless-filters.pug diff --git a/src/test-data/pug/test/cases/pipeless-tag.html b/src/tests/sample_data/pug/test/cases/pipeless-tag.html similarity index 100% rename from src/test-data/pug/test/cases/pipeless-tag.html rename to src/tests/sample_data/pug/test/cases/pipeless-tag.html diff --git a/src/test-data/pug/test/cases/pipeless-tag.pug b/src/tests/sample_data/pug/test/cases/pipeless-tag.pug similarity index 100% rename from src/test-data/pug/test/cases/pipeless-tag.pug rename to src/tests/sample_data/pug/test/cases/pipeless-tag.pug diff --git a/src/test-data/pug/test/cases/pre.html b/src/tests/sample_data/pug/test/cases/pre.html similarity index 100% rename from src/test-data/pug/test/cases/pre.html rename to src/tests/sample_data/pug/test/cases/pre.html diff --git a/src/test-data/pug/test/cases/pre.pug b/src/tests/sample_data/pug/test/cases/pre.pug similarity index 100% rename from src/test-data/pug/test/cases/pre.pug rename to src/tests/sample_data/pug/test/cases/pre.pug diff --git a/src/test-data/pug/test/cases/quotes.html b/src/tests/sample_data/pug/test/cases/quotes.html similarity index 100% rename from src/test-data/pug/test/cases/quotes.html rename to src/tests/sample_data/pug/test/cases/quotes.html diff --git a/src/test-data/pug/test/cases/quotes.pug b/src/tests/sample_data/pug/test/cases/quotes.pug similarity index 100% rename from src/test-data/pug/test/cases/quotes.pug rename to src/tests/sample_data/pug/test/cases/quotes.pug diff --git a/src/test-data/pug/test/cases/regression.1794.html b/src/tests/sample_data/pug/test/cases/regression.1794.html similarity index 100% rename from src/test-data/pug/test/cases/regression.1794.html rename to src/tests/sample_data/pug/test/cases/regression.1794.html diff --git a/src/test-data/pug/test/cases/regression.1794.pug b/src/tests/sample_data/pug/test/cases/regression.1794.pug similarity index 100% rename from src/test-data/pug/test/cases/regression.1794.pug rename to src/tests/sample_data/pug/test/cases/regression.1794.pug diff --git a/src/test-data/pug/test/cases/regression.784.html b/src/tests/sample_data/pug/test/cases/regression.784.html similarity index 100% rename from src/test-data/pug/test/cases/regression.784.html rename to src/tests/sample_data/pug/test/cases/regression.784.html diff --git a/src/test-data/pug/test/cases/regression.784.pug b/src/tests/sample_data/pug/test/cases/regression.784.pug similarity index 100% rename from src/test-data/pug/test/cases/regression.784.pug rename to src/tests/sample_data/pug/test/cases/regression.784.pug diff --git a/src/test-data/pug/test/cases/script.whitespace.html b/src/tests/sample_data/pug/test/cases/script.whitespace.html similarity index 100% rename from src/test-data/pug/test/cases/script.whitespace.html rename to src/tests/sample_data/pug/test/cases/script.whitespace.html diff --git a/src/test-data/pug/test/cases/script.whitespace.pug b/src/tests/sample_data/pug/test/cases/script.whitespace.pug similarity index 100% rename from src/test-data/pug/test/cases/script.whitespace.pug rename to src/tests/sample_data/pug/test/cases/script.whitespace.pug diff --git a/src/test-data/pug/test/cases/scripts.html b/src/tests/sample_data/pug/test/cases/scripts.html similarity index 100% rename from src/test-data/pug/test/cases/scripts.html rename to src/tests/sample_data/pug/test/cases/scripts.html diff --git a/src/test-data/pug/test/cases/scripts.non-js.html b/src/tests/sample_data/pug/test/cases/scripts.non-js.html similarity index 100% rename from src/test-data/pug/test/cases/scripts.non-js.html rename to src/tests/sample_data/pug/test/cases/scripts.non-js.html diff --git a/src/test-data/pug/test/cases/scripts.non-js.pug b/src/tests/sample_data/pug/test/cases/scripts.non-js.pug similarity index 100% rename from src/test-data/pug/test/cases/scripts.non-js.pug rename to src/tests/sample_data/pug/test/cases/scripts.non-js.pug diff --git a/src/test-data/pug/test/cases/scripts.pug b/src/tests/sample_data/pug/test/cases/scripts.pug similarity index 100% rename from src/test-data/pug/test/cases/scripts.pug rename to src/tests/sample_data/pug/test/cases/scripts.pug diff --git a/src/test-data/pug/test/cases/self-closing-html.html b/src/tests/sample_data/pug/test/cases/self-closing-html.html similarity index 100% rename from src/test-data/pug/test/cases/self-closing-html.html rename to src/tests/sample_data/pug/test/cases/self-closing-html.html diff --git a/src/test-data/pug/test/cases/self-closing-html.pug b/src/tests/sample_data/pug/test/cases/self-closing-html.pug similarity index 100% rename from src/test-data/pug/test/cases/self-closing-html.pug rename to src/tests/sample_data/pug/test/cases/self-closing-html.pug diff --git a/src/test-data/pug/test/cases/single-period.html b/src/tests/sample_data/pug/test/cases/single-period.html similarity index 100% rename from src/test-data/pug/test/cases/single-period.html rename to src/tests/sample_data/pug/test/cases/single-period.html diff --git a/src/test-data/pug/test/cases/single-period.pug b/src/tests/sample_data/pug/test/cases/single-period.pug similarity index 100% rename from src/test-data/pug/test/cases/single-period.pug rename to src/tests/sample_data/pug/test/cases/single-period.pug diff --git a/src/test-data/pug/test/cases/some-included.styl b/src/tests/sample_data/pug/test/cases/some-included.styl similarity index 100% rename from src/test-data/pug/test/cases/some-included.styl rename to src/tests/sample_data/pug/test/cases/some-included.styl diff --git a/src/test-data/pug/test/cases/some.md b/src/tests/sample_data/pug/test/cases/some.md similarity index 100% rename from src/test-data/pug/test/cases/some.md rename to src/tests/sample_data/pug/test/cases/some.md diff --git a/src/test-data/pug/test/cases/some.styl b/src/tests/sample_data/pug/test/cases/some.styl similarity index 100% rename from src/test-data/pug/test/cases/some.styl rename to src/tests/sample_data/pug/test/cases/some.styl diff --git a/src/test-data/pug/test/cases/source.html b/src/tests/sample_data/pug/test/cases/source.html similarity index 100% rename from src/test-data/pug/test/cases/source.html rename to src/tests/sample_data/pug/test/cases/source.html diff --git a/src/test-data/pug/test/cases/source.pug b/src/tests/sample_data/pug/test/cases/source.pug similarity index 100% rename from src/test-data/pug/test/cases/source.pug rename to src/tests/sample_data/pug/test/cases/source.pug diff --git a/src/test-data/pug/test/cases/styles.html b/src/tests/sample_data/pug/test/cases/styles.html similarity index 100% rename from src/test-data/pug/test/cases/styles.html rename to src/tests/sample_data/pug/test/cases/styles.html diff --git a/src/test-data/pug/test/cases/styles.pug b/src/tests/sample_data/pug/test/cases/styles.pug similarity index 100% rename from src/test-data/pug/test/cases/styles.pug rename to src/tests/sample_data/pug/test/cases/styles.pug diff --git a/src/test-data/pug/test/cases/tag.interpolation.html b/src/tests/sample_data/pug/test/cases/tag.interpolation.html similarity index 100% rename from src/test-data/pug/test/cases/tag.interpolation.html rename to src/tests/sample_data/pug/test/cases/tag.interpolation.html diff --git a/src/test-data/pug/test/cases/tag.interpolation.pug b/src/tests/sample_data/pug/test/cases/tag.interpolation.pug similarity index 100% rename from src/test-data/pug/test/cases/tag.interpolation.pug rename to src/tests/sample_data/pug/test/cases/tag.interpolation.pug diff --git a/src/test-data/pug/test/cases/tags.self-closing.html b/src/tests/sample_data/pug/test/cases/tags.self-closing.html similarity index 100% rename from src/test-data/pug/test/cases/tags.self-closing.html rename to src/tests/sample_data/pug/test/cases/tags.self-closing.html diff --git a/src/test-data/pug/test/cases/tags.self-closing.pug b/src/tests/sample_data/pug/test/cases/tags.self-closing.pug similarity index 100% rename from src/test-data/pug/test/cases/tags.self-closing.pug rename to src/tests/sample_data/pug/test/cases/tags.self-closing.pug diff --git a/src/test-data/pug/test/cases/template.html b/src/tests/sample_data/pug/test/cases/template.html similarity index 100% rename from src/test-data/pug/test/cases/template.html rename to src/tests/sample_data/pug/test/cases/template.html diff --git a/src/test-data/pug/test/cases/template.pug b/src/tests/sample_data/pug/test/cases/template.pug similarity index 100% rename from src/test-data/pug/test/cases/template.pug rename to src/tests/sample_data/pug/test/cases/template.pug diff --git a/src/test-data/pug/test/cases/text-block.html b/src/tests/sample_data/pug/test/cases/text-block.html similarity index 100% rename from src/test-data/pug/test/cases/text-block.html rename to src/tests/sample_data/pug/test/cases/text-block.html diff --git a/src/test-data/pug/test/cases/text-block.pug b/src/tests/sample_data/pug/test/cases/text-block.pug similarity index 100% rename from src/test-data/pug/test/cases/text-block.pug rename to src/tests/sample_data/pug/test/cases/text-block.pug diff --git a/src/test-data/pug/test/cases/text.html b/src/tests/sample_data/pug/test/cases/text.html similarity index 100% rename from src/test-data/pug/test/cases/text.html rename to src/tests/sample_data/pug/test/cases/text.html diff --git a/src/test-data/pug/test/cases/text.pug b/src/tests/sample_data/pug/test/cases/text.pug similarity index 100% rename from src/test-data/pug/test/cases/text.pug rename to src/tests/sample_data/pug/test/cases/text.pug diff --git a/src/test-data/pug/test/cases/utf8bom.html b/src/tests/sample_data/pug/test/cases/utf8bom.html similarity index 100% rename from src/test-data/pug/test/cases/utf8bom.html rename to src/tests/sample_data/pug/test/cases/utf8bom.html diff --git a/src/test-data/pug/test/cases/utf8bom.pug b/src/tests/sample_data/pug/test/cases/utf8bom.pug similarity index 100% rename from src/test-data/pug/test/cases/utf8bom.pug rename to src/tests/sample_data/pug/test/cases/utf8bom.pug diff --git a/src/test-data/pug/test/cases/vars.html b/src/tests/sample_data/pug/test/cases/vars.html similarity index 100% rename from src/test-data/pug/test/cases/vars.html rename to src/tests/sample_data/pug/test/cases/vars.html diff --git a/src/test-data/pug/test/cases/vars.pug b/src/tests/sample_data/pug/test/cases/vars.pug similarity index 100% rename from src/test-data/pug/test/cases/vars.pug rename to src/tests/sample_data/pug/test/cases/vars.pug diff --git a/src/test-data/pug/test/cases/while.html b/src/tests/sample_data/pug/test/cases/while.html similarity index 100% rename from src/test-data/pug/test/cases/while.html rename to src/tests/sample_data/pug/test/cases/while.html diff --git a/src/test-data/pug/test/cases/while.pug b/src/tests/sample_data/pug/test/cases/while.pug similarity index 100% rename from src/test-data/pug/test/cases/while.pug rename to src/tests/sample_data/pug/test/cases/while.pug diff --git a/src/test-data/pug/test/cases/xml.html b/src/tests/sample_data/pug/test/cases/xml.html similarity index 100% rename from src/test-data/pug/test/cases/xml.html rename to src/tests/sample_data/pug/test/cases/xml.html diff --git a/src/test-data/pug/test/cases/xml.pug b/src/tests/sample_data/pug/test/cases/xml.pug similarity index 100% rename from src/test-data/pug/test/cases/xml.pug rename to src/tests/sample_data/pug/test/cases/xml.pug diff --git a/src/test-data/pug/test/cases/yield-before-conditional-head.html b/src/tests/sample_data/pug/test/cases/yield-before-conditional-head.html similarity index 100% rename from src/test-data/pug/test/cases/yield-before-conditional-head.html rename to src/tests/sample_data/pug/test/cases/yield-before-conditional-head.html diff --git a/src/test-data/pug/test/cases/yield-before-conditional-head.pug b/src/tests/sample_data/pug/test/cases/yield-before-conditional-head.pug similarity index 100% rename from src/test-data/pug/test/cases/yield-before-conditional-head.pug rename to src/tests/sample_data/pug/test/cases/yield-before-conditional-head.pug diff --git a/src/test-data/pug/test/cases/yield-before-conditional.html b/src/tests/sample_data/pug/test/cases/yield-before-conditional.html similarity index 100% rename from src/test-data/pug/test/cases/yield-before-conditional.html rename to src/tests/sample_data/pug/test/cases/yield-before-conditional.html diff --git a/src/test-data/pug/test/cases/yield-before-conditional.pug b/src/tests/sample_data/pug/test/cases/yield-before-conditional.pug similarity index 100% rename from src/test-data/pug/test/cases/yield-before-conditional.pug rename to src/tests/sample_data/pug/test/cases/yield-before-conditional.pug diff --git a/src/test-data/pug/test/cases/yield-head.html b/src/tests/sample_data/pug/test/cases/yield-head.html similarity index 100% rename from src/test-data/pug/test/cases/yield-head.html rename to src/tests/sample_data/pug/test/cases/yield-head.html diff --git a/src/test-data/pug/test/cases/yield-head.pug b/src/tests/sample_data/pug/test/cases/yield-head.pug similarity index 100% rename from src/test-data/pug/test/cases/yield-head.pug rename to src/tests/sample_data/pug/test/cases/yield-head.pug diff --git a/src/test-data/pug/test/cases/yield-title-head.html b/src/tests/sample_data/pug/test/cases/yield-title-head.html similarity index 100% rename from src/test-data/pug/test/cases/yield-title-head.html rename to src/tests/sample_data/pug/test/cases/yield-title-head.html diff --git a/src/test-data/pug/test/cases/yield-title-head.pug b/src/tests/sample_data/pug/test/cases/yield-title-head.pug similarity index 100% rename from src/test-data/pug/test/cases/yield-title-head.pug rename to src/tests/sample_data/pug/test/cases/yield-title-head.pug diff --git a/src/test-data/pug/test/cases/yield-title.html b/src/tests/sample_data/pug/test/cases/yield-title.html similarity index 100% rename from src/test-data/pug/test/cases/yield-title.html rename to src/tests/sample_data/pug/test/cases/yield-title.html diff --git a/src/test-data/pug/test/cases/yield-title.pug b/src/tests/sample_data/pug/test/cases/yield-title.pug similarity index 100% rename from src/test-data/pug/test/cases/yield-title.pug rename to src/tests/sample_data/pug/test/cases/yield-title.pug diff --git a/src/test-data/pug/test/cases/yield.html b/src/tests/sample_data/pug/test/cases/yield.html similarity index 100% rename from src/test-data/pug/test/cases/yield.html rename to src/tests/sample_data/pug/test/cases/yield.html diff --git a/src/test-data/pug/test/cases/yield.pug b/src/tests/sample_data/pug/test/cases/yield.pug similarity index 100% rename from src/test-data/pug/test/cases/yield.pug rename to src/tests/sample_data/pug/test/cases/yield.pug diff --git a/src/test-data/pug/test/dependencies/dependency1.pug b/src/tests/sample_data/pug/test/dependencies/dependency1.pug similarity index 100% rename from src/test-data/pug/test/dependencies/dependency1.pug rename to src/tests/sample_data/pug/test/dependencies/dependency1.pug diff --git a/src/test-data/pug/test/dependencies/dependency2.pug b/src/tests/sample_data/pug/test/dependencies/dependency2.pug similarity index 100% rename from src/test-data/pug/test/dependencies/dependency2.pug rename to src/tests/sample_data/pug/test/dependencies/dependency2.pug diff --git a/src/test-data/pug/test/dependencies/dependency3.pug b/src/tests/sample_data/pug/test/dependencies/dependency3.pug similarity index 100% rename from src/test-data/pug/test/dependencies/dependency3.pug rename to src/tests/sample_data/pug/test/dependencies/dependency3.pug diff --git a/src/test-data/pug/test/dependencies/extends1.pug b/src/tests/sample_data/pug/test/dependencies/extends1.pug similarity index 100% rename from src/test-data/pug/test/dependencies/extends1.pug rename to src/tests/sample_data/pug/test/dependencies/extends1.pug diff --git a/src/test-data/pug/test/dependencies/extends2.pug b/src/tests/sample_data/pug/test/dependencies/extends2.pug similarity index 100% rename from src/test-data/pug/test/dependencies/extends2.pug rename to src/tests/sample_data/pug/test/dependencies/extends2.pug diff --git a/src/test-data/pug/test/dependencies/include1.pug b/src/tests/sample_data/pug/test/dependencies/include1.pug similarity index 100% rename from src/test-data/pug/test/dependencies/include1.pug rename to src/tests/sample_data/pug/test/dependencies/include1.pug diff --git a/src/test-data/pug/test/dependencies/include2.pug b/src/tests/sample_data/pug/test/dependencies/include2.pug similarity index 100% rename from src/test-data/pug/test/dependencies/include2.pug rename to src/tests/sample_data/pug/test/dependencies/include2.pug diff --git a/src/test-data/pug/test/duplicate-block/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug/test/duplicate-block/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug/test/duplicate-block/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug/test/duplicate-block/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug/test/duplicate-block/index.pug b/src/tests/sample_data/pug/test/duplicate-block/index.pug similarity index 100% rename from src/test-data/pug/test/duplicate-block/index.pug rename to src/tests/sample_data/pug/test/duplicate-block/index.pug diff --git a/src/test-data/pug/test/duplicate-block/index.test.js b/src/tests/sample_data/pug/test/duplicate-block/index.test.js similarity index 100% rename from src/test-data/pug/test/duplicate-block/index.test.js rename to src/tests/sample_data/pug/test/duplicate-block/index.test.js diff --git a/src/test-data/pug/test/duplicate-block/layout-with-duplicate-block.pug b/src/tests/sample_data/pug/test/duplicate-block/layout-with-duplicate-block.pug similarity index 100% rename from src/test-data/pug/test/duplicate-block/layout-with-duplicate-block.pug rename to src/tests/sample_data/pug/test/duplicate-block/layout-with-duplicate-block.pug diff --git a/src/test-data/pug/test/eachOf/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug/test/eachOf/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug/test/eachOf/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug/test/eachOf/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug/test/eachOf/error/left-side.pug b/src/tests/sample_data/pug/test/eachOf/error/left-side.pug similarity index 100% rename from src/test-data/pug/test/eachOf/error/left-side.pug rename to src/tests/sample_data/pug/test/eachOf/error/left-side.pug diff --git a/src/test-data/pug/test/eachOf/error/no-brackets.pug b/src/tests/sample_data/pug/test/eachOf/error/no-brackets.pug similarity index 100% rename from src/test-data/pug/test/eachOf/error/no-brackets.pug rename to src/tests/sample_data/pug/test/eachOf/error/no-brackets.pug diff --git a/src/test-data/pug/test/eachOf/error/one-val.pug b/src/tests/sample_data/pug/test/eachOf/error/one-val.pug similarity index 100% rename from src/test-data/pug/test/eachOf/error/one-val.pug rename to src/tests/sample_data/pug/test/eachOf/error/one-val.pug diff --git a/src/test-data/pug/test/eachOf/error/right-side.pug b/src/tests/sample_data/pug/test/eachOf/error/right-side.pug similarity index 100% rename from src/test-data/pug/test/eachOf/error/right-side.pug rename to src/tests/sample_data/pug/test/eachOf/error/right-side.pug diff --git a/src/test-data/pug/test/eachOf/index.test.js b/src/tests/sample_data/pug/test/eachOf/index.test.js similarity index 100% rename from src/test-data/pug/test/eachOf/index.test.js rename to src/tests/sample_data/pug/test/eachOf/index.test.js diff --git a/src/test-data/pug/test/eachOf/passing/brackets.pug b/src/tests/sample_data/pug/test/eachOf/passing/brackets.pug similarity index 100% rename from src/test-data/pug/test/eachOf/passing/brackets.pug rename to src/tests/sample_data/pug/test/eachOf/passing/brackets.pug diff --git a/src/test-data/pug/test/eachOf/passing/no-brackets.pug b/src/tests/sample_data/pug/test/eachOf/passing/no-brackets.pug similarity index 100% rename from src/test-data/pug/test/eachOf/passing/no-brackets.pug rename to src/tests/sample_data/pug/test/eachOf/passing/no-brackets.pug diff --git a/src/test-data/pug/test/error.reporting.test.js b/src/tests/sample_data/pug/test/error.reporting.test.js similarity index 100% rename from src/test-data/pug/test/error.reporting.test.js rename to src/tests/sample_data/pug/test/error.reporting.test.js diff --git a/src/test-data/pug/test/examples.test.js b/src/tests/sample_data/pug/test/examples.test.js similarity index 100% rename from src/test-data/pug/test/examples.test.js rename to src/tests/sample_data/pug/test/examples.test.js diff --git a/src/test-data/pug/test/extends-not-top-level/default.pug b/src/tests/sample_data/pug/test/extends-not-top-level/default.pug similarity index 100% rename from src/test-data/pug/test/extends-not-top-level/default.pug rename to src/tests/sample_data/pug/test/extends-not-top-level/default.pug diff --git a/src/test-data/pug/test/extends-not-top-level/duplicate.pug b/src/tests/sample_data/pug/test/extends-not-top-level/duplicate.pug similarity index 100% rename from src/test-data/pug/test/extends-not-top-level/duplicate.pug rename to src/tests/sample_data/pug/test/extends-not-top-level/duplicate.pug diff --git a/src/test-data/pug/test/extends-not-top-level/index.pug b/src/tests/sample_data/pug/test/extends-not-top-level/index.pug similarity index 100% rename from src/test-data/pug/test/extends-not-top-level/index.pug rename to src/tests/sample_data/pug/test/extends-not-top-level/index.pug diff --git a/src/test-data/pug/test/extends-not-top-level/index.test.js b/src/tests/sample_data/pug/test/extends-not-top-level/index.test.js similarity index 100% rename from src/test-data/pug/test/extends-not-top-level/index.test.js rename to src/tests/sample_data/pug/test/extends-not-top-level/index.test.js diff --git a/src/test-data/pug/test/fixtures/append-without-block/app-layout.pug b/src/tests/sample_data/pug/test/fixtures/append-without-block/app-layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/append-without-block/app-layout.pug rename to src/tests/sample_data/pug/test/fixtures/append-without-block/app-layout.pug diff --git a/src/test-data/pug/test/fixtures/append-without-block/layout.pug b/src/tests/sample_data/pug/test/fixtures/append-without-block/layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/append-without-block/layout.pug rename to src/tests/sample_data/pug/test/fixtures/append-without-block/layout.pug diff --git a/src/test-data/pug/test/fixtures/append-without-block/page.pug b/src/tests/sample_data/pug/test/fixtures/append-without-block/page.pug similarity index 100% rename from src/test-data/pug/test/fixtures/append-without-block/page.pug rename to src/tests/sample_data/pug/test/fixtures/append-without-block/page.pug diff --git a/src/test-data/pug/test/fixtures/append/app-layout.pug b/src/tests/sample_data/pug/test/fixtures/append/app-layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/append/app-layout.pug rename to src/tests/sample_data/pug/test/fixtures/append/app-layout.pug diff --git a/src/test-data/pug/test/fixtures/append/layout.pug b/src/tests/sample_data/pug/test/fixtures/append/layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/append/layout.pug rename to src/tests/sample_data/pug/test/fixtures/append/layout.pug diff --git a/src/test-data/pug/test/fixtures/append/page.html b/src/tests/sample_data/pug/test/fixtures/append/page.html similarity index 100% rename from src/test-data/pug/test/fixtures/append/page.html rename to src/tests/sample_data/pug/test/fixtures/append/page.html diff --git a/src/test-data/pug/test/fixtures/append/page.pug b/src/tests/sample_data/pug/test/fixtures/append/page.pug similarity index 100% rename from src/test-data/pug/test/fixtures/append/page.pug rename to src/tests/sample_data/pug/test/fixtures/append/page.pug diff --git a/src/test-data/pug/test/fixtures/compile.with.include.locals.error.pug b/src/tests/sample_data/pug/test/fixtures/compile.with.include.locals.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/compile.with.include.locals.error.pug rename to src/tests/sample_data/pug/test/fixtures/compile.with.include.locals.error.pug diff --git a/src/test-data/pug/test/fixtures/compile.with.include.syntax.error.pug b/src/tests/sample_data/pug/test/fixtures/compile.with.include.syntax.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/compile.with.include.syntax.error.pug rename to src/tests/sample_data/pug/test/fixtures/compile.with.include.syntax.error.pug diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.locals.error.pug b/src/tests/sample_data/pug/test/fixtures/compile.with.layout.locals.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/compile.with.layout.locals.error.pug rename to src/tests/sample_data/pug/test/fixtures/compile.with.layout.locals.error.pug diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.syntax.error.pug b/src/tests/sample_data/pug/test/fixtures/compile.with.layout.syntax.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/compile.with.layout.syntax.error.pug rename to src/tests/sample_data/pug/test/fixtures/compile.with.layout.syntax.error.pug diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug b/src/tests/sample_data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug rename to src/tests/sample_data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug b/src/tests/sample_data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug rename to src/tests/sample_data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug diff --git a/src/test-data/pug/test/fixtures/element-with-multiple-attributes.pug b/src/tests/sample_data/pug/test/fixtures/element-with-multiple-attributes.pug similarity index 100% rename from src/test-data/pug/test/fixtures/element-with-multiple-attributes.pug rename to src/tests/sample_data/pug/test/fixtures/element-with-multiple-attributes.pug diff --git a/src/test-data/pug/test/fixtures/include.locals.error.pug b/src/tests/sample_data/pug/test/fixtures/include.locals.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/include.locals.error.pug rename to src/tests/sample_data/pug/test/fixtures/include.locals.error.pug diff --git a/src/test-data/pug/test/fixtures/include.syntax.error.pug b/src/tests/sample_data/pug/test/fixtures/include.syntax.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/include.syntax.error.pug rename to src/tests/sample_data/pug/test/fixtures/include.syntax.error.pug diff --git a/src/test-data/pug/test/fixtures/invalid-block-in-extends.pug b/src/tests/sample_data/pug/test/fixtures/invalid-block-in-extends.pug similarity index 100% rename from src/test-data/pug/test/fixtures/invalid-block-in-extends.pug rename to src/tests/sample_data/pug/test/fixtures/invalid-block-in-extends.pug diff --git a/src/test-data/pug/test/fixtures/issue-1593/include-layout.pug b/src/tests/sample_data/pug/test/fixtures/issue-1593/include-layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/issue-1593/include-layout.pug rename to src/tests/sample_data/pug/test/fixtures/issue-1593/include-layout.pug diff --git a/src/test-data/pug/test/fixtures/issue-1593/include.pug b/src/tests/sample_data/pug/test/fixtures/issue-1593/include.pug similarity index 100% rename from src/test-data/pug/test/fixtures/issue-1593/include.pug rename to src/tests/sample_data/pug/test/fixtures/issue-1593/include.pug diff --git a/src/test-data/pug/test/fixtures/issue-1593/index.pug b/src/tests/sample_data/pug/test/fixtures/issue-1593/index.pug similarity index 100% rename from src/test-data/pug/test/fixtures/issue-1593/index.pug rename to src/tests/sample_data/pug/test/fixtures/issue-1593/index.pug diff --git a/src/test-data/pug/test/fixtures/issue-1593/layout.pug b/src/tests/sample_data/pug/test/fixtures/issue-1593/layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/issue-1593/layout.pug rename to src/tests/sample_data/pug/test/fixtures/issue-1593/layout.pug diff --git a/src/test-data/pug/test/fixtures/layout.locals.error.pug b/src/tests/sample_data/pug/test/fixtures/layout.locals.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/layout.locals.error.pug rename to src/tests/sample_data/pug/test/fixtures/layout.locals.error.pug diff --git a/src/test-data/pug/test/fixtures/layout.pug b/src/tests/sample_data/pug/test/fixtures/layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/layout.pug rename to src/tests/sample_data/pug/test/fixtures/layout.pug diff --git a/src/test-data/pug/test/fixtures/layout.syntax.error.pug b/src/tests/sample_data/pug/test/fixtures/layout.syntax.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/layout.syntax.error.pug rename to src/tests/sample_data/pug/test/fixtures/layout.syntax.error.pug diff --git a/src/test-data/pug/test/fixtures/layout.with.runtime.error.pug b/src/tests/sample_data/pug/test/fixtures/layout.with.runtime.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/layout.with.runtime.error.pug rename to src/tests/sample_data/pug/test/fixtures/layout.with.runtime.error.pug diff --git a/src/test-data/pug/test/fixtures/mixin-include.pug b/src/tests/sample_data/pug/test/fixtures/mixin-include.pug similarity index 100% rename from src/test-data/pug/test/fixtures/mixin-include.pug rename to src/tests/sample_data/pug/test/fixtures/mixin-include.pug diff --git a/src/test-data/pug/test/fixtures/mixin.error.pug b/src/tests/sample_data/pug/test/fixtures/mixin.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/mixin.error.pug rename to src/tests/sample_data/pug/test/fixtures/mixin.error.pug diff --git a/src/test-data/pug/test/fixtures/multi-append-prepend-block/redefine.pug b/src/tests/sample_data/pug/test/fixtures/multi-append-prepend-block/redefine.pug similarity index 100% rename from src/test-data/pug/test/fixtures/multi-append-prepend-block/redefine.pug rename to src/tests/sample_data/pug/test/fixtures/multi-append-prepend-block/redefine.pug diff --git a/src/test-data/pug/test/fixtures/multi-append-prepend-block/root.pug b/src/tests/sample_data/pug/test/fixtures/multi-append-prepend-block/root.pug similarity index 100% rename from src/test-data/pug/test/fixtures/multi-append-prepend-block/root.pug rename to src/tests/sample_data/pug/test/fixtures/multi-append-prepend-block/root.pug diff --git a/src/test-data/pug/test/fixtures/perf.pug b/src/tests/sample_data/pug/test/fixtures/perf.pug similarity index 100% rename from src/test-data/pug/test/fixtures/perf.pug rename to src/tests/sample_data/pug/test/fixtures/perf.pug diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/app-layout.pug b/src/tests/sample_data/pug/test/fixtures/prepend-without-block/app-layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/prepend-without-block/app-layout.pug rename to src/tests/sample_data/pug/test/fixtures/prepend-without-block/app-layout.pug diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/layout.pug b/src/tests/sample_data/pug/test/fixtures/prepend-without-block/layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/prepend-without-block/layout.pug rename to src/tests/sample_data/pug/test/fixtures/prepend-without-block/layout.pug diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/page.html b/src/tests/sample_data/pug/test/fixtures/prepend-without-block/page.html similarity index 100% rename from src/test-data/pug/test/fixtures/prepend-without-block/page.html rename to src/tests/sample_data/pug/test/fixtures/prepend-without-block/page.html diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/page.pug b/src/tests/sample_data/pug/test/fixtures/prepend-without-block/page.pug similarity index 100% rename from src/test-data/pug/test/fixtures/prepend-without-block/page.pug rename to src/tests/sample_data/pug/test/fixtures/prepend-without-block/page.pug diff --git a/src/test-data/pug/test/fixtures/prepend/app-layout.pug b/src/tests/sample_data/pug/test/fixtures/prepend/app-layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/prepend/app-layout.pug rename to src/tests/sample_data/pug/test/fixtures/prepend/app-layout.pug diff --git a/src/test-data/pug/test/fixtures/prepend/layout.pug b/src/tests/sample_data/pug/test/fixtures/prepend/layout.pug similarity index 100% rename from src/test-data/pug/test/fixtures/prepend/layout.pug rename to src/tests/sample_data/pug/test/fixtures/prepend/layout.pug diff --git a/src/test-data/pug/test/fixtures/prepend/page.html b/src/tests/sample_data/pug/test/fixtures/prepend/page.html similarity index 100% rename from src/test-data/pug/test/fixtures/prepend/page.html rename to src/tests/sample_data/pug/test/fixtures/prepend/page.html diff --git a/src/test-data/pug/test/fixtures/prepend/page.pug b/src/tests/sample_data/pug/test/fixtures/prepend/page.pug similarity index 100% rename from src/test-data/pug/test/fixtures/prepend/page.pug rename to src/tests/sample_data/pug/test/fixtures/prepend/page.pug diff --git a/src/test-data/pug/test/fixtures/runtime.error.pug b/src/tests/sample_data/pug/test/fixtures/runtime.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/runtime.error.pug rename to src/tests/sample_data/pug/test/fixtures/runtime.error.pug diff --git a/src/test-data/pug/test/fixtures/runtime.layout.error.pug b/src/tests/sample_data/pug/test/fixtures/runtime.layout.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/runtime.layout.error.pug rename to src/tests/sample_data/pug/test/fixtures/runtime.layout.error.pug diff --git a/src/test-data/pug/test/fixtures/runtime.with.mixin.error.pug b/src/tests/sample_data/pug/test/fixtures/runtime.with.mixin.error.pug similarity index 100% rename from src/test-data/pug/test/fixtures/runtime.with.mixin.error.pug rename to src/tests/sample_data/pug/test/fixtures/runtime.with.mixin.error.pug diff --git a/src/test-data/pug/test/fixtures/scripts.pug b/src/tests/sample_data/pug/test/fixtures/scripts.pug similarity index 100% rename from src/test-data/pug/test/fixtures/scripts.pug rename to src/tests/sample_data/pug/test/fixtures/scripts.pug diff --git a/src/test-data/pug/test/markdown-it/comment.md b/src/tests/sample_data/pug/test/markdown-it/comment.md similarity index 100% rename from src/test-data/pug/test/markdown-it/comment.md rename to src/tests/sample_data/pug/test/markdown-it/comment.md diff --git a/src/test-data/pug/test/markdown-it/index.test.js b/src/tests/sample_data/pug/test/markdown-it/index.test.js similarity index 100% rename from src/test-data/pug/test/markdown-it/index.test.js rename to src/tests/sample_data/pug/test/markdown-it/index.test.js diff --git a/src/test-data/pug/test/markdown-it/layout-markdown-include.pug b/src/tests/sample_data/pug/test/markdown-it/layout-markdown-include.pug similarity index 100% rename from src/test-data/pug/test/markdown-it/layout-markdown-include.pug rename to src/tests/sample_data/pug/test/markdown-it/layout-markdown-include.pug diff --git a/src/test-data/pug/test/markdown-it/layout-markdown-inline.pug b/src/tests/sample_data/pug/test/markdown-it/layout-markdown-inline.pug similarity index 100% rename from src/test-data/pug/test/markdown-it/layout-markdown-inline.pug rename to src/tests/sample_data/pug/test/markdown-it/layout-markdown-inline.pug diff --git a/src/test-data/pug/test/output-es2015/attr.html b/src/tests/sample_data/pug/test/output-es2015/attr.html similarity index 100% rename from src/test-data/pug/test/output-es2015/attr.html rename to src/tests/sample_data/pug/test/output-es2015/attr.html diff --git a/src/test-data/pug/test/plugins.test.js b/src/tests/sample_data/pug/test/plugins.test.js similarity index 100% rename from src/test-data/pug/test/plugins.test.js rename to src/tests/sample_data/pug/test/plugins.test.js diff --git a/src/test-data/pug/test/pug.test.js b/src/tests/sample_data/pug/test/pug.test.js similarity index 100% rename from src/test-data/pug/test/pug.test.js rename to src/tests/sample_data/pug/test/pug.test.js diff --git a/src/test-data/pug/test/regression-2436/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug/test/regression-2436/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug/test/regression-2436/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug/test/regression-2436/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug/test/regression-2436/index.test.js b/src/tests/sample_data/pug/test/regression-2436/index.test.js similarity index 100% rename from src/test-data/pug/test/regression-2436/index.test.js rename to src/tests/sample_data/pug/test/regression-2436/index.test.js diff --git a/src/test-data/pug/test/regression-2436/issue1.pug b/src/tests/sample_data/pug/test/regression-2436/issue1.pug similarity index 100% rename from src/test-data/pug/test/regression-2436/issue1.pug rename to src/tests/sample_data/pug/test/regression-2436/issue1.pug diff --git a/src/test-data/pug/test/regression-2436/issue2.pug b/src/tests/sample_data/pug/test/regression-2436/issue2.pug similarity index 100% rename from src/test-data/pug/test/regression-2436/issue2.pug rename to src/tests/sample_data/pug/test/regression-2436/issue2.pug diff --git a/src/test-data/pug/test/regression-2436/layout.pug b/src/tests/sample_data/pug/test/regression-2436/layout.pug similarity index 100% rename from src/test-data/pug/test/regression-2436/layout.pug rename to src/tests/sample_data/pug/test/regression-2436/layout.pug diff --git a/src/test-data/pug/test/regression-2436/other1.pug b/src/tests/sample_data/pug/test/regression-2436/other1.pug similarity index 100% rename from src/test-data/pug/test/regression-2436/other1.pug rename to src/tests/sample_data/pug/test/regression-2436/other1.pug diff --git a/src/test-data/pug/test/regression-2436/other2.pug b/src/tests/sample_data/pug/test/regression-2436/other2.pug similarity index 100% rename from src/test-data/pug/test/regression-2436/other2.pug rename to src/tests/sample_data/pug/test/regression-2436/other2.pug diff --git a/src/test-data/pug/test/regression-2436/other_layout.pug b/src/tests/sample_data/pug/test/regression-2436/other_layout.pug similarity index 100% rename from src/test-data/pug/test/regression-2436/other_layout.pug rename to src/tests/sample_data/pug/test/regression-2436/other_layout.pug diff --git a/src/test-data/pug/test/run-es2015.test.js b/src/tests/sample_data/pug/test/run-es2015.test.js similarity index 100% rename from src/test-data/pug/test/run-es2015.test.js rename to src/tests/sample_data/pug/test/run-es2015.test.js diff --git a/src/test-data/pug/test/run-syntax-errors.test.js b/src/tests/sample_data/pug/test/run-syntax-errors.test.js similarity index 100% rename from src/test-data/pug/test/run-syntax-errors.test.js rename to src/tests/sample_data/pug/test/run-syntax-errors.test.js diff --git a/src/test-data/pug/test/run-utils.js b/src/tests/sample_data/pug/test/run-utils.js similarity index 100% rename from src/test-data/pug/test/run-utils.js rename to src/tests/sample_data/pug/test/run-utils.js diff --git a/src/test-data/pug/test/run.test.js b/src/tests/sample_data/pug/test/run.test.js similarity index 100% rename from src/test-data/pug/test/run.test.js rename to src/tests/sample_data/pug/test/run.test.js diff --git a/src/test-data/pug/test/shadowed-block/__snapshots__/index.test.js.snap b/src/tests/sample_data/pug/test/shadowed-block/__snapshots__/index.test.js.snap similarity index 100% rename from src/test-data/pug/test/shadowed-block/__snapshots__/index.test.js.snap rename to src/tests/sample_data/pug/test/shadowed-block/__snapshots__/index.test.js.snap diff --git a/src/test-data/pug/test/shadowed-block/base.pug b/src/tests/sample_data/pug/test/shadowed-block/base.pug similarity index 100% rename from src/test-data/pug/test/shadowed-block/base.pug rename to src/tests/sample_data/pug/test/shadowed-block/base.pug diff --git a/src/test-data/pug/test/shadowed-block/index.pug b/src/tests/sample_data/pug/test/shadowed-block/index.pug similarity index 100% rename from src/test-data/pug/test/shadowed-block/index.pug rename to src/tests/sample_data/pug/test/shadowed-block/index.pug diff --git a/src/test-data/pug/test/shadowed-block/index.test.js b/src/tests/sample_data/pug/test/shadowed-block/index.test.js similarity index 100% rename from src/test-data/pug/test/shadowed-block/index.test.js rename to src/tests/sample_data/pug/test/shadowed-block/index.test.js diff --git a/src/test-data/pug/test/shadowed-block/layout.pug b/src/tests/sample_data/pug/test/shadowed-block/layout.pug similarity index 100% rename from src/test-data/pug/test/shadowed-block/layout.pug rename to src/tests/sample_data/pug/test/shadowed-block/layout.pug diff --git a/src/test-data/pug/test/temp/input-compileFile.pug b/src/tests/sample_data/pug/test/temp/input-compileFile.pug similarity index 100% rename from src/test-data/pug/test/temp/input-compileFile.pug rename to src/tests/sample_data/pug/test/temp/input-compileFile.pug diff --git a/src/test-data/pug/test/temp/input-compileFileClient.pug b/src/tests/sample_data/pug/test/temp/input-compileFileClient.pug similarity index 100% rename from src/test-data/pug/test/temp/input-compileFileClient.pug rename to src/tests/sample_data/pug/test/temp/input-compileFileClient.pug diff --git a/src/test-data/pug/test/temp/input-renderFile.pug b/src/tests/sample_data/pug/test/temp/input-renderFile.pug similarity index 100% rename from src/test-data/pug/test/temp/input-renderFile.pug rename to src/tests/sample_data/pug/test/temp/input-renderFile.pug diff --git a/src/v1/codegen.zig b/src/v1/codegen.zig deleted file mode 100644 index 04ddf4b..0000000 --- a/src/v1/codegen.zig +++ /dev/null @@ -1,815 +0,0 @@ -// codegen.zig - Zig port of pug-code-gen -// -// Compiles a Pug AST to HTML output. -// This is a direct HTML generator (unlike the JS version which generates JS code). - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const mem = std.mem; - -// Import AST types from parser -const parser = @import("parser.zig"); -pub const Node = parser.Node; -pub const NodeType = parser.NodeType; -pub const Attribute = parser.Attribute; - -// Import runtime for attribute handling and HTML escaping -const runtime = @import("runtime.zig"); -pub const escapeChar = runtime.escapeChar; - -// Import error types -const pug_error = @import("error.zig"); -pub const PugError = pug_error.PugError; - -// ============================================================================ -// Doctypes -// ============================================================================ - -pub const doctypes = std.StaticStringMap([]const u8).initComptime(.{ - .{ "html", "" }, - .{ "xml", "" }, - .{ "transitional", "" }, - .{ "strict", "" }, - .{ "frameset", "" }, - .{ "1.1", "" }, - .{ "basic", "" }, - .{ "mobile", "" }, - .{ "plist", "" }, -}); - -// Self-closing (void) elements in HTML5 -pub const void_elements = std.StaticStringMap(void).initComptime(.{ - .{ "area", {} }, - .{ "base", {} }, - .{ "br", {} }, - .{ "col", {} }, - .{ "embed", {} }, - .{ "hr", {} }, - .{ "img", {} }, - .{ "input", {} }, - .{ "link", {} }, - .{ "meta", {} }, - .{ "param", {} }, - .{ "source", {} }, - .{ "track", {} }, - .{ "wbr", {} }, -}); - -// Whitespace-sensitive tags -pub const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{ - .{ "pre", {} }, - .{ "textarea", {} }, - .{ "script", {} }, - .{ "style", {} }, -}); - -// ============================================================================ -// Compiler Options -// ============================================================================ - -pub const CompilerOptions = struct { - /// Pretty print output with indentation - pretty: bool = false, - /// Indentation string (default: 2 spaces) - indent_str: []const u8 = " ", - /// Use terse mode (HTML5 style: boolean attrs, > instead of />) - terse: bool = true, - /// Doctype to use - doctype: ?[]const u8 = null, - /// Include debug info - debug: bool = false, - /// Self-closing style (true = />, false = >) - self_closing: bool = false, -}; - -// ============================================================================ -// Compiler Errors -// ============================================================================ - -pub const CompilerError = error{ - OutOfMemory, - InvalidNode, - UnsupportedNodeType, - SelfClosingContent, - InvalidDoctype, -}; - -// ============================================================================ -// Compiler -// ============================================================================ - -pub const Compiler = struct { - allocator: Allocator, - options: CompilerOptions, - output: std.ArrayListUnmanaged(u8), - indent_level: usize = 0, - has_doctype: bool = false, - has_tag: bool = false, - escape_pretty: bool = false, - terse: bool = true, - doctype_str: ?[]const u8 = null, - - pub fn init(allocator: Allocator, options: CompilerOptions) Compiler { - var compiler = Compiler{ - .allocator = allocator, - .options = options, - .output = .{}, - .terse = options.terse, - }; - - // Set up doctype - if (options.doctype) |dt| { - compiler.setDoctype(dt); - } - - return compiler; - } - - pub fn deinit(self: *Compiler) void { - self.output.deinit(self.allocator); - } - - /// Compile an AST node to HTML - pub fn compile(self: *Compiler, node: *Node) CompilerError![]const u8 { - try self.visit(node); - return self.output.toOwnedSlice(self.allocator); - } - - /// Set the doctype - pub fn setDoctype(self: *Compiler, name: []const u8) void { - const lower = name; // TODO: lowercase conversion - if (doctypes.get(lower)) |dt| { - self.doctype_str = dt; - } else { - // Custom doctype - self.doctype_str = null; - } - - // HTML5 uses terse mode - self.terse = mem.eql(u8, lower, "html"); - } - - // ======================================================================== - // Output Helpers - // ======================================================================== - - fn write(self: *Compiler, str: []const u8) CompilerError!void { - try self.output.appendSlice(self.allocator, str); - } - - fn writeChar(self: *Compiler, c: u8) CompilerError!void { - try self.output.append(self.allocator, c); - } - - fn writeEscaped(self: *Compiler, str: []const u8) CompilerError!void { - // Use shared escapeChar from runtime to avoid duplicate escape logic - for (str) |c| { - if (escapeChar(c)) |escaped| { - try self.write(escaped); - } else { - try self.writeChar(c); - } - } - } - - fn prettyIndent(self: *Compiler) CompilerError!void { - if (self.options.pretty and !self.escape_pretty) { - try self.writeChar('\n'); - for (0..self.indent_level) |_| { - try self.write(self.options.indent_str); - } - } - } - - // ======================================================================== - // Visitor Methods - // ======================================================================== - - fn visit(self: *Compiler, node: *Node) CompilerError!void { - switch (node.type) { - .Block, .NamedBlock => try self.visitBlock(node), - .Tag => try self.visitTag(node), - .InterpolatedTag => try self.visitTag(node), - .Text => try self.visitText(node), - .Code => try self.visitCode(node), - .Comment => try self.visitComment(node), - .BlockComment => try self.visitBlockComment(node), - .Doctype => try self.visitDoctype(node), - .Mixin => try self.visitMixin(node), - .MixinBlock => try self.visitMixinBlock(node), - .Case => try self.visitCase(node), - .When => try self.visitWhen(node), - .Conditional => try self.visitConditional(node), - .While => try self.visitWhile(node), - .Each => try self.visitEach(node), - .EachOf => try self.visitEachOf(node), - .YieldBlock => {}, // No-op - .Include, .Extends, .RawInclude, .Filter, .IncludeFilter, .FileReference, .AttributeBlock => { - // These should be processed by linker/loader before codegen - return error.UnsupportedNodeType; - }, - } - } - - fn visitBlock(self: *Compiler, block: *Node) CompilerError!void { - for (block.nodes.items) |child| { - try self.visit(child); - } - } - - fn visitTag(self: *Compiler, tag: *Node) CompilerError!void { - const name = tag.name orelse return error.InvalidNode; - - // Check for whitespace-sensitive tags - use defer to ensure state restoration - const was_escape_pretty = self.escape_pretty; - defer self.escape_pretty = was_escape_pretty; - - if (whitespace_sensitive_tags.has(name)) { - self.escape_pretty = true; - } - - // Auto-doctype for html tag - if (!self.has_tag) { - if (!self.has_doctype and mem.eql(u8, name, "html")) { - try self.visitDoctype(null); - } - self.has_tag = true; - } - - // Pretty indent before tag - if (self.options.pretty and !tag.is_inline) { - try self.prettyIndent(); - } - - self.indent_level += 1; - defer self.indent_level -= 1; - - // Check if self-closing - const is_void = void_elements.has(name); - const is_self_closing = tag.self_closing or is_void; - - // Opening tag - try self.writeChar('<'); - try self.write(name); - - // Attributes - try self.visitAttributes(tag); - - if (is_self_closing) { - if (self.terse and !tag.self_closing) { - try self.writeChar('>'); - } else { - try self.write("/>"); - } - - // Check for content in self-closing tag - if (tag.nodes.items.len > 0) { - return error.SelfClosingContent; - } - } else { - try self.writeChar('>'); - - // Visit children - for (tag.nodes.items) |child| { - try self.visit(child); - } - - // Pretty indent before closing tag - if (self.options.pretty and !tag.is_inline and !whitespace_sensitive_tags.has(name)) { - try self.prettyIndent(); - } - - // Closing tag - try self.write("'); - } - // escape_pretty restoration handled by defer above - } - - fn visitAttributes(self: *Compiler, tag: *Node) CompilerError!void { - for (tag.attrs.items) |attr| { - try self.writeChar(' '); - try self.write(attr.name); - - if (attr.val) |val| { - // Check for boolean attributes in terse mode - const is_bool = mem.eql(u8, val, "true") or mem.eql(u8, val, "false"); - if (self.terse and is_bool) { - if (mem.eql(u8, val, "true")) { - // Terse boolean: just the attribute name - continue; - } else { - // false: don't output the attribute at all - // Rewind the space and name we just wrote - const len = 1 + attr.name.len; - self.output.shrinkRetainingCapacity(self.output.items.len - len); - continue; - } - } - - try self.write("=\""); - if (attr.must_escape) { - try self.writeEscaped(val); - } else { - try self.write(val); - } - try self.writeChar('"'); - } - } - } - - fn visitText(self: *Compiler, text: *Node) CompilerError!void { - if (text.val) |val| { - if (text.is_html) { - try self.write(val); - } else { - try self.writeEscaped(val); - } - } - } - - fn visitCode(self: *Compiler, code: *Node) CompilerError!void { - // Code nodes contain runtime expressions - // In a real implementation, we would evaluate these - // For now, just output the value as-is if buffered - if (code.buffer) { - if (code.val) |val| { - if (code.must_escape) { - try self.writeEscaped(val); - } else { - try self.write(val); - } - } - } - - // Visit block if present - for (code.nodes.items) |child| { - try self.visit(child); - } - } - - fn visitComment(self: *Compiler, comment: *Node) CompilerError!void { - if (!comment.buffer) return; - - try self.prettyIndent(); - try self.write(""); - } - - fn visitBlockComment(self: *Compiler, comment: *Node) CompilerError!void { - if (!comment.buffer) return; - - try self.prettyIndent(); - try self.write(""); - } - - fn visitDoctype(self: *Compiler, doctype: ?*Node) CompilerError!void { - if (doctype) |dt| { - if (dt.val) |val| { - self.setDoctype(val); - } - } - - if (self.doctype_str) |dt_str| { - try self.write(dt_str); - } else { - try self.write(""); - } - self.has_doctype = true; - } - - fn visitMixin(self: *Compiler, mixin: *Node) CompilerError!void { - // Mixin calls would be expanded at link time - // For now, just visit the block if it's a definition - if (!mixin.call) { - // This is a definition - skip it - return; - } - - // Mixin call - visit block if present - for (mixin.nodes.items) |child| { - try self.visit(child); - } - } - - fn visitMixinBlock(_: *Compiler, _: *Node) CompilerError!void { - // MixinBlock is a placeholder for mixin content - // Handled at mixin call site - } - - fn visitCase(self: *Compiler, case_node: *Node) CompilerError!void { - // Case/switch - visit block - for (case_node.nodes.items) |child| { - try self.visit(child); - } - } - - fn visitWhen(self: *Compiler, when_node: *Node) CompilerError!void { - // When - visit block if present - for (when_node.nodes.items) |child| { - try self.visit(child); - } - } - - fn visitConditional(self: *Compiler, cond: *Node) CompilerError!void { - // In static compilation, we can't evaluate conditions - // Visit consequent by default - if (cond.consequent) |cons| { - try self.visit(cons); - } - } - - fn visitWhile(_: *Compiler, _: *Node) CompilerError!void { - // While loops need runtime evaluation - // In static mode, skip - } - - fn visitEach(_: *Compiler, _: *Node) CompilerError!void { - // Each loops need runtime evaluation - // In static mode, skip - } - - fn visitEachOf(_: *Compiler, _: *Node) CompilerError!void { - // EachOf loops need runtime evaluation - // In static mode, skip - } -}; - -// ============================================================================ -// Convenience Functions -// ============================================================================ - -/// Compile an AST to HTML with default options -pub fn compile(allocator: Allocator, ast: *Node) CompilerError![]const u8 { - var compiler = Compiler.init(allocator, .{}); - defer compiler.deinit(); - return compiler.compile(ast); -} - -/// Compile an AST to HTML with custom options -pub fn compileWithOptions(allocator: Allocator, ast: *Node, options: CompilerOptions) CompilerError![]const u8 { - var compiler = Compiler.init(allocator, options); - defer compiler.deinit(); - return compiler.compile(ast); -} - -/// Compile an AST to pretty-printed HTML -pub fn compilePretty(allocator: Allocator, ast: *Node) CompilerError![]const u8 { - return compileWithOptions(allocator, ast, .{ .pretty = true }); -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "compile - simple text" { - const allocator = std.testing.allocator; - - const text = try allocator.create(Node); - text.* = Node{ - .type = .Text, - .val = "Hello, World!", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, text); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("Hello, World!", output); -} - -test "compile - simple tag" { - const allocator = std.testing.allocator; - - const tag = try allocator.create(Node); - tag.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("
", output); -} - -test "compile - tag with text" { - const allocator = std.testing.allocator; - - const text = try allocator.create(Node); - text.* = Node{ - .type = .Text, - .val = "Hello", - .line = 1, - .column = 5, - }; - - const tag = try allocator.create(Node); - tag.* = Node{ - .type = .Tag, - .name = "p", - .line = 1, - .column = 1, - }; - try tag.nodes.append(allocator, text); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("

Hello

", output); -} - -test "compile - tag with attributes" { - const allocator = std.testing.allocator; - - const tag = try allocator.create(Node); - tag.* = Node{ - .type = .Tag, - .name = "a", - .line = 1, - .column = 1, - }; - try tag.attrs.append(allocator, .{ - .name = "href", - .val = "/home", - .line = 1, - .column = 3, - .filename = null, - .must_escape = true, - }); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("", output); -} - -test "compile - self-closing tag" { - const allocator = std.testing.allocator; - - const tag = try allocator.create(Node); - tag.* = Node{ - .type = .Tag, - .name = "br", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("
", output); -} - -test "compile - nested tags" { - const allocator = std.testing.allocator; - - const span = try allocator.create(Node); - span.* = Node{ - .type = .Tag, - .name = "span", - .line = 2, - .column = 3, - }; - - const div = try allocator.create(Node); - div.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - try div.nodes.append(allocator, span); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, div); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("
", output); -} - -test "compile - doctype" { - const allocator = std.testing.allocator; - - const doctype = try allocator.create(Node); - doctype.* = Node{ - .type = .Doctype, - .val = "html", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, doctype); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("", output); -} - -test "compile - comment" { - const allocator = std.testing.allocator; - - const comment = try allocator.create(Node); - comment.* = Node{ - .type = .Comment, - .val = " this is a comment ", - .buffer = true, - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, comment); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("", output); -} - -test "compile - text escaping" { - const allocator = std.testing.allocator; - - const text = try allocator.create(Node); - text.* = Node{ - .type = .Text, - .val = "", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, text); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compile(allocator, root); - defer allocator.free(output); - - try std.testing.expectEqualStrings("<script>alert('xss')</script>", output); -} - -test "compile - pretty print" { - const allocator = std.testing.allocator; - - const inner = try allocator.create(Node); - inner.* = Node{ - .type = .Tag, - .name = "span", - .line = 2, - .column = 3, - }; - - const outer = try allocator.create(Node); - outer.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - try outer.nodes.append(allocator, inner); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, outer); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const output = try compilePretty(allocator, root); - defer allocator.free(output); - - // Pretty output has newlines and indentation - try std.testing.expect(mem.indexOf(u8, output, "\n") != null); -} diff --git a/src/v1/error.zig b/src/v1/error.zig deleted file mode 100644 index 3138c6f..0000000 --- a/src/v1/error.zig +++ /dev/null @@ -1,403 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const Allocator = std.mem.Allocator; -const ArrayListUnmanaged = std.ArrayListUnmanaged; - -// ============================================================================ -// Pug Error - Error formatting with source context -// Based on pug-error package -// ============================================================================ - -/// Pug error with source context and formatting -pub const PugError = struct { - /// Error code (e.g., "PUG:SYNTAX_ERROR") - code: []const u8, - /// Short error message - msg: []const u8, - /// Line number (1-indexed) - line: usize, - /// Column number (1-indexed, 0 if unknown) - column: usize, - /// Source filename (optional) - filename: ?[]const u8, - /// Source code (optional, for context display) - src: ?[]const u8, - /// Full formatted message with context - full_message: ?[]const u8, - - allocator: Allocator, - /// Track if full_message was allocated - owns_full_message: bool, - - pub fn deinit(self: *PugError) void { - if (self.owns_full_message) { - if (self.full_message) |msg| { - self.allocator.free(msg); - } - } - } - - /// Get the formatted message (with context if available) - pub fn getMessage(self: *const PugError) []const u8 { - if (self.full_message) |msg| { - return msg; - } - return self.msg; - } - - /// Format as JSON-like structure for serialization - pub fn toJson(self: *const PugError, allocator: Allocator) ![]const u8 { - var result: ArrayListUnmanaged(u8) = .{}; - errdefer result.deinit(allocator); - - try result.appendSlice(allocator, "{\"code\":\""); - try result.appendSlice(allocator, self.code); - try result.appendSlice(allocator, "\",\"msg\":\""); - try appendJsonEscaped(allocator, &result, self.msg); - try result.appendSlice(allocator, "\",\"line\":"); - - var buf: [32]u8 = undefined; - const line_str = std.fmt.bufPrint(&buf, "{d}", .{self.line}) catch return error.FormatError; - try result.appendSlice(allocator, line_str); - - try result.appendSlice(allocator, ",\"column\":"); - const col_str = std.fmt.bufPrint(&buf, "{d}", .{self.column}) catch return error.FormatError; - try result.appendSlice(allocator, col_str); - - if (self.filename) |fname| { - try result.appendSlice(allocator, ",\"filename\":\""); - try appendJsonEscaped(allocator, &result, fname); - try result.append(allocator, '"'); - } - - try result.append(allocator, '}'); - return try result.toOwnedSlice(allocator); - } -}; - -/// Append JSON-escaped string to result -fn appendJsonEscaped(allocator: Allocator, result: *ArrayListUnmanaged(u8), s: []const u8) !void { - for (s) |c| { - switch (c) { - '"' => try result.appendSlice(allocator, "\\\""), - '\\' => try result.appendSlice(allocator, "\\\\"), - '\n' => try result.appendSlice(allocator, "\\n"), - '\r' => try result.appendSlice(allocator, "\\r"), - '\t' => try result.appendSlice(allocator, "\\t"), - else => { - if (c < 0x20) { - // Control character - encode as \uXXXX - var hex_buf: [6]u8 = undefined; - _ = std.fmt.bufPrint(&hex_buf, "\\u{x:0>4}", .{c}) catch unreachable; - try result.appendSlice(allocator, &hex_buf); - } else { - try result.append(allocator, c); - } - }, - } - } -} - -/// Create a Pug error with formatted message and source context. -/// Equivalent to pug-error's makeError function. -pub fn makeError( - allocator: Allocator, - code: []const u8, - message: []const u8, - options: struct { - line: usize, - column: usize = 0, - filename: ?[]const u8 = null, - src: ?[]const u8 = null, - }, -) !PugError { - var err = PugError{ - .code = code, - .msg = message, - .line = options.line, - .column = options.column, - .filename = options.filename, - .src = options.src, - .full_message = null, - .allocator = allocator, - .owns_full_message = false, - }; - - // Format full message with context - err.full_message = try formatErrorMessage( - allocator, - code, - message, - options.line, - options.column, - options.filename, - options.src, - ); - err.owns_full_message = true; - - return err; -} - -/// Format error message with source context (±3 lines) -fn formatErrorMessage( - allocator: Allocator, - code: []const u8, - message: []const u8, - line: usize, - column: usize, - filename: ?[]const u8, - src: ?[]const u8, -) ![]const u8 { - _ = code; // Code is embedded in PugError struct - - var result: ArrayListUnmanaged(u8) = .{}; - errdefer result.deinit(allocator); - - // Header: filename:line:column or Pug:line:column - if (filename) |fname| { - try result.appendSlice(allocator, fname); - } else { - try result.appendSlice(allocator, "Pug"); - } - try result.append(allocator, ':'); - - var buf: [32]u8 = undefined; - const line_str = std.fmt.bufPrint(&buf, "{d}", .{line}) catch return error.FormatError; - try result.appendSlice(allocator, line_str); - - if (column > 0) { - try result.append(allocator, ':'); - const col_str = std.fmt.bufPrint(&buf, "{d}", .{column}) catch return error.FormatError; - try result.appendSlice(allocator, col_str); - } - try result.append(allocator, '\n'); - - // Source context if available - if (src) |source| { - const lines = try splitLines(allocator, source); - defer allocator.free(lines); - - if (line >= 1 and line <= lines.len) { - // Show ±3 lines around error - const start = if (line > 3) line - 3 else 1; - const end = @min(lines.len, line + 3); - - var i = start; - while (i <= end) : (i += 1) { - const line_idx = i - 1; - if (line_idx >= lines.len) break; - - const src_line = lines[line_idx]; - - // Preamble: " > 5| " or " 5| " - if (i == line) { - try result.appendSlice(allocator, " > "); - } else { - try result.appendSlice(allocator, " "); - } - - // Line number (right-aligned) - const num_str = std.fmt.bufPrint(&buf, "{d}", .{i}) catch return error.FormatError; - try result.appendSlice(allocator, num_str); - try result.appendSlice(allocator, "| "); - - // Source line - try result.appendSlice(allocator, src_line); - try result.append(allocator, '\n'); - - // Column marker for error line - if (i == line and column > 0) { - // Calculate preamble length - const preamble_len = 4 + num_str.len + 2; // " > " + num + "| " - var j: usize = 0; - while (j < preamble_len + column - 1) : (j += 1) { - try result.append(allocator, '-'); - } - try result.append(allocator, '^'); - try result.append(allocator, '\n'); - } - } - try result.append(allocator, '\n'); - } - } else { - try result.append(allocator, '\n'); - } - - // Error message - try result.appendSlice(allocator, message); - - return try result.toOwnedSlice(allocator); -} - -/// Split source into lines (handles \n, \r\n, \r) -fn splitLines(allocator: Allocator, src: []const u8) ![][]const u8 { - var lines: ArrayListUnmanaged([]const u8) = .{}; - errdefer lines.deinit(allocator); - - var start: usize = 0; - var i: usize = 0; - - while (i < src.len) { - if (src[i] == '\n') { - try lines.append(allocator, src[start..i]); - start = i + 1; - i += 1; - } else if (src[i] == '\r') { - try lines.append(allocator, src[start..i]); - // Handle \r\n - if (i + 1 < src.len and src[i + 1] == '\n') { - i += 2; - } else { - i += 1; - } - start = i; - } else { - i += 1; - } - } - - // Last line (may not end with newline) - if (start <= src.len) { - try lines.append(allocator, src[start..]); - } - - return try lines.toOwnedSlice(allocator); -} - -// ============================================================================ -// Common error codes -// ============================================================================ - -pub const ErrorCode = struct { - pub const SYNTAX_ERROR = "PUG:SYNTAX_ERROR"; - pub const INVALID_TOKEN = "PUG:INVALID_TOKEN"; - pub const UNEXPECTED_TOKEN = "PUG:UNEXPECTED_TOKEN"; - pub const INVALID_INDENTATION = "PUG:INVALID_INDENTATION"; - pub const INCONSISTENT_INDENTATION = "PUG:INCONSISTENT_INDENTATION"; - pub const EXTENDS_NOT_FIRST = "PUG:EXTENDS_NOT_FIRST"; - pub const UNEXPECTED_BLOCK = "PUG:UNEXPECTED_BLOCK"; - pub const UNEXPECTED_NODES_IN_EXTENDING_ROOT = "PUG:UNEXPECTED_NODES_IN_EXTENDING_ROOT"; - pub const NO_EXTENDS_PATH = "PUG:NO_EXTENDS_PATH"; - pub const NO_INCLUDE_PATH = "PUG:NO_INCLUDE_PATH"; - pub const MALFORMED_EXTENDS = "PUG:MALFORMED_EXTENDS"; - pub const MALFORMED_INCLUDE = "PUG:MALFORMED_INCLUDE"; - pub const FILTER_NOT_FOUND = "PUG:FILTER_NOT_FOUND"; - pub const INVALID_FILTER = "PUG:INVALID_FILTER"; -}; - -// ============================================================================ -// Tests -// ============================================================================ - -test "makeError - basic error without source" { - const allocator = std.testing.allocator; - var err = try makeError(allocator, "PUG:TEST", "test error", .{ - .line = 5, - .column = 10, - .filename = "test.pug", - }); - defer err.deinit(); - - try std.testing.expectEqualStrings("PUG:TEST", err.code); - try std.testing.expectEqualStrings("test error", err.msg); - try std.testing.expectEqual(@as(usize, 5), err.line); - try std.testing.expectEqual(@as(usize, 10), err.column); - try std.testing.expectEqualStrings("test.pug", err.filename.?); - - const msg = err.getMessage(); - try std.testing.expect(mem.indexOf(u8, msg, "test.pug:5:10") != null); - try std.testing.expect(mem.indexOf(u8, msg, "test error") != null); -} - -test "makeError - error with source context" { - const allocator = std.testing.allocator; - const src = "line 1\nline 2\nline 3 with error\nline 4\nline 5"; - var err = try makeError(allocator, "PUG:SYNTAX_ERROR", "unexpected token", .{ - .line = 3, - .column = 8, - .filename = "template.pug", - .src = src, - }); - defer err.deinit(); - - const msg = err.getMessage(); - // Should contain filename:line:column - try std.testing.expect(mem.indexOf(u8, msg, "template.pug:3:8") != null); - // Should contain the error line with marker - try std.testing.expect(mem.indexOf(u8, msg, "line 3 with error") != null); - // Should contain the error message - try std.testing.expect(mem.indexOf(u8, msg, "unexpected token") != null); - // Should have column marker - try std.testing.expect(mem.indexOf(u8, msg, "^") != null); -} - -test "makeError - error with source shows context lines" { - const allocator = std.testing.allocator; - const src = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8"; - var err = try makeError(allocator, "PUG:TEST", "test", .{ - .line = 5, - .filename = null, - .src = src, - }); - defer err.deinit(); - - const msg = err.getMessage(); - // Should show lines 2-8 (5 ± 3) - try std.testing.expect(mem.indexOf(u8, msg, "line 2") != null); - try std.testing.expect(mem.indexOf(u8, msg, "line 5") != null); - try std.testing.expect(mem.indexOf(u8, msg, "line 8") != null); - // Line 1 should not be shown (too far before) - // Note: line 1 might appear in context depending on implementation -} - -test "makeError - no filename uses Pug" { - const allocator = std.testing.allocator; - var err = try makeError(allocator, "PUG:TEST", "test error", .{ - .line = 1, - }); - defer err.deinit(); - - const msg = err.getMessage(); - try std.testing.expect(mem.indexOf(u8, msg, "Pug:1") != null); -} - -test "PugError.toJson" { - const allocator = std.testing.allocator; - var err = try makeError(allocator, "PUG:TEST", "test message", .{ - .line = 10, - .column = 5, - .filename = "file.pug", - }); - defer err.deinit(); - - const json = try err.toJson(allocator); - defer allocator.free(json); - - try std.testing.expect(mem.indexOf(u8, json, "\"code\":\"PUG:TEST\"") != null); - try std.testing.expect(mem.indexOf(u8, json, "\"msg\":\"test message\"") != null); - try std.testing.expect(mem.indexOf(u8, json, "\"line\":10") != null); - try std.testing.expect(mem.indexOf(u8, json, "\"column\":5") != null); - try std.testing.expect(mem.indexOf(u8, json, "\"filename\":\"file.pug\"") != null); -} - -test "splitLines - basic" { - const allocator = std.testing.allocator; - const lines = try splitLines(allocator, "a\nb\nc"); - defer allocator.free(lines); - - try std.testing.expectEqual(@as(usize, 3), lines.len); - try std.testing.expectEqualStrings("a", lines[0]); - try std.testing.expectEqualStrings("b", lines[1]); - try std.testing.expectEqualStrings("c", lines[2]); -} - -test "splitLines - windows line endings" { - const allocator = std.testing.allocator; - const lines = try splitLines(allocator, "a\r\nb\r\nc"); - defer allocator.free(lines); - - try std.testing.expectEqual(@as(usize, 3), lines.len); - try std.testing.expectEqualStrings("a", lines[0]); - try std.testing.expectEqualStrings("b", lines[1]); - try std.testing.expectEqualStrings("c", lines[2]); -} diff --git a/src/v1/lexer.zig b/src/v1/lexer.zig deleted file mode 100644 index 71bfedf..0000000 --- a/src/v1/lexer.zig +++ /dev/null @@ -1,2686 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const Allocator = std.mem.Allocator; - -// ============================================================================ -// Token Types -// ============================================================================ - -pub const TokenType = enum { - tag, - id, - class, - text, - text_html, - comment, - doctype, - filter, - extends, - include, - path, - block, - mixin_block, - mixin, - call, - yield, - code, - blockcode, - interpolation, - interpolated_code, - @"if", - else_if, - @"else", - case, - when, - default, - each, - each_of, - @"while", - indent, - outdent, - newline, - eos, - dot, - colon, - slash, - start_attributes, - end_attributes, - attribute, - @"&attributes", - start_pug_interpolation, - end_pug_interpolation, - start_pipeless_text, - end_pipeless_text, -}; - -// ============================================================================ -// Token Value - Tagged Union for type-safe token values -// ============================================================================ - -pub const TokenValue = union(enum) { - none, - string: []const u8, - boolean: bool, - - pub fn isNone(self: TokenValue) bool { - return self == .none; - } - - pub fn getString(self: TokenValue) ?[]const u8 { - return switch (self) { - .string => |s| s, - else => null, - }; - } - - pub fn getBool(self: TokenValue) ?bool { - return switch (self) { - .boolean => |b| b, - else => null, - }; - } - - pub fn fromString(s: []const u8) TokenValue { - return .{ .string = s }; - } - - pub fn fromBool(b: bool) TokenValue { - return .{ .boolean = b }; - } - - pub fn format( - self: TokenValue, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = fmt; - _ = options; - switch (self) { - .none => try writer.writeAll("none"), - .string => |s| try writer.print("\"{s}\"", .{s}), - .boolean => |b| try writer.print("{}", .{b}), - } - } -}; - -// ============================================================================ -// Location and Token -// ============================================================================ - -pub const Location = struct { - line: usize, - column: usize, -}; - -pub const TokenLoc = struct { - start: Location, - end: ?Location = null, - filename: ?[]const u8 = null, -}; - -pub const Token = struct { - type: TokenType, - val: TokenValue = .none, - loc: TokenLoc, - // Additional fields for specific token types - buffer: TokenValue = .none, // boolean for comment/code tokens - must_escape: TokenValue = .none, // boolean for code/attribute tokens - mode: TokenValue = .none, // string: "prepend", "append", "replace" for block - args: TokenValue = .none, // string for mixin/call - key: TokenValue = .none, // string for each - code: TokenValue = .none, // string for each/eachOf - name: TokenValue = .none, // string for attribute - - /// Helper to get val as string - pub fn getVal(self: Token) ?[]const u8 { - return self.val.getString(); - } - - /// Helper to check if buffer is true - pub fn isBuffered(self: Token) bool { - return self.buffer.getBool() orelse false; - } - - /// Helper to check if must_escape is true - pub fn shouldEscape(self: Token) bool { - return self.must_escape.getBool() orelse true; - } - - /// Helper to get mode as string - pub fn getMode(self: Token) ?[]const u8 { - return self.mode.getString(); - } - - /// Helper to get args as string - pub fn getArgs(self: Token) ?[]const u8 { - return self.args.getString(); - } - - /// Helper to get key as string - pub fn getKey(self: Token) ?[]const u8 { - return self.key.getString(); - } - - /// Helper to get code as string - pub fn getCode(self: Token) ?[]const u8 { - return self.code.getString(); - } - - /// Helper to get attribute name as string - pub fn getName(self: Token) ?[]const u8 { - return self.name.getString(); - } -}; - -// ============================================================================ -// Character Parser State (simplified) - Zig 0.15 style with ArrayListUnmanaged -// ============================================================================ - -const BracketType = enum { paren, brace, bracket }; - -const CharParserState = struct { - nesting_stack: std.ArrayListUnmanaged(BracketType) = .{}, - in_string: bool = false, - string_char: ?u8 = null, - in_template: bool = false, - escape_next: bool = false, - - pub fn deinit(self: *CharParserState, allocator: Allocator) void { - self.nesting_stack.deinit(allocator); - } - - pub fn isNesting(self: *const CharParserState) bool { - return self.nesting_stack.items.len > 0; - } - - pub fn isString(self: *const CharParserState) bool { - return self.in_string or self.in_template; - } - - pub fn getStringChar(self: *const CharParserState) ?u8 { - if (self.in_string) return self.string_char; - if (self.in_template) return '`'; - return null; - } - - pub fn parseChar(self: *CharParserState, allocator: Allocator, char: u8) !void { - if (self.escape_next) { - self.escape_next = false; - return; - } - - if (char == '\\') { - self.escape_next = true; - return; - } - - if (self.in_string) { - if (char == self.string_char.?) { - self.in_string = false; - self.string_char = null; - } - return; - } - - if (self.in_template) { - if (char == '`') { - self.in_template = false; - } - return; - } - - switch (char) { - '"', '\'' => { - self.in_string = true; - self.string_char = char; - }, - '`' => { - self.in_template = true; - }, - '(' => try self.nesting_stack.append(allocator, .paren), - '{' => try self.nesting_stack.append(allocator, .brace), - '[' => try self.nesting_stack.append(allocator, .bracket), - ')' => { - if (self.nesting_stack.items.len > 0 and - self.nesting_stack.items[self.nesting_stack.items.len - 1] == .paren) - { - _ = self.nesting_stack.pop(); - } - }, - '}' => { - if (self.nesting_stack.items.len > 0 and - self.nesting_stack.items[self.nesting_stack.items.len - 1] == .brace) - { - _ = self.nesting_stack.pop(); - } - }, - ']' => { - if (self.nesting_stack.items.len > 0 and - self.nesting_stack.items[self.nesting_stack.items.len - 1] == .bracket) - { - _ = self.nesting_stack.pop(); - } - }, - else => {}, - } - } -}; - -// ============================================================================ -// Lexer Error -// ============================================================================ - -pub const LexerErrorCode = enum { - ASSERT_FAILED, - SYNTAX_ERROR, - INCORRECT_NESTING, - NO_END_BRACKET, - BRACKET_MISMATCH, - INVALID_ID, - INVALID_CLASS_NAME, - NO_EXTENDS_PATH, - MALFORMED_EXTENDS, - NO_INCLUDE_PATH, - MALFORMED_INCLUDE, - NO_CASE_EXPRESSION, - NO_WHEN_EXPRESSION, - DEFAULT_WITH_EXPRESSION, - NO_WHILE_EXPRESSION, - MALFORMED_EACH, - MALFORMED_EACH_OF_LVAL, - INVALID_INDENTATION, - INCONSISTENT_INDENTATION, - UNEXPECTED_TEXT, - INVALID_KEY_CHARACTER, - ELSE_CONDITION, -}; - -pub const LexerError = struct { - code: LexerErrorCode, - message: []const u8, - line: usize, - column: usize, - filename: ?[]const u8, -}; - -// ============================================================================ -// BracketExpression Result -// ============================================================================ - -const BracketExpressionResult = struct { - src: []const u8, - end: usize, -}; - -// ============================================================================ -// Lexer - Zig 0.15 style with ArrayListUnmanaged -// ============================================================================ - -pub const Lexer = struct { - allocator: Allocator, - input: []const u8, - input_allocated: []const u8, // Keep reference to allocated memory for cleanup - original_input: []const u8, - filename: ?[]const u8, - interpolated: bool, - lineno: usize, - colno: usize, - indent_stack: std.ArrayListUnmanaged(usize) = .{}, - indent_re_type: ?IndentType = null, - interpolation_allowed: bool, - tokens: std.ArrayListUnmanaged(Token) = .{}, - ended: bool, - last_error: ?LexerError = null, - - const IndentType = enum { tabs, spaces }; - - pub fn init(allocator: Allocator, str: []const u8, options: LexerOptions) !Lexer { - // Strip UTF-8 BOM if present - var input = str; - if (input.len >= 3 and input[0] == 0xEF and input[1] == 0xBB and input[2] == 0xBF) { - input = input[3..]; - } - - // Normalize line endings - var normalized: std.ArrayListUnmanaged(u8) = .{}; - errdefer normalized.deinit(allocator); - - var i: usize = 0; - while (i < input.len) { - if (input[i] == '\r') { - if (i + 1 < input.len and input[i + 1] == '\n') { - try normalized.append(allocator, '\n'); - i += 2; - } else { - try normalized.append(allocator, '\n'); - i += 1; - } - } else { - try normalized.append(allocator, input[i]); - i += 1; - } - } - - var indent_stack: std.ArrayListUnmanaged(usize) = .{}; - try indent_stack.append(allocator, 0); - - const input_slice = try normalized.toOwnedSlice(allocator); - - return Lexer{ - .allocator = allocator, - .input = input_slice, - .input_allocated = input_slice, - .original_input = str, - .filename = options.filename, - .interpolated = options.interpolated, - .lineno = options.starting_line, - .colno = options.starting_column, - .indent_stack = indent_stack, - .interpolation_allowed = true, - .tokens = .{}, - .ended = false, - }; - } - - pub fn deinit(self: *Lexer) void { - self.indent_stack.deinit(self.allocator); - self.tokens.deinit(self.allocator); - if (self.input_allocated.len > 0) { - self.allocator.free(self.input_allocated); - } - } - - // ======================================================================== - // Error handling - // ======================================================================== - - fn setError(self: *Lexer, err_code: LexerErrorCode, message: []const u8) void { - self.last_error = LexerError{ - .code = err_code, - .message = message, - .line = self.lineno, - .column = self.colno, - .filename = self.filename, - }; - } - - /// Set error and return false - common pattern for scan functions - fn failWith(self: *Lexer, err_code: LexerErrorCode, message: []const u8) bool { - self.setError(err_code, message); - return false; - } - - /// Set error and return LexerError - for functions with error unions - fn failWithError(self: *Lexer, err_code: LexerErrorCode, message: []const u8) error{LexerError} { - self.setError(err_code, message); - return error.LexerError; - } - - // ======================================================================== - // Token creation - // ======================================================================== - - fn tok(self: *Lexer, token_type: TokenType, val: TokenValue) Token { - return Token{ - .type = token_type, - .val = val, - .loc = TokenLoc{ - .start = Location{ - .line = self.lineno, - .column = self.colno, - }, - .filename = self.filename, - }, - }; - } - - fn tokWithString(self: *Lexer, token_type: TokenType, val: ?[]const u8) Token { - return self.tok(token_type, if (val) |v| TokenValue.fromString(v) else .none); - } - - fn tokEnd(self: *Lexer, token: *Token) void { - token.loc.end = Location{ - .line = self.lineno, - .column = self.colno, - }; - } - - /// Helper to emit a token with common boilerplate: - /// 1. Creates token with type and string value - /// 2. Appends to tokens list - /// 3. Increments column by specified amount - /// 4. Sets token end location - /// Returns false on allocation failure. - fn emitToken(self: *Lexer, token_type: TokenType, val: ?[]const u8, col_increment: usize) bool { - var token = self.tokWithString(token_type, val); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(col_increment); - self.tokEnd(&token); - return true; - } - - /// Helper to emit a token with a TokenValue (for non-string values) - fn emitTokenVal(self: *Lexer, token_type: TokenType, val: TokenValue, col_increment: usize) bool { - var token = self.tok(token_type, val); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(col_increment); - self.tokEnd(&token); - return true; - } - - // ======================================================================== - // Position tracking - // ======================================================================== - - fn incrementLine(self: *Lexer, increment: usize) void { - self.lineno += increment; - if (increment > 0) { - self.colno = 1; - } - } - - fn incrementColumn(self: *Lexer, increment: usize) void { - self.colno += increment; - } - - fn consume(self: *Lexer, len: usize) void { - self.input = self.input[len..]; - } - - // ======================================================================== - // Scanning helpers - // ======================================================================== - - fn isWhitespace(char: u8) bool { - return char == ' ' or char == '\n' or char == '\t'; - } - - // ======================================================================== - // Bracket expression parsing - // ======================================================================== - - fn bracketExpression(self: *Lexer, skip: usize) !BracketExpressionResult { - if (skip >= self.input.len) { - return self.failWithError(.NO_END_BRACKET, "Empty input for bracket expression"); - } - - const start_char = self.input[skip]; - const end_char: u8 = switch (start_char) { - '(' => ')', - '{' => '}', - '[' => ']', - else => { - return self.failWithError(.ASSERT_FAILED, "The start character should be '(', '{' or '['"); - }, - }; - - var state: CharParserState = .{}; - defer state.deinit(self.allocator); - - var i = skip + 1; - - // Use fixed-size stack buffer for bracket tracking (avoids allocations) - // 256 levels of nesting should be more than enough for any real code - var bracket_stack: [256]u8 = undefined; - var bracket_depth: usize = 1; - bracket_stack[0] = start_char; - - while (i < self.input.len) { - const char = self.input[i]; - - try state.parseChar(self.allocator, char); - - if (!state.isString()) { - // Check for opening brackets - if (char == '(' or char == '[' or char == '{') { - if (bracket_depth >= bracket_stack.len) { - return self.failWithError(.BRACKET_MISMATCH, "Bracket nesting too deep (max 256 levels)"); - } - bracket_stack[bracket_depth] = char; - bracket_depth += 1; - } - // Check for closing brackets - else if (char == ')' or char == ']' or char == '}') { - // Check for bracket type mismatch - if (bracket_depth > 0) { - const last_open = bracket_stack[bracket_depth - 1]; - const expected_close: u8 = switch (last_open) { - '(' => ')', - '[' => ']', - '{' => '}', - else => 0, - }; - if (char != expected_close) { - return self.failWithError(.BRACKET_MISMATCH, "Mismatched bracket - expected different closing bracket"); - } - bracket_depth -= 1; - } - - if (char == end_char and bracket_depth == 0) { - return BracketExpressionResult{ - .src = self.input[skip + 1 .. i], - .end = i, - }; - } - } - } - - i += 1; - } - - return self.failWithError(.NO_END_BRACKET, "The end of the string reached with no closing bracket found."); - } - - // ======================================================================== - // Indentation scanning - // ======================================================================== - - fn scanIndentation(self: *Lexer) ?struct { indent: []const u8, total_len: usize } { - if (self.input.len == 0 or self.input[0] != '\n') { - return null; - } - - const indent_start: usize = 1; - - // Single-pass: detect indent type from first whitespace character - if (indent_start >= self.input.len) { - return .{ .indent = "", .total_len = 1 }; - } - - const first_char = self.input[indent_start]; - - // Determine indent type from first character (or use existing type) - if (first_char == '\t') { - // Tab-based indentation - if (self.indent_re_type == .spaces) { - // Already using spaces, but found tab - scan tabs then trailing spaces - var i = indent_start; - while (i < self.input.len and self.input[i] == '\t') : (i += 1) {} - const tab_end = i; - // Skip trailing spaces after tabs - while (i < self.input.len and self.input[i] == ' ') : (i += 1) {} - return .{ .indent = self.input[indent_start..tab_end], .total_len = i }; - } - // Using tabs or undetermined - self.indent_re_type = .tabs; - var i = indent_start; - while (i < self.input.len and self.input[i] == '\t') : (i += 1) {} - const tab_end = i; - // Skip trailing spaces after tabs - while (i < self.input.len and self.input[i] == ' ') : (i += 1) {} - return .{ .indent = self.input[indent_start..tab_end], .total_len = i }; - } else if (first_char == ' ') { - // Space-based indentation - self.indent_re_type = .spaces; - var i = indent_start; - while (i < self.input.len and self.input[i] == ' ') : (i += 1) {} - return .{ .indent = self.input[indent_start..i], .total_len = i }; - } - - // Just a newline with no indentation - return .{ .indent = "", .total_len = 1 }; - } - - // ======================================================================== - // Token parsing methods - // ======================================================================== - - fn eos(self: *Lexer) bool { - if (self.input.len > 0) return false; - - if (self.interpolated) { - self.setError(.NO_END_BRACKET, "End of line was reached with no closing bracket for interpolation."); - return false; - } - - // Add outdent tokens for remaining indentation - var i: usize = 0; - while (i < self.indent_stack.items.len and self.indent_stack.items[i] > 0) : (i += 1) { - var outdent_tok = self.tok(.outdent, .none); - self.tokEnd(&outdent_tok); - self.tokens.append(self.allocator, outdent_tok) catch return false; - } - - var eos_tok = self.tok(.eos, .none); - self.tokEnd(&eos_tok); - self.tokens.append(self.allocator, eos_tok) catch return false; - self.ended = true; - return true; - } - - fn blank(self: *Lexer) bool { - // Match /^\n[ \t]*\n/ - if (self.input.len < 2 or self.input[0] != '\n') return false; - - var i: usize = 1; - while (i < self.input.len and (self.input[i] == ' ' or self.input[i] == '\t')) { - i += 1; - } - - if (i < self.input.len and self.input[i] == '\n') { - self.consume(i); // Don't consume the second newline - self.incrementLine(1); - return true; - } - - return false; - } - - fn comment(self: *Lexer) bool { - // Match /^\/\/(-)?([^\n]*)/ - if (self.input.len < 2 or self.input[0] != '/' or self.input[1] != '/') { - return false; - } - - var i: usize = 2; - var buffer = true; - - if (i < self.input.len and self.input[i] == '-') { - buffer = false; - i += 1; - } - - const comment_start = i; - while (i < self.input.len and self.input[i] != '\n') { - i += 1; - } - - const comment_text = self.input[comment_start..i]; - self.consume(i); - - var token = self.tokWithString(.comment, comment_text); - token.buffer = TokenValue.fromBool(buffer); - self.interpolation_allowed = buffer; - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(i); - self.tokEnd(&token); - - _ = self.pipelessText(null); - return true; - } - - fn interpolation(self: *Lexer) bool { - // Match /^#\{/ - if (self.input.len < 2 or self.input[0] != '#' or self.input[1] != '{') { - return false; - } - - const match = self.bracketExpression(1) catch return false; - self.consume(match.end + 1); - - var token = self.tokWithString(.interpolation, match.src); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(2); // '#{' - - // Count newlines in expression - var lines: usize = 0; - var last_line_len: usize = 0; - for (match.src) |c| { - if (c == '\n') { - lines += 1; - last_line_len = 0; - } else { - last_line_len += 1; - } - } - - self.incrementLine(lines); - self.incrementColumn(last_line_len + 1); // + 1 for '}' - self.tokEnd(&token); - return true; - } - - fn tag(self: *Lexer) bool { - // Match /^(\w(?:[-:\w]*\w)?)/ - if (self.input.len == 0) return false; - - const first = self.input[0]; - if (!isWordChar(first)) return false; - - var end: usize = 1; - while (end < self.input.len) { - const c = self.input[end]; - if (isWordChar(c) or c == '-' or c == ':') { - end += 1; - } else { - break; - } - } - - // Ensure it doesn't end with - or : - while (end > 1 and (self.input[end - 1] == '-' or self.input[end - 1] == ':')) { - end -= 1; - } - - if (end == 0) return false; - - const name = self.input[0..end]; - self.consume(end); - - var token = self.tokWithString(.tag, name); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn isWordChar(c: u8) bool { - return (c >= 'a' and c <= 'z') or - (c >= 'A' and c <= 'Z') or - (c >= '0' and c <= '9') or - c == '_'; - } - - fn filter(self: *Lexer, in_include: bool) bool { - // Match /^:([\w\-]+)/ - if (self.input.len < 2 or self.input[0] != ':') return false; - - var end: usize = 1; - while (end < self.input.len) { - const c = self.input[end]; - if (isWordChar(c) or c == '-') { - end += 1; - } else { - break; - } - } - - if (end == 1) return false; - - const filter_name = self.input[1..end]; - self.consume(end); - - var token = self.tokWithString(.filter, filter_name); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(filter_name.len); - self.tokEnd(&token); - _ = self.attrs(); - - if (!in_include) { - self.interpolation_allowed = false; - _ = self.pipelessText(null); - } - return true; - } - - fn doctype(self: *Lexer) bool { - // Match /^doctype *([^\n]*)/ - const prefix = "doctype"; - if (!mem.startsWith(u8, self.input, prefix)) return false; - - var i = prefix.len; - - // Skip spaces - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // Find end of line - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - const doctype_val = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.doctype, if (doctype_val.len > 0) doctype_val else null); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn id(self: *Lexer) bool { - // Match /^#([\w-]+)/ - if (self.input.len < 2 or self.input[0] != '#') return false; - - // Check it's not #{ - if (self.input[1] == '{') return false; - - var end: usize = 1; - while (end < self.input.len) { - const c = self.input[end]; - if (isWordChar(c) or c == '-') { - end += 1; - } else { - break; - } - } - - if (end == 1) { - self.setError(.INVALID_ID, "Invalid ID"); - return false; - } - - const id_val = self.input[1..end]; - self.consume(end); - - var token = self.tokWithString(.id, id_val); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(id_val.len); - self.tokEnd(&token); - return true; - } - - fn className(self: *Lexer) bool { - // Match /^\.([_a-z0-9\-]*[_a-z][_a-z0-9\-]*)/i - if (self.input.len < 2 or self.input[0] != '.') return false; - - var end: usize = 1; - var has_letter = false; - - while (end < self.input.len) { - const c = self.input[end]; - if (isWordChar(c) or c == '-') { - if ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '_') { - has_letter = true; - } - end += 1; - } else { - break; - } - } - - if (end == 1 or !has_letter) { - if (end > 1) { - self.setError(.INVALID_CLASS_NAME, "Class names must contain at least one letter or underscore."); - } - return false; - } - - const class_name = self.input[1..end]; - self.consume(end); - - var token = self.tokWithString(.class, class_name); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(class_name.len); - self.tokEnd(&token); - return true; - } - - fn endInterpolation(self: *Lexer) bool { - if (self.interpolated and self.input.len > 0 and self.input[0] == ']') { - self.consume(1); - self.ended = true; - return true; - } - return false; - } - - fn text(self: *Lexer) bool { - // Match /^(?:\| ?| )([^\n]+)/ or /^( )/ or /^\|( ?)/ - // This handles: - // 1. "| text" - piped text - // 2. " text" - inline text after tag (space followed by text) - // 3. "|" or "| " - empty pipe - if (self.input.len == 0) return false; - - // Case 1: Pipe syntax "| text" or "|" - if (self.input[0] == '|') { - var i: usize = 1; - // Skip optional space after | - if (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // Find end of line - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - const text_val = self.input[i..end]; - self.consume(end); - - self.addText(.text, text_val, "", 0); - return true; - } - - // Case 2: Inline text after tag " text" (space followed by content) - if (self.input[0] == ' ') { - // Find end of potential text (until newline) - var end: usize = 1; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - // Check what's in the rest of the line after the space - const rest = self.input[1..end]; - - // If it's only whitespace, don't treat as text (let indent handle newlines) - var all_whitespace = true; - for (rest) |c| { - if (c != ' ' and c != '\t') { - all_whitespace = false; - break; - } - } - - if (all_whitespace) { - // Only whitespace until newline - consume it but don't create text token - self.consume(end); - self.incrementColumn(end); - return true; - } - - // Check if it's just " /" pattern (self-closing tag with space) - var trimmed_start: usize = 0; - while (trimmed_start < rest.len and rest[trimmed_start] == ' ') { - trimmed_start += 1; - } - if (trimmed_start < rest.len and rest[trimmed_start] == '/' and - (trimmed_start + 1 >= rest.len or rest[trimmed_start + 1] == ' ' or rest[trimmed_start + 1] == '\n')) - { - // This is "tag /" pattern - consume spaces, let slash handler deal with / - self.consume(1 + trimmed_start); - self.incrementColumn(1 + trimmed_start); - return true; - } - - const text_val = self.input[1..end]; - self.consume(end); - - self.addText(.text, text_val, "", 0); - return true; - } - - return false; - } - - fn textHtml(self: *Lexer) bool { - // Match /^(<[^\n]*)/ - if (self.input.len == 0 or self.input[0] != '<') return false; - - var end: usize = 1; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - const html_val = self.input[0..end]; - self.consume(end); - - self.addText(.text_html, html_val, "", 0); - return true; - } - - fn dot(self: *Lexer) bool { - // Match /^\./ - if (self.input.len == 0 or self.input[0] != '.') return false; - - // Check if it's followed by end of line or colon - if (self.input.len == 1 or self.input[1] == '\n' or self.input[1] == ':') { - self.consume(1); - var token = self.tok(.dot, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(1); - self.tokEnd(&token); - _ = self.pipelessText(null); - return true; - } - - return false; - } - - fn extendsToken(self: *Lexer) bool { - // Match /^extends?(?= |$|\n)/ - if (mem.startsWith(u8, self.input, "extends")) { - const after = if (self.input.len > 7) self.input[7] else 0; - if (after == 0 or after == ' ' or after == '\n') { - self.consume(7); - var token = self.tok(.extends, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(7); - self.tokEnd(&token); - - if (!self.path()) { - self.setError(.NO_EXTENDS_PATH, "missing path for extends"); - return true; - } - return true; - } - // "extends" followed by something else (like "(") - malformed - if (after != 0) { - self.setError(.MALFORMED_EXTENDS, "malformed extends"); - return true; - } - } else if (mem.startsWith(u8, self.input, "extend")) { - const after = if (self.input.len > 6) self.input[6] else 0; - if (after == 0 or after == ' ' or after == '\n') { - self.consume(6); - var token = self.tok(.extends, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(6); - self.tokEnd(&token); - - if (!self.path()) { - self.setError(.NO_EXTENDS_PATH, "missing path for extends"); - return true; - } - return true; - } - // "extend" followed by something else (like "(") - malformed - if (after != 0 and after != 's') { - self.setError(.MALFORMED_EXTENDS, "malformed extends"); - return true; - } - } - return false; - } - - fn prepend(self: *Lexer) bool { - return self.blockHelper("prepend", .prepend); - } - - fn append(self: *Lexer) bool { - return self.blockHelper("append", .append); - } - - fn blockToken(self: *Lexer) bool { - return self.blockHelper("block", .replace); - } - - const BlockMode = enum { prepend, append, replace }; - - fn blockHelper(self: *Lexer, keyword: []const u8, mode: BlockMode) bool { - const full_prefix = switch (mode) { - .prepend => "prepend ", - .append => "append ", - .replace => "block ", - }; - const block_prefix = switch (mode) { - .prepend => "block prepend ", - .append => "block append ", - .replace => "block ", - }; - - var name_start: usize = 0; - - if (mem.startsWith(u8, self.input, block_prefix)) { - name_start = block_prefix.len; - } else if (mem.startsWith(u8, self.input, full_prefix)) { - name_start = full_prefix.len; - } else { - _ = keyword; - return false; - } - - // Find end of line - var end = name_start; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - // Extract name (trim and handle comments) - var name_end = end; - // Check for comment - var i = name_start; - while (i < end) { - if (i + 1 < end and self.input[i] == '/' and self.input[i + 1] == '/') { - name_end = i; - break; - } - i += 1; - } - - // Trim whitespace - while (name_end > name_start and isWhitespace(self.input[name_end - 1])) { - name_end -= 1; - } - - if (name_end <= name_start) return false; - - const name = self.input[name_start..name_end]; - self.consume(end); - - var token = self.tokWithString(.block, name); - token.mode = TokenValue.fromString(switch (mode) { - .prepend => "prepend", - .append => "append", - .replace => "replace", - }); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn mixinBlock(self: *Lexer) bool { - if (!mem.startsWith(u8, self.input, "block")) return false; - - // Check if followed by end of line, colon, or only whitespace until newline - var consume_len: usize = 5; - var is_mixin_block = false; - - if (self.input.len == 5 or self.input[5] == '\n' or self.input[5] == ':') { - is_mixin_block = true; - } else if (self.input[5] == ' ' or self.input[5] == '\t') { - // Check if only whitespace until newline - var i: usize = 5; - while (i < self.input.len and (self.input[i] == ' ' or self.input[i] == '\t')) { - i += 1; - } - if (i >= self.input.len or self.input[i] == '\n') { - is_mixin_block = true; - consume_len = i; - } - } - - if (is_mixin_block) { - self.consume(consume_len); - var token = self.tok(.mixin_block, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(consume_len); - self.tokEnd(&token); - return true; - } - - return false; - } - - fn yieldToken(self: *Lexer) bool { - if (!mem.startsWith(u8, self.input, "yield")) return false; - - if (self.input.len == 5 or self.input[5] == '\n' or self.input[5] == ':') { - self.consume(5); - var token = self.tok(.yield, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(5); - self.tokEnd(&token); - return true; - } - - return false; - } - - fn includeToken(self: *Lexer) bool { - if (!mem.startsWith(u8, self.input, "include")) return false; - - const after = if (self.input.len > 7) self.input[7] else 0; - if (after != 0 and after != ' ' and after != ':' and after != '\n') { - // "include" followed by something else (like "(") - malformed - self.setError(.MALFORMED_INCLUDE, "malformed include"); - return true; - } - - self.consume(7); - var token = self.tok(.include, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(7); - self.tokEnd(&token); - - // Parse filters - while (self.filter(true)) {} - - if (!self.path()) { - self.setError(.NO_INCLUDE_PATH, "missing path for include"); - return true; - } - return true; - } - - fn path(self: *Lexer) bool { - // Match /^ ([^\n]+)/ - if (self.input.len == 0 or self.input[0] != ' ') return false; - - var i: usize = 1; - // Skip leading spaces - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - // Trim trailing spaces - var path_end = end; - while (path_end > i and self.input[path_end - 1] == ' ') { - path_end -= 1; - } - - if (path_end <= i) return false; - - const path_val = self.input[i..path_end]; - self.consume(end); - - var token = self.tokWithString(.path, path_val); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn caseToken(self: *Lexer) bool { - // Match /^case +([^\n]+)/ - if (!mem.startsWith(u8, self.input, "case")) return false; - - // Check if followed by word boundary - if (self.input.len > 4 and self.input[4] != ' ' and self.input[4] != '\n') { - return false; - } - - // Check for "case" without expression - if (self.input.len == 4 or self.input[4] == '\n') { - self.consume(4); - self.incrementColumn(4); - self.setError(.NO_CASE_EXPRESSION, "missing expression for case"); - return false; - } - - var i: usize = 5; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // If only spaces after "case", that's also an error - if (i >= self.input.len or self.input[i] == '\n') { - self.consume(i); - self.incrementColumn(i); - self.setError(.NO_CASE_EXPRESSION, "missing expression for case"); - return false; - } - - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - if (end <= i) { - self.setError(.NO_CASE_EXPRESSION, "missing expression for case"); - return false; - } - - const expr = self.input[i..end]; - - // Validate brackets are balanced in the expression - if (!self.validateExpressionBrackets(expr)) { - self.consume(end); - self.incrementColumn(end); - return true; // Error already set - } - - self.consume(end); - - var token = self.tokWithString(.case, expr); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - /// Validates that brackets in an expression are balanced - fn validateExpressionBrackets(self: *Lexer, expr: []const u8) bool { - var bracket_stack = std.ArrayListUnmanaged(u8){}; - defer bracket_stack.deinit(self.allocator); - - var in_string: u8 = 0; - var i: usize = 0; - - while (i < expr.len) { - const c = expr[i]; - if (in_string != 0) { - if (c == in_string and (i == 0 or expr[i - 1] != '\\')) { - in_string = 0; - } - } else { - if (c == '"' or c == '\'' or c == '`') { - in_string = c; - } else if (c == '(' or c == '[' or c == '{') { - bracket_stack.append(self.allocator, c) catch return false; - } else if (c == ')' or c == ']' or c == '}') { - if (bracket_stack.items.len == 0) { - self.setError(.BRACKET_MISMATCH, "Unexpected closing bracket in expression"); - return false; - } - const last_open = bracket_stack.items[bracket_stack.items.len - 1]; - const expected_close: u8 = switch (last_open) { - '(' => ')', - '[' => ']', - '{' => '}', - else => 0, - }; - if (c != expected_close) { - self.setError(.BRACKET_MISMATCH, "Mismatched bracket in expression"); - return false; - } - _ = bracket_stack.pop(); - } - } - i += 1; - } - - if (bracket_stack.items.len > 0) { - self.setError(.NO_END_BRACKET, "Unclosed bracket in expression"); - return false; - } - - return true; - } - - fn when(self: *Lexer) bool { - // Match /^when +([^:\n]+)/ but handle colons inside strings - if (!mem.startsWith(u8, self.input, "when")) return false; - - // Check if followed by word boundary (space, newline, or end) - if (self.input.len > 4 and self.input[4] != ' ' and self.input[4] != '\n') { - return false; - } - - // Check for "when" without expression (just "when" or "when\n") - if (self.input.len == 4 or self.input[4] == '\n') { - self.consume(4); - self.incrementColumn(4); - self.setError(.NO_WHEN_EXPRESSION, "missing expression for when"); - return false; - } - - var i: usize = 5; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // If only spaces after "when", that's also an error - if (i >= self.input.len or self.input[i] == '\n') { - self.consume(i); - self.incrementColumn(i); - self.setError(.NO_WHEN_EXPRESSION, "missing expression for when"); - return false; - } - - // Parse until colon or newline, but handle strings properly - var end = i; - var in_string = false; - var string_char: u8 = 0; - var escape_next = false; - var brace_depth: usize = 0; - - while (end < self.input.len and self.input[end] != '\n') { - const c = self.input[end]; - - if (escape_next) { - escape_next = false; - end += 1; - continue; - } - - if (c == '\\') { - escape_next = true; - end += 1; - continue; - } - - if (in_string) { - if (c == string_char) { - in_string = false; - } - end += 1; - continue; - } - - // Not in string - if (c == '\'' or c == '"' or c == '`') { - in_string = true; - string_char = c; - end += 1; - continue; - } - - // Track braces for object literals like {tim: 'g'} - if (c == '{') { - brace_depth += 1; - end += 1; - continue; - } - if (c == '}') { - if (brace_depth > 0) brace_depth -= 1; - end += 1; - continue; - } - - // Colon outside string and outside braces ends the expression - if (c == ':' and brace_depth == 0) { - break; - } - - end += 1; - } - - if (end <= i) { - self.setError(.NO_WHEN_EXPRESSION, "missing expression for when"); - return false; - } - - const expr = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.when, expr); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn defaultToken(self: *Lexer) bool { - if (!mem.startsWith(u8, self.input, "default")) return false; - - if (self.input.len == 7 or self.input[7] == '\n' or self.input[7] == ':') { - self.consume(7); - var token = self.tok(.default, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(7); - self.tokEnd(&token); - return true; - } - - // Check if "default" is followed by something other than whitespace/newline/colon - // "default foo" should error - if (self.input[7] == ' ') { - // Skip spaces and check if there's content after - var i: usize = 8; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - if (i < self.input.len and self.input[i] != '\n' and self.input[i] != ':') { - self.consume(i); - self.incrementColumn(i); - self.setError(.DEFAULT_WITH_EXPRESSION, "`default` cannot have an expression"); - return true; // Return true to stop advance chain, error is set - } - // Just spaces then newline/colon or end of input is fine - self.consume(7); - var token = self.tok(.default, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(7); - self.tokEnd(&token); - return true; - } - - return false; - } - - fn call(self: *Lexer) bool { - // Match /^\+(\s*)(([-\w]+)|(#\{))/ - if (self.input.len < 2 or self.input[0] != '+') return false; - - var i: usize = 1; - // Skip whitespace - while (i < self.input.len and (self.input[i] == ' ' or self.input[i] == '\t')) { - i += 1; - } - - // Check for interpolated call #{ - if (i + 1 < self.input.len and self.input[i] == '#' and self.input[i + 1] == '{') { - const match = self.bracketExpression(i + 1) catch return false; - const increment = match.end + 1; - self.consume(increment); - - var token = self.tok(.call, .none); - // Store the interpolated expression - use the original slice from input - // Format: #{expression} - we store just the expression part, prefixed with #{ - // The value points to input[i..match.end+1] which includes #{ and } - token.val = TokenValue.fromString(self.original_input[self.original_input.len - self.input.len - increment + i .. self.original_input.len - self.input.len - increment + match.end + 1]); - self.incrementColumn(increment); - token.args = .none; - - // Check for args - if (self.input.len > 0 and self.input[0] == '(') { - if (self.bracketExpression(0)) |args_match| { - self.incrementColumn(1); - self.consume(args_match.end + 1); - token.args = TokenValue.fromString(args_match.src); - } else |_| {} - } - - self.tokens.append(self.allocator, token) catch return false; - self.tokEnd(&token); - return true; - } - - // Simple call - var end = i; - while (end < self.input.len) { - const c = self.input[end]; - if (isWordChar(c) or c == '-') { - end += 1; - } else { - break; - } - } - - if (end == i) return false; - - const name = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.call, name); - self.incrementColumn(end); - token.args = .none; - - // Check for args (not attributes) - if (self.input.len > 0) { - var j: usize = 0; - while (j < self.input.len and self.input[j] == ' ') { - j += 1; - } - if (j < self.input.len and self.input[j] == '(') { - if (self.bracketExpression(j)) |args_match| { - // Check if it looks like args, not attributes - var is_args = true; - var k: usize = 0; - while (k < args_match.src.len and (args_match.src[k] == ' ' or args_match.src[k] == '\t')) { - k += 1; - } - // Check for key= pattern (attributes) - var key_end = k; - while (key_end < args_match.src.len and (isWordChar(args_match.src[key_end]) or args_match.src[key_end] == '-')) { - key_end += 1; - } - if (key_end < args_match.src.len) { - var eq_pos = key_end; - while (eq_pos < args_match.src.len and args_match.src[eq_pos] == ' ') { - eq_pos += 1; - } - if (eq_pos < args_match.src.len and args_match.src[eq_pos] == '=') { - is_args = false; - } - } - - if (is_args) { - self.incrementColumn(j + 1); - self.consume(j + args_match.end + 1); - token.args = TokenValue.fromString(args_match.src); - } - } else |_| {} - } - } - - self.tokens.append(self.allocator, token) catch return false; - self.tokEnd(&token); - return true; - } - - fn mixin(self: *Lexer) bool { - // Match /^mixin +([-\w]+)(?: *\((.*)\))? */ - if (!mem.startsWith(u8, self.input, "mixin ")) return false; - - var i: usize = 6; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // Get mixin name - var name_end = i; - while (name_end < self.input.len) { - const c = self.input[name_end]; - if (isWordChar(c) or c == '-') { - name_end += 1; - } else { - break; - } - } - - if (name_end == i) return false; - - const name = self.input[i..name_end]; - var end = name_end; - - // Skip spaces - while (end < self.input.len and self.input[end] == ' ') { - end += 1; - } - - var args: TokenValue = .none; - - // Check for args - if (end < self.input.len and self.input[end] == '(') { - const bracket_result = self.bracketExpression(end) catch return false; - args = TokenValue.fromString(bracket_result.src); - end = bracket_result.end + 1; - } - - self.consume(end); - - var token = self.tokWithString(.mixin, name); - token.args = args; - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn conditional(self: *Lexer) bool { - // Match /^(if|unless|else if|else)\b([^\n]*)/ - var keyword: []const u8 = undefined; - var token_type: TokenType = undefined; - - if (mem.startsWith(u8, self.input, "else if")) { - keyword = "else if"; - token_type = .else_if; - } else if (mem.startsWith(u8, self.input, "if")) { - keyword = "if"; - token_type = .@"if"; - } else if (mem.startsWith(u8, self.input, "unless")) { - keyword = "unless"; - token_type = .@"if"; // unless becomes if with negated condition - } else if (mem.startsWith(u8, self.input, "else")) { - keyword = "else"; - token_type = .@"else"; - } else { - return false; - } - - // Check word boundary - if (self.input.len > keyword.len) { - const next = self.input[keyword.len]; - if (isWordChar(next)) return false; - } - - const i = keyword.len; - - // Get expression - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - var js = self.input[i..end]; - // Trim - while (js.len > 0 and (js[0] == ' ' or js[0] == '\t')) { - js = js[1..]; - } - while (js.len > 0 and (js[js.len - 1] == ' ' or js[js.len - 1] == '\t')) { - js = js[0 .. js.len - 1]; - } - - self.consume(end); - - var token = self.tokWithString(token_type, if (js.len > 0) js else null); - - // Handle else with condition - if (token_type == .@"else" and js.len > 0) { - self.setError(.ELSE_CONDITION, "`else` cannot have a condition, perhaps you meant `else if`"); - return true; // Return true to stop advance chain, error is set - } - - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn whileToken(self: *Lexer) bool { - // Match /^while +([^\n]+)/ - if (!mem.startsWith(u8, self.input, "while")) return false; - - // Check if followed by word boundary - if (self.input.len > 5 and self.input[5] != ' ' and self.input[5] != '\n') { - return false; - } - - // Check for "while" without expression - if (self.input.len == 5 or self.input[5] == '\n') { - self.consume(5); - self.incrementColumn(5); - self.setError(.NO_WHILE_EXPRESSION, "missing expression for while"); - return false; - } - - var i: usize = 6; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // If only spaces after "while", that's also an error - if (i >= self.input.len or self.input[i] == '\n') { - self.consume(i); - self.incrementColumn(i); - self.setError(.NO_WHILE_EXPRESSION, "missing expression for while"); - return false; - } - - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - if (end <= i) { - self.setError(.NO_WHILE_EXPRESSION, "missing expression for while"); - return false; - } - - const expr = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.@"while", expr); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn each(self: *Lexer) bool { - const is_each = mem.startsWith(u8, self.input, "each "); - const is_for = mem.startsWith(u8, self.input, "for "); - - if (!is_each and !is_for) return false; - - const prefix_len: usize = if (is_each) 5 else 4; - var i = prefix_len; - - // Skip spaces - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // Get first identifier - if (i >= self.input.len or !isIdentStart(self.input[i])) { - return self.eachOf(); - } - - var ident_end = i + 1; - while (ident_end < self.input.len and isIdentChar(self.input[ident_end])) { - ident_end += 1; - } - - const val_name = self.input[i..ident_end]; - i = ident_end; - - // Skip spaces - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - var key_name: TokenValue = .none; - - // Check for , key - if (i < self.input.len and self.input[i] == ',') { - i += 1; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - if (i < self.input.len and isIdentStart(self.input[i])) { - var key_end = i + 1; - while (key_end < self.input.len and isIdentChar(self.input[key_end])) { - key_end += 1; - } - key_name = TokenValue.fromString(self.input[i..key_end]); - i = key_end; - } - } - - // Skip spaces - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // Check for 'in' or 'of' - if (mem.startsWith(u8, self.input[i..], "of ")) { - return self.eachOf(); - } - - if (!mem.startsWith(u8, self.input[i..], "in ")) { - self.setError(.MALFORMED_EACH, "Malformed each statement"); - return false; - } - - i += 3; // skip "in " - - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - // Get expression - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - if (end <= i) { - self.setError(.MALFORMED_EACH, "missing expression for each"); - return false; - } - - const expr = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.each, val_name); - token.key = key_name; - token.code = TokenValue.fromString(expr); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn isIdentStart(c: u8) bool { - return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '_' or c == '$'; - } - - fn isIdentChar(c: u8) bool { - return isIdentStart(c) or (c >= '0' and c <= '9'); - } - - fn eachOf(self: *Lexer) bool { - const is_each = mem.startsWith(u8, self.input, "each "); - const is_for = mem.startsWith(u8, self.input, "for "); - - if (!is_each and !is_for) return false; - - const prefix_len: usize = if (is_each) 5 else 4; - var i = prefix_len; - - // Find " of " - var of_pos: ?usize = null; - var j = i; - while (j + 3 < self.input.len) { - if (self.input[j] == ' ' and self.input[j + 1] == 'o' and self.input[j + 2] == 'f' and self.input[j + 3] == ' ') { - of_pos = j; - break; - } - if (self.input[j] == '\n') break; - j += 1; - } - - if (of_pos == null) return false; - - const value = self.input[i..of_pos.?]; - - i = of_pos.? + 4; // skip " of " - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - if (end <= i) return false; - - const expr = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.each_of, value); - token.code = TokenValue.fromString(expr); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn code(self: *Lexer) bool { - if (self.input.len == 0) return false; - - var flags_end: usize = 0; - var must_escape = false; - var buffer = false; - - if (self.input[0] == '-') { - flags_end = 1; - buffer = false; - } else if (self.input[0] == '=') { - flags_end = 1; - must_escape = true; - buffer = true; - } else if (self.input.len >= 2 and self.input[0] == '!' and self.input[1] == '=') { - flags_end = 2; - must_escape = false; - buffer = true; - } else { - return false; - } - - var i = flags_end; - // Skip spaces/tabs - while (i < self.input.len and (self.input[i] == ' ' or self.input[i] == '\t')) { - i += 1; - } - - // Check for old-style "- each" or "- for" prefixed syntax - if (flags_end == 1 and self.input[0] == '-') { - const rest = self.input[i..]; - // Match: each/for VAR(, VAR)? in EXPR - if (mem.startsWith(u8, rest, "each ") or mem.startsWith(u8, rest, "for ")) { - // Check if it looks like the old prefixed each/for syntax - var j: usize = 0; - if (mem.startsWith(u8, rest, "each ")) { - j = 5; - } else { - j = 4; - } - // Skip whitespace - while (j < rest.len and (rest[j] == ' ' or rest[j] == '\t')) { - j += 1; - } - // Check for identifier - if (j < rest.len and (std.ascii.isAlphabetic(rest[j]) or rest[j] == '_' or rest[j] == '$')) { - // This looks like "- each var in expr" which is old syntax - self.setError(.MALFORMED_EACH, "Pug each and for should no longer be prefixed with a dash (\"-\"). They are pug keywords and not part of JavaScript."); - return true; - } - } - } - - var end = i; - while (end < self.input.len and self.input[end] != '\n') { - end += 1; - } - - const code_val = self.input[i..end]; - self.consume(end); - - var token = self.tokWithString(.code, code_val); - token.must_escape = TokenValue.fromBool(must_escape); - token.buffer = TokenValue.fromBool(buffer); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(end); - self.tokEnd(&token); - return true; - } - - fn blockCode(self: *Lexer) bool { - if (self.input.len == 0 or self.input[0] != '-') return false; - - // Must be followed by end of line - if (self.input.len > 1 and self.input[1] != '\n' and self.input[1] != ':') { - return false; - } - - self.consume(1); - var token = self.tok(.blockcode, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(1); - self.tokEnd(&token); - self.interpolation_allowed = false; - _ = self.pipelessText(null); - return true; - } - - fn attrs(self: *Lexer) bool { - if (self.input.len == 0 or self.input[0] != '(') return false; - - var token = self.tok(.start_attributes, .none); - const bracket_result = self.bracketExpression(0) catch return false; - const str = self.input[1..bracket_result.end]; - - self.incrementColumn(1); - self.tokens.append(self.allocator, token) catch return false; - self.tokEnd(&token); - self.consume(bracket_result.end + 1); - - // Parse attributes from str - self.parseAttributes(str); - - // Check if parseAttributes set an error - if (self.last_error != null) { - return true; // Error is set, return true to stop further parsing - } - - var end_token = self.tok(.end_attributes, .none); - self.incrementColumn(1); - self.tokens.append(self.allocator, end_token) catch return false; - self.tokEnd(&end_token); - return true; - } - - fn parseAttributes(self: *Lexer, str: []const u8) void { - var i: usize = 0; - - while (i < str.len) { - // Skip whitespace - while (i < str.len and isWhitespace(str[i])) { - if (str[i] == '\n') { - self.incrementLine(1); - } else { - self.incrementColumn(1); - } - i += 1; - } - - if (i >= str.len) break; - - var attr_token = self.tok(.attribute, .none); - - // Check for quoted key - var key: []const u8 = undefined; - - if (str[i] == '"' or str[i] == '\'') { - const quote = str[i]; - self.incrementColumn(1); - i += 1; - const key_start = i; - while (i < str.len and str[i] != quote) { - if (str[i] == '\n') { - self.incrementLine(1); - } else { - self.incrementColumn(1); - } - i += 1; - } - key = str[key_start..i]; - if (i < str.len) { - self.incrementColumn(1); - i += 1; - } - } else { - // Unquoted key - const key_start = i; - while (i < str.len and !isWhitespace(str[i]) and str[i] != '!' and str[i] != '=' and str[i] != ',') { - if (str[i] == '\n') { - self.incrementLine(1); - } else { - self.incrementColumn(1); - } - i += 1; - } - key = str[key_start..i]; - } - - attr_token.name = TokenValue.fromString(key); - - // Skip whitespace - while (i < str.len and (str[i] == ' ' or str[i] == '\t')) { - self.incrementColumn(1); - i += 1; - } - - // Check for value - var must_escape = true; - if (i < str.len and str[i] == '!') { - must_escape = false; - self.incrementColumn(1); - i += 1; - } - - if (i < str.len and str[i] == '=') { - self.incrementColumn(1); - i += 1; - - // Skip whitespace (including newlines) - while (i < str.len and isWhitespace(str[i])) { - if (str[i] == '\n') { - self.incrementLine(1); - } else { - self.incrementColumn(1); - } - i += 1; - } - - // Parse value - var state: CharParserState = .{}; - defer state.deinit(self.allocator); - - const val_start = i; - var has_content = false; // Track if we've seen non-whitespace - while (i < str.len) { - const char = str[i]; - state.parseChar(self.allocator, char) catch break; - - if (!isWhitespace(char)) { - has_content = true; - } - - if (!state.isNesting() and !state.isString() and has_content) { - if (isWhitespace(char) or char == ',') { - break; - } - } - - // Check for invalid newline inside single/double quoted string - // (template literals with backticks can have newlines) - if (char == '\n') { - if (state.isString()) { - const quote_char = state.getStringChar(); - if (quote_char) |qc| { - if (qc == '\'' or qc == '"') { - self.setError(.SYNTAX_ERROR, "Invalid newline in string literal"); - return; - } - } - } - } - - if (str[i] == '\n') { - self.incrementLine(1); - } else { - self.incrementColumn(1); - } - i += 1; - } - - attr_token.val = TokenValue.fromString(str[val_start..i]); - attr_token.must_escape = TokenValue.fromBool(must_escape); - } else { - // Boolean attribute - attr_token.val = TokenValue.fromBool(true); - attr_token.must_escape = TokenValue.fromBool(true); - } - - self.tokens.append(self.allocator, attr_token) catch return; - self.tokEnd(&attr_token); - - // Skip whitespace and comma - while (i < str.len and (isWhitespace(str[i]) or str[i] == ',')) { - if (str[i] == '\n') { - self.incrementLine(1); - } else { - self.incrementColumn(1); - } - i += 1; - } - } - } - - fn attributesBlock(self: *Lexer) bool { - if (!mem.startsWith(u8, self.input, "&attributes")) return false; - - if (self.input.len > 11 and isWordChar(self.input[11])) return false; - - self.consume(11); - var token = self.tok(.@"&attributes", .none); - self.incrementColumn(11); - - const args = self.bracketExpression(0) catch return false; - self.consume(args.end + 1); - token.val = TokenValue.fromString(args.src); - self.incrementColumn(args.end + 1); - - self.tokens.append(self.allocator, token) catch return false; - self.tokEnd(&token); - return true; - } - - fn indent(self: *Lexer) bool { - const captures = self.scanIndentation() orelse return false; - - const indents = captures.indent.len; - - self.incrementLine(1); - self.consume(captures.total_len); - - // Blank line - if (self.input.len > 0 and self.input[0] == '\n') { - self.interpolation_allowed = true; - var newline_token = self.tok(.newline, .none); - self.tokEnd(&newline_token); - return true; - } - - // Outdent - if (indents < self.indent_stack.items[0]) { - var outdent_count: usize = 0; - while (self.indent_stack.items[0] > indents) { - if (self.indent_stack.items.len > 1 and self.indent_stack.items[1] < indents) { - self.setError(.INCONSISTENT_INDENTATION, "Inconsistent indentation"); - return false; - } - outdent_count += 1; - _ = self.indent_stack.orderedRemove(0); - } - while (outdent_count > 0) : (outdent_count -= 1) { - self.colno = 1; - var outdent_token = self.tok(.outdent, .none); - self.colno = self.indent_stack.items[0] + 1; - self.tokens.append(self.allocator, outdent_token) catch return false; - self.tokEnd(&outdent_token); - } - } else if (indents > 0 and indents != self.indent_stack.items[0]) { - // Indent - var indent_token = self.tok(.indent, .none); - self.colno = 1 + indents; - self.tokens.append(self.allocator, indent_token) catch return false; - self.tokEnd(&indent_token); - self.indent_stack.insert(self.allocator, 0, indents) catch return false; - } else { - // Newline - var newline_token = self.tok(.newline, .none); - self.colno = 1 + @min(self.indent_stack.items[0], indents); - self.tokens.append(self.allocator, newline_token) catch return false; - self.tokEnd(&newline_token); - } - - self.interpolation_allowed = true; - return true; - } - - fn pipelessText(self: *Lexer, forced_indents: ?usize) bool { - while (self.blank()) {} - - const captures = self.scanIndentation() orelse return false; - const indents = forced_indents orelse captures.indent.len; - - if (indents <= self.indent_stack.items[0]) return false; - - var start_token = self.tok(.start_pipeless_text, .none); - self.tokEnd(&start_token); - self.tokens.append(self.allocator, start_token) catch return false; - - var string_ptr: usize = 0; - var tokens_list: std.ArrayListUnmanaged([]const u8) = .{}; - var token_indent_list: std.ArrayListUnmanaged(bool) = .{}; - defer tokens_list.deinit(self.allocator); - defer token_indent_list.deinit(self.allocator); - - while (string_ptr < self.input.len) { - // text has `\n` as a prefix - const line_start = string_ptr + 1; // skip the \n - if (string_ptr >= self.input.len or self.input[string_ptr] != '\n') { - break; - } - - // Find end of line - var line_end = line_start; - while (line_end < self.input.len and self.input[line_end] != '\n') { - line_end += 1; - } - - const str = self.input[line_start..line_end]; - - // Check indentation of this line (count leading whitespace) - var line_indent: usize = 0; - for (str) |c| { - if (c == ' ' or c == '\t') { - line_indent += 1; - } else { - break; - } - } - - const is_match = line_indent >= indents; - token_indent_list.append(self.allocator, is_match) catch return false; - - // Match if indented enough OR if line is empty/whitespace - const trimmed = mem.trim(u8, str, " \t"); - if (is_match or trimmed.len == 0) { - // consume line along with `\n` prefix - string_ptr = line_end; - // Extract text after the indent - const text_content = if (str.len > indents) str[indents..] else ""; - tokens_list.append(self.allocator, text_content) catch return false; - } else if (line_indent > self.indent_stack.items[0]) { - // line is indented less than the first line but is still indented - // need to retry lexing the text block with new indent level - _ = self.tokens.pop(); - return self.pipelessText(line_indent); - } else { - break; - } - } - - self.consume(string_ptr); - - // Remove trailing empty lines when input is exhausted - while (self.input.len == 0 and tokens_list.items.len > 0 and tokens_list.items[tokens_list.items.len - 1].len == 0) { - _ = tokens_list.pop(); - } - - for (tokens_list.items, 0..) |token_text, ii| { - self.incrementLine(1); - if (ii != 0) { - var newline_token = self.tok(.newline, .none); - self.tokens.append(self.allocator, newline_token) catch return false; - self.tokEnd(&newline_token); - } - if (ii < token_indent_list.items.len and token_indent_list.items[ii]) { - self.incrementColumn(indents); - } - self.addText(.text, token_text, "", 0); - } - - var end_token = self.tok(.end_pipeless_text, .none); - self.tokEnd(&end_token); - self.tokens.append(self.allocator, end_token) catch return false; - return true; - } - - fn slash(self: *Lexer) bool { - if (self.input.len == 0 or self.input[0] != '/') return false; - - self.consume(1); - var token = self.tok(.slash, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(1); - self.tokEnd(&token); - return true; - } - - fn colon(self: *Lexer) bool { - if (self.input.len < 2 or self.input[0] != ':' or self.input[1] != ' ') return false; - - var i: usize = 2; - while (i < self.input.len and self.input[i] == ' ') { - i += 1; - } - - self.consume(i); - var token = self.tok(.colon, .none); - self.tokens.append(self.allocator, token) catch return false; - self.incrementColumn(i); - self.tokEnd(&token); - return true; - } - - fn fail(self: *Lexer) void { - self.setError(.UNEXPECTED_TEXT, "unexpected text"); - } - - fn addText(self: *Lexer, token_type: TokenType, value: []const u8, prefix: []const u8, escaped: usize) void { - if (value.len + prefix.len == 0) return; - - // Check for unclosed or mismatched tag interpolations #[...] - // Note: Inside #[...] is full Pug syntax, so we need to track ALL bracket types - if (self.interpolation_allowed) { - var i: usize = 0; - while (i + 1 < value.len) { - // Skip escaped \#[ - if (value[i] == '\\' and i + 2 < value.len and value[i + 1] == '#' and value[i + 2] == '[') { - i += 3; - continue; - } - if (value[i] == '#' and value[i + 1] == '[') { - // Found start of tag interpolation, look for matching ] - var j = i + 2; - var in_string: u8 = 0; - - // Track bracket stack - inside #[...] you can have (...) and {...} for attrs/code - var bracket_stack = std.ArrayListUnmanaged(u8){}; - defer bracket_stack.deinit(self.allocator); - bracket_stack.append(self.allocator, '[') catch return; - - while (j < value.len and bracket_stack.items.len > 0) { - const c = value[j]; - if (in_string != 0) { - if (c == in_string and (j == i + 2 or value[j - 1] != '\\')) { - in_string = 0; - } - } else { - if (c == '"' or c == '\'' or c == '`') { - in_string = c; - } else if (c == '[' or c == '(' or c == '{') { - bracket_stack.append(self.allocator, c) catch return; - } else if (c == ']' or c == ')' or c == '}') { - if (bracket_stack.items.len > 0) { - const last_open = bracket_stack.items[bracket_stack.items.len - 1]; - const expected_close: u8 = switch (last_open) { - '[' => ']', - '(' => ')', - '{' => '}', - else => 0, - }; - if (c != expected_close) { - // Mismatched bracket type - self.setError(.BRACKET_MISMATCH, "Mismatched bracket in tag interpolation"); - return; - } - _ = bracket_stack.pop(); - } - } - } - j += 1; - } - if (bracket_stack.items.len > 0) { - // Unclosed interpolation - self.setError(.NO_END_BRACKET, "Unclosed tag interpolation - missing ]"); - return; - } - i = j; - } else { - i += 1; - } - } - } - - var token = self.tokWithString(token_type, value); - self.incrementColumn(value.len + escaped); - self.tokens.append(self.allocator, token) catch return; - self.tokEnd(&token); - } - - // ======================================================================== - // Main advance and getTokens - // ======================================================================== - - fn advance(self: *Lexer) bool { - return self.blank() or - self.eos() or - self.endInterpolation() or - self.yieldToken() or - self.doctype() or - self.interpolation() or - self.caseToken() or - self.when() or - self.defaultToken() or - self.extendsToken() or - self.append() or - self.prepend() or - self.blockToken() or - self.mixinBlock() or - self.includeToken() or - self.mixin() or - self.call() or - self.conditional() or - self.eachOf() or - self.each() or - self.whileToken() or - self.tag() or - self.filter(false) or - self.blockCode() or - self.code() or - self.id() or - self.dot() or - self.className() or - self.attrs() or - self.attributesBlock() or - self.indent() or - self.text() or - self.textHtml() or - self.comment() or - self.slash() or - self.colon() or - blk: { - self.fail(); - break :blk false; - }; - } - - pub fn getTokens(self: *Lexer) ![]Token { - while (!self.ended) { - const advanced = self.advance(); - // Check for errors after every advance, regardless of return value - if (self.last_error) |err| { - std.debug.print("Lexer error at {d}:{d}: {s}\n", .{ err.line, err.column, err.message }); - return error.LexerError; - } - if (!advanced) { - break; - } - } - return self.tokens.items; - } -}; - -// ============================================================================ -// Options -// ============================================================================ - -pub const LexerOptions = struct { - filename: ?[]const u8 = null, - interpolated: bool = false, - starting_line: usize = 1, - starting_column: usize = 1, -}; - -// ============================================================================ -// Public API -// ============================================================================ - -/// Lexes the input string and returns a slice of tokens. -/// IMPORTANT: The caller must keep the Lexer alive while using the returned tokens, -/// as token string values are slices into the lexer's input buffer. -/// For simpler usage, use Lexer.init() and Lexer.getTokens() directly. -pub fn lex(allocator: Allocator, str: []const u8, options: LexerOptions) !struct { tokens: []Token, lexer: *Lexer } { - const lexer = try allocator.create(Lexer); - lexer.* = try Lexer.init(allocator, str, options); - const tokens = try lexer.getTokens(); - return .{ .tokens = tokens, .lexer = lexer }; -} - -/// Frees resources from a lex() call -pub fn freeLexResult(allocator: Allocator, lexer: *Lexer) void { - lexer.deinit(); - allocator.destroy(lexer); -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "TokenValue - none" { - const val: TokenValue = .none; - try std.testing.expect(val.isNone()); - try std.testing.expect(val.getString() == null); - try std.testing.expect(val.getBool() == null); -} - -test "TokenValue - string" { - const val = TokenValue.fromString("hello"); - try std.testing.expect(!val.isNone()); - try std.testing.expectEqualStrings("hello", val.getString().?); - try std.testing.expect(val.getBool() == null); -} - -test "TokenValue - boolean" { - const val_true = TokenValue.fromBool(true); - const val_false = TokenValue.fromBool(false); - - try std.testing.expect(!val_true.isNone()); - try std.testing.expect(val_true.getBool().? == true); - try std.testing.expect(val_true.getString() == null); - - try std.testing.expect(val_false.getBool().? == false); -} - -test "basic tag lexing" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "div", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 2); - try std.testing.expectEqual(TokenType.tag, tokens[0].type); - try std.testing.expectEqualStrings("div", tokens[0].getVal().?); -} - -test "tag with id" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "div#main", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 3); - try std.testing.expectEqual(TokenType.tag, tokens[0].type); - try std.testing.expectEqual(TokenType.id, tokens[1].type); - try std.testing.expectEqualStrings("main", tokens[1].getVal().?); -} - -test "tag with class" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "div.container", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 3); - try std.testing.expectEqual(TokenType.tag, tokens[0].type); - try std.testing.expectEqual(TokenType.class, tokens[1].type); - try std.testing.expectEqualStrings("container", tokens[1].getVal().?); -} - -test "doctype" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "doctype html", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 2); - try std.testing.expectEqual(TokenType.doctype, tokens[0].type); - try std.testing.expectEqualStrings("html", tokens[0].getVal().?); -} - -test "comment with buffer" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "// this is a comment", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 2); - try std.testing.expectEqual(TokenType.comment, tokens[0].type); - try std.testing.expect(tokens[0].isBuffered() == true); -} - -test "comment without buffer" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "//- this is a silent comment", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 2); - try std.testing.expectEqual(TokenType.comment, tokens[0].type); - try std.testing.expect(tokens[0].isBuffered() == false); -} - -test "code with escape" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "= foo", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 2); - try std.testing.expectEqual(TokenType.code, tokens[0].type); - try std.testing.expect(tokens[0].shouldEscape() == true); - try std.testing.expect(tokens[0].isBuffered() == true); -} - -test "code without escape" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "!= foo", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - try std.testing.expect(tokens.len >= 2); - try std.testing.expectEqual(TokenType.code, tokens[0].type); - try std.testing.expect(tokens[0].shouldEscape() == false); - try std.testing.expect(tokens[0].isBuffered() == true); -} - -test "boolean attribute" { - const allocator = std.testing.allocator; - var lexer = try Lexer.init(allocator, "input(disabled)", .{}); - defer lexer.deinit(); - - const tokens = try lexer.getTokens(); - - // Find the attribute token - var attr_found = false; - for (tokens) |tok| { - if (tok.type == .attribute) { - attr_found = true; - try std.testing.expectEqualStrings("disabled", tok.getName().?); - // Boolean attribute should have boolean true value - try std.testing.expect(tok.val.getBool().? == true); - break; - } - } - try std.testing.expect(attr_found); -} diff --git a/src/v1/linker.zig b/src/v1/linker.zig deleted file mode 100644 index 969d344..0000000 --- a/src/v1/linker.zig +++ /dev/null @@ -1,699 +0,0 @@ -// linker.zig - Zig port of pug-linker -// -// Handles template inheritance and linking: -// - Resolves extends (parent template inheritance) -// - Handles named blocks (replace/append/prepend modes) -// - Processes includes with yield blocks -// - Manages mixin hoisting from child to parent - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const mem = std.mem; - -// Import AST types from parser -const parser = @import("parser.zig"); -pub const Node = parser.Node; -pub const NodeType = parser.NodeType; - -// Import walk module -const walk_mod = @import("walk.zig"); -pub const WalkOptions = walk_mod.WalkOptions; -pub const WalkContext = walk_mod.WalkContext; -pub const WalkError = walk_mod.WalkError; -pub const ReplaceResult = walk_mod.ReplaceResult; - -// Import error types -const pug_error = @import("error.zig"); -pub const PugError = pug_error.PugError; - -// ============================================================================ -// Linker Errors -// ============================================================================ - -pub const LinkerError = error{ - OutOfMemory, - InvalidAST, - ExtendsNotFirst, - UnexpectedNodesInExtending, - UnexpectedBlock, - WalkError, -}; - -// ============================================================================ -// Block Definitions Map -// ============================================================================ - -/// Map of block names to their definition nodes -pub const BlockDefinitions = std.StringHashMapUnmanaged(*Node); - -// ============================================================================ -// Linker Result -// ============================================================================ - -pub const LinkerResult = struct { - ast: *Node, - declared_blocks: BlockDefinitions, - has_extends: bool = false, - err: ?PugError = null, - - pub fn deinit(self: *LinkerResult, allocator: Allocator) void { - self.declared_blocks.deinit(allocator); - if (self.err) |*e| { - e.deinit(); - } - } -}; - -// ============================================================================ -// Link Implementation -// ============================================================================ - -/// Link an AST, resolving extends and includes -pub fn link(allocator: Allocator, ast: *Node) LinkerError!LinkerResult { - // Top level must be a Block - if (ast.type != .Block) { - return error.InvalidAST; - } - - var result = LinkerResult{ - .ast = ast, - .declared_blocks = .{}, - }; - - // Check for extends - var extends_node: ?*Node = null; - if (ast.nodes.items.len > 0) { - const first_node = ast.nodes.items[0]; - if (first_node.type == .Extends) { - // Verify extends position - try checkExtendsPosition(allocator, ast); - - // Remove extends node from the list - extends_node = ast.nodes.orderedRemove(0); - } - } - - // Apply includes (convert RawInclude to Text, link Include ASTs) - result.ast = try applyIncludes(allocator, ast); - - // Find declared blocks - result.declared_blocks = try findDeclaredBlocks(allocator, result.ast); - - // Handle extends - if (extends_node) |ext_node| { - // Get mixins and expected blocks from current template - var mixins = std.ArrayListUnmanaged(*Node){}; - defer mixins.deinit(allocator); - - var expected_blocks = std.ArrayListUnmanaged(*Node){}; - defer expected_blocks.deinit(allocator); - - try collectMixinsAndBlocks(allocator, result.ast, &mixins, &expected_blocks); - - // Link the parent template - if (ext_node.file) |file| { - _ = file; - // In a real implementation, we would: - // 1. Get file.ast (the loaded parent AST) - // 2. Recursively link it - // 3. Extend parent blocks with child blocks - // 4. Verify all expected blocks exist - // 5. Merge mixin definitions - - // For now, mark that we have extends - result.has_extends = true; - } - } - - return result; -} - -/// Find all declared blocks (NamedBlock with mode="replace") -fn findDeclaredBlocks(allocator: Allocator, ast: *Node) LinkerError!BlockDefinitions { - var definitions = BlockDefinitions{}; - - const FindContext = struct { - defs: *BlockDefinitions, - alloc: Allocator, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - - if (node.type == .NamedBlock) { - // Check mode - default is "replace" - const mode = node.mode orelse "replace"; - if (mem.eql(u8, mode, "replace")) { - if (node.name) |name| { - self.defs.put(self.alloc, name, node) catch return error.OutOfMemory; - } - } - } - return null; - } - }; - - var find_ctx = FindContext{ - .defs = &definitions, - .alloc = allocator, - }; - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - _ = walk_mod.walkASTWithUserData( - allocator, - ast, - FindContext.before, - null, - &walk_options, - &find_ctx, - ) catch { - return error.WalkError; - }; - - return definitions; -} - -/// Collect mixin definitions and named blocks from the AST -fn collectMixinsAndBlocks( - allocator: Allocator, - ast: *Node, - mixins: *std.ArrayListUnmanaged(*Node), - expected_blocks: *std.ArrayListUnmanaged(*Node), -) LinkerError!void { - for (ast.nodes.items) |node| { - switch (node.type) { - .NamedBlock => { - try expected_blocks.append(allocator, node); - }, - .Block => { - // Recurse into nested blocks - try collectMixinsAndBlocks(allocator, node, mixins, expected_blocks); - }, - .Mixin => { - // Only collect mixin definitions (not calls) - if (!node.call) { - try mixins.append(allocator, node); - } - }, - else => { - // In extending template, only named blocks and mixins allowed at top level - // This would be an error in strict mode - }, - } - } -} - -/// Extend parent blocks with child block content -fn extendBlocks( - allocator: Allocator, - parent_blocks: *BlockDefinitions, - child_ast: *Node, -) LinkerError!void { - const ExtendContext = struct { - parent: *BlockDefinitions, - stack: std.StringHashMapUnmanaged(void), - alloc: Allocator, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - - if (node.type == .NamedBlock) { - if (node.name) |name| { - // Check for circular reference - if (self.stack.contains(name)) { - return null; // Skip to avoid infinite loop - } - - self.stack.put(self.alloc, name, {}) catch return error.OutOfMemory; - - // Find parent block - if (self.parent.get(name)) |parent_block| { - const mode = node.mode orelse "replace"; - - if (mem.eql(u8, mode, "append")) { - // Append child nodes to parent - for (node.nodes.items) |child_node| { - parent_block.nodes.append(self.alloc, child_node) catch return error.OutOfMemory; - } - } else if (mem.eql(u8, mode, "prepend")) { - // Prepend child nodes to parent - for (node.nodes.items, 0..) |child_node, i| { - parent_block.nodes.insert(self.alloc, i, child_node) catch return error.OutOfMemory; - } - } else { - // Replace - clear parent and add child nodes - parent_block.nodes.clearRetainingCapacity(); - for (node.nodes.items) |child_node| { - parent_block.nodes.append(self.alloc, child_node) catch return error.OutOfMemory; - } - } - } - } - } - return null; - } - - fn after(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - - if (node.type == .NamedBlock) { - if (node.name) |name| { - _ = self.stack.remove(name); - } - } - return null; - } - }; - - var extend_ctx = ExtendContext{ - .parent = parent_blocks, - .stack = .{}, - .alloc = allocator, - }; - defer extend_ctx.stack.deinit(allocator); - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - _ = walk_mod.walkASTWithUserData( - allocator, - child_ast, - ExtendContext.before, - ExtendContext.after, - &walk_options, - &extend_ctx, - ) catch { - return error.WalkError; - }; -} - -/// Apply includes - convert RawInclude to Text, process Include nodes -fn applyIncludes(allocator: Allocator, ast: *Node) LinkerError!*Node { - const IncludeContext = struct { - alloc: Allocator, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - _ = self; - - // Convert RawInclude to Text - if (node.type == .RawInclude) { - // In a real implementation: - // - Get file.str (the loaded file content) - // - Create a Text node with that content - // For now, just keep the node as-is - node.type = .Text; - // node.val = file.str with \r removed - } - return null; - } - - fn after(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - _ = self; - - // Process Include nodes - if (node.type == .Include) { - // In a real implementation: - // 1. Link the included file's AST - // 2. If it has extends, remove named blocks - // 3. Apply yield block - // For now, keep the node as-is - } - return null; - } - }; - - var include_ctx = IncludeContext{ - .alloc = allocator, - }; - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - const result = walk_mod.walkASTWithUserData( - allocator, - ast, - IncludeContext.before, - IncludeContext.after, - &walk_options, - &include_ctx, - ) catch { - return error.WalkError; - }; - - return result; -} - -/// Check that extends is the first thing in the file -fn checkExtendsPosition(allocator: Allocator, ast: *Node) LinkerError!void { - var found_legit_extends = false; - - const CheckContext = struct { - legit_extends: *bool, - has_extends: bool, - alloc: Allocator, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - - if (node.type == .Extends) { - if (self.has_extends and !self.legit_extends.*) { - self.legit_extends.* = true; - } else { - // This would be an error - extends not first or multiple extends - // For now we just skip - } - } - return null; - } - }; - - var check_ctx = CheckContext{ - .legit_extends = &found_legit_extends, - .has_extends = true, - .alloc = allocator, - }; - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - _ = walk_mod.walkASTWithUserData( - allocator, - ast, - CheckContext.before, - null, - &walk_options, - &check_ctx, - ) catch { - return error.WalkError; - }; -} - -/// Remove named blocks (convert to regular blocks) -pub fn removeNamedBlocks(allocator: Allocator, ast: *Node) LinkerError!*Node { - const RemoveContext = struct { - alloc: Allocator, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - _ = self; - - if (node.type == .NamedBlock) { - node.type = .Block; - node.name = null; - node.mode = null; - } - return null; - } - }; - - var remove_ctx = RemoveContext{ - .alloc = allocator, - }; - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - return walk_mod.walkASTWithUserData( - allocator, - ast, - RemoveContext.before, - null, - &walk_options, - &remove_ctx, - ) catch error.WalkError; -} - -/// Apply yield block to included content -pub fn applyYield(allocator: Allocator, ast: *Node, block: ?*Node) LinkerError!*Node { - if (block == null or block.?.nodes.items.len == 0) { - return ast; - } - - var replaced = false; - - const YieldContext = struct { - yield_block: *Node, - was_replaced: *bool, - alloc: Allocator, - - fn after(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - - if (node.type == .YieldBlock) { - self.was_replaced.* = true; - node.type = .Block; - node.nodes.clearRetainingCapacity(); - node.nodes.append(self.alloc, self.yield_block) catch return error.OutOfMemory; - } - return null; - } - }; - - var yield_ctx = YieldContext{ - .yield_block = block.?, - .was_replaced = &replaced, - .alloc = allocator, - }; - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - const result = walk_mod.walkASTWithUserData( - allocator, - ast, - null, - YieldContext.after, - &walk_options, - &yield_ctx, - ) catch { - return error.WalkError; - }; - - // If no yield block found, append to default location - if (!replaced) { - const default_loc = findDefaultYieldLocation(result); - default_loc.nodes.append(allocator, block.?) catch return error.OutOfMemory; - } - - return result; -} - -/// Find the default yield location (deepest block) -fn findDefaultYieldLocation(node: *Node) *Node { - var result = node; - - for (node.nodes.items) |child| { - if (child.text_only) continue; - - if (child.type == .Block) { - result = findDefaultYieldLocation(child); - } else if (child.nodes.items.len > 0) { - result = findDefaultYieldLocation(child); - } - } - - return result; -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "link - basic block" { - const allocator = std.testing.allocator; - - // Create a simple AST - const text_node = try allocator.create(Node); - text_node.* = Node{ - .type = .Text, - .val = "Hello", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, text_node); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - var result = try link(allocator, root); - defer result.deinit(allocator); - - try std.testing.expectEqual(root, result.ast); - try std.testing.expectEqual(false, result.has_extends); -} - -test "link - with named block" { - const allocator = std.testing.allocator; - - // Create named block - const text_node = try allocator.create(Node); - text_node.* = Node{ - .type = .Text, - .val = "content", - .line = 2, - .column = 3, - }; - - const named_block = try allocator.create(Node); - named_block.* = Node{ - .type = .NamedBlock, - .name = "content", - .mode = "replace", - .line = 2, - .column = 1, - }; - try named_block.nodes.append(allocator, text_node); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, named_block); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - var result = try link(allocator, root); - defer result.deinit(allocator); - - // Should find the declared block - try std.testing.expect(result.declared_blocks.contains("content")); -} - -test "findDeclaredBlocks - multiple blocks" { - const allocator = std.testing.allocator; - - const block1 = try allocator.create(Node); - block1.* = Node{ - .type = .NamedBlock, - .name = "header", - .mode = "replace", - .line = 1, - .column = 1, - }; - - const block2 = try allocator.create(Node); - block2.* = Node{ - .type = .NamedBlock, - .name = "footer", - .mode = "replace", - .line = 5, - .column = 1, - }; - - const block3 = try allocator.create(Node); - block3.* = Node{ - .type = .NamedBlock, - .name = "sidebar", - .mode = "append", // Should not be in declared blocks - .line = 10, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, block1); - try root.nodes.append(allocator, block2); - try root.nodes.append(allocator, block3); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - var blocks = try findDeclaredBlocks(allocator, root); - defer blocks.deinit(allocator); - - try std.testing.expect(blocks.contains("header")); - try std.testing.expect(blocks.contains("footer")); - try std.testing.expect(!blocks.contains("sidebar")); // append mode -} - -test "removeNamedBlocks" { - const allocator = std.testing.allocator; - - const named_block = try allocator.create(Node); - named_block.* = Node{ - .type = .NamedBlock, - .name = "content", - .mode = "replace", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, named_block); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const result = try removeNamedBlocks(allocator, root); - - // Named block should now be a regular Block - try std.testing.expectEqual(NodeType.Block, result.nodes.items[0].type); - try std.testing.expectEqual(@as(?[]const u8, null), result.nodes.items[0].name); -} - -test "findDefaultYieldLocation - nested blocks" { - const allocator = std.testing.allocator; - - const inner_block = try allocator.create(Node); - inner_block.* = Node{ - .type = .Block, - .line = 3, - .column = 1, - }; - - const outer_block = try allocator.create(Node); - outer_block.* = Node{ - .type = .Block, - .line = 2, - .column = 1, - }; - try outer_block.nodes.append(allocator, inner_block); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, outer_block); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - const location = findDefaultYieldLocation(root); - - // Should find the innermost block - try std.testing.expectEqual(inner_block, location); -} diff --git a/src/v1/load.zig b/src/v1/load.zig deleted file mode 100644 index 48cd449..0000000 --- a/src/v1/load.zig +++ /dev/null @@ -1,412 +0,0 @@ -// load.zig - Zig port of pug-load -// -// Handles loading of include/extends files during AST processing. -// Walks the AST and loads file dependencies. - -const std = @import("std"); -const fs = std.fs; -const Allocator = std.mem.Allocator; -const mem = std.mem; - -// Import AST types from parser -const parser = @import("parser.zig"); -pub const Node = parser.Node; -pub const NodeType = parser.NodeType; -pub const FileReference = parser.FileReference; - -// Import walk module -const walk_mod = @import("walk.zig"); -pub const walkAST = walk_mod.walkAST; -pub const WalkOptions = walk_mod.WalkOptions; -pub const WalkContext = walk_mod.WalkContext; -pub const WalkError = walk_mod.WalkError; -pub const ReplaceResult = walk_mod.ReplaceResult; - -// Import lexer for lexing includes -const lexer = @import("lexer.zig"); -pub const Token = lexer.Token; -pub const Lexer = lexer.Lexer; - -// Import error types -const pug_error = @import("error.zig"); -pub const PugError = pug_error.PugError; - -// ============================================================================ -// Load Options -// ============================================================================ - -/// Function type for resolving file paths -pub const ResolveFn = *const fn ( - filename: []const u8, - source: ?[]const u8, - options: *const LoadOptions, -) LoadError![]const u8; - -/// Function type for reading file contents -pub const ReadFn = *const fn ( - allocator: Allocator, - filename: []const u8, - options: *const LoadOptions, -) LoadError![]const u8; - -/// Function type for lexing source -pub const LexFn = *const fn ( - allocator: Allocator, - src: []const u8, - options: *const LoadOptions, -) LoadError![]const Token; - -/// Function type for parsing tokens -pub const ParseFn = *const fn ( - allocator: Allocator, - tokens: []const Token, - options: *const LoadOptions, -) LoadError!*Node; - -pub const LoadOptions = struct { - /// Base directory for absolute paths - basedir: ?[]const u8 = null, - /// Source filename - filename: ?[]const u8 = null, - /// Source content - src: ?[]const u8 = null, - /// Path resolution function - resolve: ?ResolveFn = null, - /// File reading function - read: ?ReadFn = null, - /// Lexer function - lex: ?LexFn = null, - /// Parser function - parse: ?ParseFn = null, - /// User data for callbacks - user_data: ?*anyopaque = null, -}; - -// ============================================================================ -// Load Errors -// ============================================================================ - -pub const LoadError = error{ - OutOfMemory, - FileNotFound, - AccessDenied, - InvalidPath, - MissingFilename, - MissingBasedir, - InvalidFileReference, - LexError, - ParseError, - WalkError, - InvalidUtf8, -}; - -// ============================================================================ -// Load Result -// ============================================================================ - -pub const LoadResult = struct { - ast: *Node, - err: ?PugError = null, - - pub fn deinit(self: *LoadResult, allocator: Allocator) void { - if (self.err) |*e| { - e.deinit(); - } - self.ast.deinit(allocator); - allocator.destroy(self.ast); - } -}; - -// ============================================================================ -// Default Implementations -// ============================================================================ - -/// Default path resolution - handles relative and absolute paths -pub fn defaultResolve( - filename: []const u8, - source: ?[]const u8, - options: *const LoadOptions, -) LoadError![]const u8 { - const trimmed = mem.trim(u8, filename, " \t\r\n"); - - if (trimmed.len == 0) { - return error.InvalidPath; - } - - // Absolute path (starts with /) - if (trimmed[0] == '/') { - if (options.basedir == null) { - return error.MissingBasedir; - } - // Join basedir with filename (without leading /) - // Note: In a real implementation, we'd use path.join - // For now, return the path as-is for testing - return trimmed; - } - - // Relative path - if (source == null) { - return error.MissingFilename; - } - - // In a real implementation, join dirname(source) with filename - // For now, return the path as-is for testing - return trimmed; -} - -/// Default file reading using std.fs -pub fn defaultRead( - allocator: Allocator, - filename: []const u8, - options: *const LoadOptions, -) LoadError![]const u8 { - _ = options; - - const file = fs.cwd().openFile(filename, .{}) catch |err| { - return switch (err) { - error.FileNotFound => error.FileNotFound, - error.AccessDenied => error.AccessDenied, - else => error.FileNotFound, - }; - }; - defer file.close(); - - const content = file.readToEndAlloc(allocator, 1024 * 1024 * 10) catch { - return error.OutOfMemory; - }; - - return content; -} - -// ============================================================================ -// Load Implementation -// ============================================================================ - -/// Load file dependencies from an AST -/// Walks the AST and loads Include, RawInclude, and Extends nodes -pub fn load( - allocator: Allocator, - ast: *Node, - options: LoadOptions, -) LoadError!*Node { - // Create a context for the walk - const LoadContext = struct { - allocator: Allocator, - options: LoadOptions, - err: ?PugError = null, - - fn beforeCallback(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - - // Only process Include, RawInclude, and Extends nodes - if (node.type != .Include and node.type != .RawInclude and node.type != .Extends) { - return null; - } - - // Check if already loaded (str is set) - if (node.file) |*file| { - // Load the file content - self.loadFileReference(file, node) catch { - // Store error but continue walking - return null; - }; - } - - return null; - } - - fn loadFileReference(self: *@This(), file: *FileReference, node: *Node) LoadError!void { - _ = node; - - if (file.path == null) { - return error.InvalidFileReference; - } - - // Resolve the path - const resolve_fn = self.options.resolve orelse defaultResolve; - const resolved_path = try resolve_fn(file.path.?, self.options.filename, &self.options); - - // Read the file - const read_fn = self.options.read orelse defaultRead; - const content = try read_fn(self.allocator, resolved_path, &self.options); - _ = content; - - // For Include/Extends, parse the content into an AST - // This would require lexer and parser functions to be provided - // For now, we just load the raw content - } - }; - - var load_ctx = LoadContext{ - .allocator = allocator, - .options = options, - }; - - var walk_options = WalkOptions{}; - defer walk_options.deinit(allocator); - - const result = walk_mod.walkASTWithUserData( - allocator, - ast, - LoadContext.beforeCallback, - null, - &walk_options, - &load_ctx, - ) catch { - return error.WalkError; - }; - - if (load_ctx.err) |*e| { - e.deinit(); - return error.FileNotFound; - } - - return result; -} - -/// Load from a string source -pub fn loadString( - allocator: Allocator, - src: []const u8, - options: LoadOptions, -) LoadError!*Node { - // Need lex and parse functions - const lex_fn = options.lex orelse return error.LexError; - const parse_fn = options.parse orelse return error.ParseError; - - // Lex the source - const tokens = try lex_fn(allocator, src, &options); - - // Parse the tokens - var parse_options = options; - parse_options.src = src; - const ast = try parse_fn(allocator, tokens, &parse_options); - - // Load dependencies - return load(allocator, ast, parse_options); -} - -/// Load from a file -pub fn loadFile( - allocator: Allocator, - filename: []const u8, - options: LoadOptions, -) LoadError!*Node { - // Read the file - const read_fn = options.read orelse defaultRead; - const content = try read_fn(allocator, filename, &options); - defer allocator.free(content); - - // Load from string with filename set - var file_options = options; - file_options.filename = filename; - return loadString(allocator, content, file_options); -} - -// ============================================================================ -// Path Utilities -// ============================================================================ - -/// Get the directory name from a path -pub fn dirname(path: []const u8) []const u8 { - if (mem.lastIndexOf(u8, path, "/")) |idx| { - if (idx == 0) return "/"; - return path[0..idx]; - } - return "."; -} - -/// Join two path components -pub fn pathJoin(allocator: Allocator, base: []const u8, relative: []const u8) ![]const u8 { - if (relative.len > 0 and relative[0] == '/') { - return allocator.dupe(u8, relative); - } - - const base_dir = dirname(base); - - // Handle .. and . components - var result = std.ArrayListUnmanaged(u8){}; - errdefer result.deinit(allocator); - - try result.appendSlice(allocator, base_dir); - if (base_dir.len > 0 and base_dir[base_dir.len - 1] != '/') { - try result.append(allocator, '/'); - } - try result.appendSlice(allocator, relative); - - return result.toOwnedSlice(allocator); -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "dirname - basic paths" { - try std.testing.expectEqualStrings(".", dirname("file.pug")); - try std.testing.expectEqualStrings("/home/user", dirname("/home/user/file.pug")); - try std.testing.expectEqualStrings("views", dirname("views/file.pug")); - try std.testing.expectEqualStrings("/", dirname("/file.pug")); - try std.testing.expectEqualStrings(".", dirname("")); -} - -test "pathJoin - relative paths" { - const allocator = std.testing.allocator; - - const result1 = try pathJoin(allocator, "/home/user/views/index.pug", "partials/header.pug"); - defer allocator.free(result1); - try std.testing.expectEqualStrings("/home/user/views/partials/header.pug", result1); - - const result2 = try pathJoin(allocator, "views/index.pug", "footer.pug"); - defer allocator.free(result2); - try std.testing.expectEqualStrings("views/footer.pug", result2); -} - -test "pathJoin - absolute paths" { - const allocator = std.testing.allocator; - - const result = try pathJoin(allocator, "/home/user/views/index.pug", "/absolute/path.pug"); - defer allocator.free(result); - try std.testing.expectEqualStrings("/absolute/path.pug", result); -} - -test "defaultResolve - missing basedir for absolute path" { - const options = LoadOptions{}; - const result = defaultResolve("/absolute/path.pug", null, &options); - try std.testing.expectError(error.MissingBasedir, result); -} - -test "defaultResolve - missing filename for relative path" { - const options = LoadOptions{ .basedir = "/base" }; - const result = defaultResolve("relative/path.pug", null, &options); - try std.testing.expectError(error.MissingFilename, result); -} - -test "load - basic AST without includes" { - const allocator = std.testing.allocator; - - // Create a simple AST with no includes - const text_node = try allocator.create(Node); - text_node.* = Node{ - .type = .Text, - .val = "Hello", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, text_node); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Load should succeed with no changes - const result = try load(allocator, root, .{}); - try std.testing.expectEqual(root, result); -} diff --git a/src/v1/parser.zig b/src/v1/parser.zig deleted file mode 100644 index 53f97c8..0000000 --- a/src/v1/parser.zig +++ /dev/null @@ -1,1646 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const Allocator = std.mem.Allocator; - -// Import token types from lexer -const lexer = @import("lexer.zig"); -pub const TokenType = lexer.TokenType; -pub const TokenValue = lexer.TokenValue; -pub const Location = lexer.Location; -pub const TokenLoc = lexer.TokenLoc; -pub const Token = lexer.Token; - -// ============================================================================ -// Inline Tags (tags that are typically inline in HTML) -// ============================================================================ - -const inline_tags = [_][]const u8{ - "a", - "abbr", - "acronym", - "b", - "br", - "code", - "em", - "font", - "i", - "img", - "ins", - "kbd", - "map", - "samp", - "small", - "span", - "strong", - "sub", - "sup", -}; - -fn isInlineTag(name: []const u8) bool { - for (inline_tags) |tag| { - if (mem.eql(u8, name, tag)) return true; - } - return false; -} - -// ============================================================================ -// AST Node Types -// ============================================================================ - -pub const NodeType = enum { - Block, - NamedBlock, - Tag, - InterpolatedTag, - Text, - Code, - Comment, - BlockComment, - Doctype, - Mixin, - MixinBlock, - Case, - When, - Conditional, - While, - Each, - EachOf, - Extends, - Include, - RawInclude, - Filter, - IncludeFilter, - FileReference, - YieldBlock, - AttributeBlock, -}; - -// ============================================================================ -// AST Node - A tagged union representing all possible AST nodes -// ============================================================================ - -pub const Attribute = struct { - name: []const u8, - val: ?[]const u8, - line: usize, - column: usize, - filename: ?[]const u8, - must_escape: bool, - val_owned: bool = false, // true if val was allocated and needs to be freed -}; - -pub const AttributeBlock = struct { - val: []const u8, - line: usize, - column: usize, - filename: ?[]const u8, -}; - -pub const FileReference = struct { - path: ?[]const u8, - line: usize, - column: usize, - filename: ?[]const u8, -}; - -pub const Node = struct { - type: NodeType, - line: usize = 0, - column: usize = 0, - filename: ?[]const u8 = null, - - // Block fields - nodes: std.ArrayListUnmanaged(*Node) = .{}, - - // NamedBlock additional fields - name: ?[]const u8 = null, // Also used for Tag, Mixin, Filter - mode: ?[]const u8 = null, // "prepend", "append", "replace" - - // Tag fields - self_closing: bool = false, - attrs: std.ArrayListUnmanaged(Attribute) = .{}, - attribute_blocks: std.ArrayListUnmanaged(AttributeBlock) = .{}, - is_inline: bool = false, - text_only: bool = false, - self_closing_allowed: bool = false, - - // Text fields - val: ?[]const u8 = null, // Also used for Code, Comment, Doctype, Case expr, When expr, Conditional test, While test - is_html: bool = false, - - // Code fields - buffer: bool = false, - must_escape: bool = true, - is_inline_code: bool = false, - - // Mixin fields - args: ?[]const u8 = null, - call: bool = false, - - // Each fields - obj: ?[]const u8 = null, - key: ?[]const u8 = null, - - // Conditional fields - test_expr: ?[]const u8 = null, // "test" in JS - consequent: ?*Node = null, - alternate: ?*Node = null, - - // Extends/Include fields - file: ?FileReference = null, - - // Include fields - filters: std.ArrayListUnmanaged(*Node) = .{}, - - // InterpolatedTag fields - expr: ?[]const u8 = null, - - // When/Conditional debug field - debug: bool = true, - - // Memory ownership flags - val_owned: bool = false, // true if val was allocated and needs to be freed - - pub fn deinit(self: *Node, allocator: Allocator) void { - // Free owned val string - if (self.val_owned) { - if (self.val) |v| { - allocator.free(v); - } - } - - // Free child nodes recursively - for (self.nodes.items) |child| { - child.deinit(allocator); - allocator.destroy(child); - } - self.nodes.deinit(allocator); - - // Free attrs (including owned val strings) - for (self.attrs.items) |attr| { - if (attr.val_owned) { - if (attr.val) |v| { - allocator.free(v); - } - } - } - self.attrs.deinit(allocator); - - // Free attribute_blocks - self.attribute_blocks.deinit(allocator); - - // Free filters - for (self.filters.items) |filter| { - filter.deinit(allocator); - allocator.destroy(filter); - } - self.filters.deinit(allocator); - - // Free consequent and alternate - if (self.consequent) |c| { - c.deinit(allocator); - allocator.destroy(c); - } - if (self.alternate) |a| { - a.deinit(allocator); - allocator.destroy(a); - } - } - - pub fn addNode(self: *Node, allocator: Allocator, node: *Node) !void { - try self.nodes.append(allocator, node); - } -}; - -// ============================================================================ -// Parser Error -// ============================================================================ - -pub const ParserErrorCode = enum { - INVALID_TOKEN, - BLOCK_IN_BUFFERED_CODE, - BLOCK_OUTISDE_MIXIN, - MIXIN_WITHOUT_BODY, - RAW_INCLUDE_BLOCK, - DUPLICATE_ID, - DUPLICATE_ATTRIBUTE, - UNEXPECTED_END, -}; - -pub const ParserError = struct { - code: ParserErrorCode, - message: []const u8, - line: usize, - column: usize, - filename: ?[]const u8, -}; - -// ============================================================================ -// Parser -// ============================================================================ - -pub const Parser = struct { - allocator: Allocator, - tokens: []const Token, - pos: usize = 0, - deferred: std.ArrayListUnmanaged(Token) = .{}, - filename: ?[]const u8 = null, - src: ?[]const u8 = null, - in_mixin: usize = 0, - err: ?ParserError = null, - - pub fn init(allocator: Allocator, tokens: []const Token, filename: ?[]const u8, src: ?[]const u8) Parser { - return .{ - .allocator = allocator, - .tokens = tokens, - .filename = filename, - .src = src, - }; - } - - pub fn deinit(self: *Parser) void { - self.deferred.deinit(self.allocator); - } - - // ======================================================================== - // Token Stream Methods - // ======================================================================== - - /// Return the next token without consuming it - pub fn peek(self: *Parser) Token { - if (self.deferred.items.len > 0) { - return self.deferred.items[0]; - } - if (self.pos < self.tokens.len) { - return self.tokens[self.pos]; - } - // Return EOS token if past end - return .{ - .type = .eos, - .loc = .{ .start = .{ .line = 0, .column = 0 } }, - }; - } - - /// Return the token at offset n from current position (0 = current) - pub fn lookahead(self: *Parser, n: usize) Token { - const deferred_len = self.deferred.items.len; - if (n < deferred_len) { - return self.deferred.items[n]; - } - const index = self.pos + (n - deferred_len); - if (index < self.tokens.len) { - return self.tokens[index]; - } - return .{ - .type = .eos, - .loc = .{ .start = .{ .line = 0, .column = 0 } }, - }; - } - - /// Consume and return the next token - pub fn advance(self: *Parser) Token { - if (self.deferred.items.len > 0) { - return self.deferred.orderedRemove(0); - } - if (self.pos < self.tokens.len) { - const tok = self.tokens[self.pos]; - self.pos += 1; - return tok; - } - return .{ - .type = .eos, - .loc = .{ .start = .{ .line = 0, .column = 0 } }, - }; - } - - /// Push a token to the front of the stream - pub fn defer_token(self: *Parser, token: Token) !void { - try self.deferred.insert(self.allocator, 0, token); - } - - /// Expect a specific token type, return error if not found - pub fn expect(self: *Parser, token_type: TokenType) !Token { - const tok = self.peek(); - if (tok.type == token_type) { - return self.advance(); - } - self.setError(.INVALID_TOKEN, "expected different token type", tok); - return error.InvalidToken; - } - - /// Accept a token if it matches, otherwise return null - pub fn accept(self: *Parser, token_type: TokenType) ?Token { - if (self.peek().type == token_type) { - return self.advance(); - } - return null; - } - - // ======================================================================== - // Error Handling - // ======================================================================== - - fn setError(self: *Parser, code: ParserErrorCode, message: []const u8, token: Token) void { - self.err = .{ - .code = code, - .message = message, - .line = token.loc.start.line, - .column = token.loc.start.column, - .filename = self.filename, - }; - } - - pub fn getError(self: *const Parser) ?ParserError { - return self.err; - } - - // ======================================================================== - // Block Helpers - // ======================================================================== - - fn initBlock(self: *Parser, line: usize) !*Node { - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Block, - .line = line, - .filename = self.filename, - }; - return node; - } - - fn emptyBlock(self: *Parser, line: usize) !*Node { - return self.initBlock(line); - } - - // ======================================================================== - // Main Parse Entry Point - // ======================================================================== - - pub fn parse(self: *Parser) !*Node { - var block = try self.emptyBlock(0); - - while (self.peek().type != .eos) { - if (self.peek().type == .newline) { - _ = self.advance(); - } else if (self.peek().type == .text_html) { - var html_nodes = try self.parseTextHtml(); - for (html_nodes.items) |node| { - try block.addNode(self.allocator, node); - } - html_nodes.deinit(self.allocator); - } else { - const expr = try self.parseExpr(); - if (expr.type == .Block) { - // Flatten block nodes into parent - for (expr.nodes.items) |node| { - try block.addNode(self.allocator, node); - } - // Clear the expr's nodes list (already moved) - expr.nodes.clearAndFree(self.allocator); - self.allocator.destroy(expr); - } else { - try block.addNode(self.allocator, expr); - } - } - } - - return block; - } - - // ======================================================================== - // Expression Parsing - // ======================================================================== - - fn parseExpr(self: *Parser) anyerror!*Node { - const tok = self.peek(); - return switch (tok.type) { - .tag => self.parseTag(), - .mixin => self.parseMixin(), - .block => self.parseBlock(), - .mixin_block => self.parseMixinBlock(), - .case => self.parseCase(), - .extends => self.parseExtends(), - .include => self.parseInclude(), - .doctype => self.parseDoctype(), - .filter => self.parseFilter(), - .comment => self.parseComment(), - .text, .interpolated_code, .start_pug_interpolation => self.parseText(true), - .text_html => blk: { - var html_nodes = try self.parseTextHtml(); - const block = try self.initBlock(tok.loc.start.line); - for (html_nodes.items) |node| { - try block.addNode(self.allocator, node); - } - html_nodes.deinit(self.allocator); - break :blk block; - }, - .dot => self.parseDot(), - .each => self.parseEach(), - .each_of => self.parseEachOf(), - .code => self.parseCode(false), - .blockcode => self.parseBlockCode(), - .@"if" => self.parseConditional(), - .@"while" => self.parseWhile(), - .call => self.parseCall(), - .interpolation => self.parseInterpolation(), - .yield => self.parseYield(), - .id, .class => blk: { - // Implicit div tag for #id or .class - try self.defer_token(.{ - .type = .tag, - .val = .{ .string = "div" }, - .loc = tok.loc, - }); - break :blk self.parseExpr(); - }, - else => { - self.setError(.INVALID_TOKEN, "unexpected token", tok); - return error.InvalidToken; - }, - }; - } - - fn parseDot(self: *Parser) !*Node { - _ = self.advance(); - return self.parseTextBlock() orelse try self.emptyBlock(self.peek().loc.start.line); - } - - // ======================================================================== - // Text Parsing - // ======================================================================== - - fn parseText(self: *Parser, allow_block: bool) !*Node { - const lineno = self.peek().loc.start.line; - var tags = std.ArrayListUnmanaged(*Node){}; - defer tags.deinit(self.allocator); - - while (true) { - const next_tok = self.peek(); - switch (next_tok.type) { - .text => { - const tok = self.advance(); - const text_node = try self.allocator.create(Node); - text_node.* = .{ - .type = .Text, - .val = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - try tags.append(self.allocator, text_node); - }, - .interpolated_code => { - const tok = self.advance(); - const code_node = try self.allocator.create(Node); - code_node.* = .{ - .type = .Code, - .val = tok.val.getString(), - .buffer = tok.isBuffered(), - .must_escape = tok.shouldEscape(), - .is_inline_code = true, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - try tags.append(self.allocator, code_node); - }, - .newline => { - if (!allow_block) break; - const tok = self.advance(); - const next_type = self.peek().type; - if (next_type == .text or next_type == .interpolated_code) { - const nl_node = try self.allocator.create(Node); - nl_node.* = .{ - .type = .Text, - .val = "\n", - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - try tags.append(self.allocator, nl_node); - } - }, - .start_pug_interpolation => { - _ = self.advance(); - const expr = try self.parseExpr(); - try tags.append(self.allocator, expr); - _ = try self.expect(.end_pug_interpolation); - }, - else => break, - } - } - - if (tags.items.len == 1) { - const result = tags.items[0]; - tags.clearAndFree(self.allocator); - return result; - } else { - const block = try self.initBlock(lineno); - for (tags.items) |node| { - try block.addNode(self.allocator, node); - } - tags.clearAndFree(self.allocator); - return block; - } - } - - fn parseTextHtml(self: *Parser) !std.ArrayListUnmanaged(*Node) { - var nodes = std.ArrayListUnmanaged(*Node){}; - var current_node: ?*Node = null; - - while (true) { - switch (self.peek().type) { - .text_html => { - const text = self.advance(); - if (current_node == null) { - current_node = try self.allocator.create(Node); - current_node.?.* = .{ - .type = .Text, - .val = text.val.getString(), - .filename = self.filename, - .line = text.loc.start.line, - .column = text.loc.start.column, - .is_html = true, - }; - try nodes.append(self.allocator, current_node.?); - } else { - // Concatenate with newline - need to allocate new string - // For now, create a new text node (simplified) - const new_node = try self.allocator.create(Node); - new_node.* = .{ - .type = .Text, - .val = text.val.getString(), - .filename = self.filename, - .line = text.loc.start.line, - .column = text.loc.start.column, - .is_html = true, - }; - try nodes.append(self.allocator, new_node); - } - }, - .indent => { - const block_nodes = try self.block_(); - for (block_nodes.nodes.items) |node| { - if (node.is_html) { - if (current_node == null) { - current_node = node; - try nodes.append(self.allocator, current_node.?); - } else { - try nodes.append(self.allocator, node); - } - } else { - current_node = null; - try nodes.append(self.allocator, node); - } - } - block_nodes.nodes.deinit(self.allocator); - self.allocator.destroy(block_nodes); - }, - .code => { - current_node = null; - const code_node = try self.parseCode(true); - try nodes.append(self.allocator, code_node); - }, - .newline => { - _ = self.advance(); - }, - else => break, - } - } - - return nodes; - } - - fn parseTextBlock(self: *Parser) ?*Node { - const tok = self.accept(.start_pipeless_text) orelse return null; - var block = self.emptyBlock(tok.loc.start.line) catch return null; - - while (self.peek().type != .end_pipeless_text) { - const cur_tok = self.advance(); - switch (cur_tok.type) { - .text => { - const text_node = self.allocator.create(Node) catch return null; - text_node.* = .{ - .type = .Text, - .val = cur_tok.val.getString(), - .line = cur_tok.loc.start.line, - .column = cur_tok.loc.start.column, - .filename = self.filename, - }; - block.addNode(self.allocator, text_node) catch return null; - }, - .newline => { - const nl_node = self.allocator.create(Node) catch return null; - nl_node.* = .{ - .type = .Text, - .val = "\n", - .line = cur_tok.loc.start.line, - .column = cur_tok.loc.start.column, - .filename = self.filename, - }; - block.addNode(self.allocator, nl_node) catch return null; - }, - .start_pug_interpolation => { - const expr = self.parseExpr() catch return null; - block.addNode(self.allocator, expr) catch return null; - _ = self.expect(.end_pug_interpolation) catch return null; - }, - .interpolated_code => { - const code_node = self.allocator.create(Node) catch return null; - code_node.* = .{ - .type = .Code, - .val = cur_tok.val.getString(), - .buffer = cur_tok.isBuffered(), - .must_escape = cur_tok.shouldEscape(), - .is_inline_code = true, - .line = cur_tok.loc.start.line, - .column = cur_tok.loc.start.column, - .filename = self.filename, - }; - block.addNode(self.allocator, code_node) catch return null; - }, - else => { - self.setError(.INVALID_TOKEN, "Unexpected token in text block", cur_tok); - return null; - }, - } - } - _ = self.advance(); // consume end_pipeless_text - return block; - } - - // ======================================================================== - // Block Expansion - // ======================================================================== - - fn parseBlockExpansion(self: *Parser) !*Node { - if (self.accept(.colon)) |tok| { - const expr = try self.parseExpr(); - if (expr.type == .Block) { - return expr; - } - const block = try self.initBlock(tok.loc.start.line); - try block.addNode(self.allocator, expr); - return block; - } - return self.block_(); - } - - // ======================================================================== - // Case/When/Default - // ======================================================================== - - fn parseCase(self: *Parser) !*Node { - const tok = try self.expect(.case); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Case, - .expr = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - var block = try self.emptyBlock(tok.loc.start.line + 1); - _ = try self.expect(.indent); - - while (self.peek().type != .outdent) { - switch (self.peek().type) { - .comment, .newline => { - _ = self.advance(); - }, - .when => { - const when_node = try self.parseWhen(); - try block.addNode(self.allocator, when_node); - }, - .default => { - const default_node = try self.parseDefault(); - try block.addNode(self.allocator, default_node); - }, - else => { - self.setError(.INVALID_TOKEN, "Expected 'when', 'default' or 'newline'", self.peek()); - return error.InvalidToken; - }, - } - } - _ = try self.expect(.outdent); - - // Move block nodes to case node - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - - return node; - } - - fn parseWhen(self: *Parser) !*Node { - const tok = try self.expect(.when); - const node = try self.allocator.create(Node); - - if (self.peek().type != .newline) { - node.* = .{ - .type = .When, - .expr = tok.val.getString(), - .debug = false, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - const block = try self.parseBlockExpansion(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - } else { - node.* = .{ - .type = .When, - .expr = tok.val.getString(), - .debug = false, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - } - - return node; - } - - fn parseDefault(self: *Parser) !*Node { - const tok = try self.expect(.default); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .When, - .expr = "default", - .debug = false, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - const block = try self.parseBlockExpansion(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - return node; - } - - // ======================================================================== - // Code Parsing - // ======================================================================== - - fn parseCode(self: *Parser, no_block: bool) !*Node { - const tok = try self.expect(.code); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Code, - .val = tok.val.getString(), - .buffer = tok.isBuffered(), - .must_escape = tok.shouldEscape(), - .is_inline_code = no_block, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - // Check for "else" pattern - disable debug - if (node.val) |v| { - if (mem.indexOf(u8, v, "else") != null) { - node.debug = false; - } - } - - if (no_block) return node; - - // Handle block - if (self.peek().type == .indent) { - if (tok.isBuffered()) { - self.setError(.BLOCK_IN_BUFFERED_CODE, "Buffered code cannot have a block attached", self.peek()); - return error.BlockInBufferedCode; - } - const block = try self.block_(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - } - - return node; - } - - fn parseConditional(self: *Parser) !*Node { - const tok = try self.expect(.@"if"); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Conditional, - .test_expr = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - node.consequent = try self.emptyBlock(tok.loc.start.line); - - // Handle block - if (self.peek().type == .indent) { - const block = try self.block_(); - // Replace empty consequent with actual block - self.allocator.destroy(node.consequent.?); - node.consequent = block; - } - - var current_node = node; - while (true) { - if (self.peek().type == .newline) { - _ = try self.expect(.newline); - } else if (self.peek().type == .else_if) { - const else_if_tok = try self.expect(.else_if); - const else_if_node = try self.allocator.create(Node); - else_if_node.* = .{ - .type = .Conditional, - .test_expr = else_if_tok.val.getString(), - .line = else_if_tok.loc.start.line, - .column = else_if_tok.loc.start.column, - .filename = self.filename, - }; - else_if_node.consequent = try self.emptyBlock(else_if_tok.loc.start.line); - current_node.alternate = else_if_node; - current_node = else_if_node; - - if (self.peek().type == .indent) { - const block = try self.block_(); - self.allocator.destroy(current_node.consequent.?); - current_node.consequent = block; - } - } else if (self.peek().type == .@"else") { - _ = try self.expect(.@"else"); - if (self.peek().type == .indent) { - current_node.alternate = try self.block_(); - } - break; - } else { - break; - } - } - - return node; - } - - fn parseWhile(self: *Parser) !*Node { - const tok = try self.expect(.@"while"); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .While, - .test_expr = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - // Handle block - if (self.peek().type == .indent) { - const block = try self.block_(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - } - - return node; - } - - fn parseBlockCode(self: *Parser) !*Node { - const tok = try self.expect(.blockcode); - const line = tok.loc.start.line; - const column = tok.loc.start.column; - - var text = std.ArrayListUnmanaged(u8){}; - defer text.deinit(self.allocator); - - if (self.peek().type == .start_pipeless_text) { - _ = self.advance(); - while (self.peek().type != .end_pipeless_text) { - const inner_tok = self.advance(); - switch (inner_tok.type) { - .text => { - if (inner_tok.val.getString()) |s| { - try text.appendSlice(self.allocator, s); - } - }, - .newline => { - try text.append(self.allocator, '\n'); - }, - else => { - self.setError(.INVALID_TOKEN, "Unexpected token in block code", inner_tok); - return error.InvalidToken; - }, - } - } - _ = self.advance(); - } - - const node = try self.allocator.create(Node); - // Need to dupe the text to persist it - const text_slice = try self.allocator.dupe(u8, text.items); - node.* = .{ - .type = .Code, - .val = text_slice, - .val_owned = true, // We allocated this string - .buffer = false, - .must_escape = false, - .is_inline_code = false, - .line = line, - .column = column, - .filename = self.filename, - }; - return node; - } - - // ======================================================================== - // Comment Parsing - // ======================================================================== - - fn parseComment(self: *Parser) !*Node { - const tok = try self.expect(.comment); - - if (self.parseTextBlock()) |block| { - const node = try self.allocator.create(Node); - node.* = .{ - .type = .BlockComment, - .val = tok.val.getString(), - .buffer = tok.isBuffered(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - // Move block nodes to comment - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - return node; - } else { - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Comment, - .val = tok.val.getString(), - .buffer = tok.isBuffered(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - return node; - } - } - - // ======================================================================== - // Doctype Parsing - // ======================================================================== - - fn parseDoctype(self: *Parser) !*Node { - const tok = try self.expect(.doctype); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Doctype, - .val = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - return node; - } - - // ======================================================================== - // Filter Parsing - // ======================================================================== - - fn parseIncludeFilter(self: *Parser) !*Node { - const tok = try self.expect(.filter); - var filter_attrs = std.ArrayListUnmanaged(Attribute){}; - - if (self.peek().type == .start_attributes) { - filter_attrs = try self.attrs(null); - } - - const node = try self.allocator.create(Node); - node.* = .{ - .type = .IncludeFilter, - .name = tok.val.getString(), - .attrs = filter_attrs, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - return node; - } - - fn parseFilter(self: *Parser) !*Node { - const tok = try self.expect(.filter); - var filter_attrs = std.ArrayListUnmanaged(Attribute){}; - - if (self.peek().type == .start_attributes) { - filter_attrs = try self.attrs(null); - } - - var block: *Node = undefined; - if (self.peek().type == .text) { - const text_token = self.advance(); - block = try self.initBlock(text_token.loc.start.line); - const text_node = try self.allocator.create(Node); - text_node.* = .{ - .type = .Text, - .val = text_token.val.getString(), - .line = text_token.loc.start.line, - .column = text_token.loc.start.column, - .filename = self.filename, - }; - try block.addNode(self.allocator, text_node); - } else if (self.peek().type == .filter) { - block = try self.initBlock(tok.loc.start.line); - const nested_filter = try self.parseFilter(); - try block.addNode(self.allocator, nested_filter); - } else { - block = self.parseTextBlock() orelse try self.emptyBlock(tok.loc.start.line); - } - - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Filter, - .name = tok.val.getString(), - .attrs = filter_attrs, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - return node; - } - - // ======================================================================== - // Each Parsing - // ======================================================================== - - fn parseEach(self: *Parser) !*Node { - const tok = try self.expect(.each); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Each, - .obj = tok.code.getString(), - .val = tok.val.getString(), - .key = tok.key.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - const block = try self.block_(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - - if (self.peek().type == .@"else") { - _ = self.advance(); - node.alternate = try self.block_(); - } - - return node; - } - - fn parseEachOf(self: *Parser) !*Node { - const tok = try self.expect(.each_of); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .EachOf, - .obj = tok.code.getString(), - .val = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - const block = try self.block_(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - - return node; - } - - // ======================================================================== - // Extends Parsing - // ======================================================================== - - fn parseExtends(self: *Parser) !*Node { - const tok = try self.expect(.extends); - const path_tok = try self.expect(.path); - - const path_val = if (path_tok.val.getString()) |s| mem.trim(u8, s, " \t") else null; - - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Extends, - .file = .{ - .path = path_val, - .line = path_tok.loc.start.line, - .column = path_tok.loc.start.column, - .filename = self.filename, - }, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - return node; - } - - // ======================================================================== - // Block Parsing - // ======================================================================== - - fn parseBlock(self: *Parser) !*Node { - const tok = try self.expect(.block); - - var node: *Node = undefined; - if (self.peek().type == .indent) { - node = try self.block_(); - } else { - node = try self.emptyBlock(tok.loc.start.line); - } - - node.type = .NamedBlock; - node.name = if (tok.val.getString()) |s| mem.trim(u8, s, " \t") else null; - node.mode = tok.mode.getString(); - node.line = tok.loc.start.line; - node.column = tok.loc.start.column; - - return node; - } - - fn parseMixinBlock(self: *Parser) !*Node { - const tok = try self.expect(.mixin_block); - if (self.in_mixin == 0) { - self.setError(.BLOCK_OUTISDE_MIXIN, "Anonymous blocks are not allowed unless they are part of a mixin.", tok); - return error.BlockOutsideMixin; - } - const node = try self.allocator.create(Node); - node.* = .{ - .type = .MixinBlock, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - return node; - } - - fn parseYield(self: *Parser) !*Node { - const tok = try self.expect(.yield); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .YieldBlock, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - return node; - } - - // ======================================================================== - // Include Parsing - // ======================================================================== - - fn parseInclude(self: *Parser) !*Node { - const tok = try self.expect(.include); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Include, - .file = .{ - .path = null, - .line = 0, - .column = 0, - .filename = self.filename, - }, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - // Parse filters - while (self.peek().type == .filter) { - const filter_node = try self.parseIncludeFilter(); - try node.filters.append(self.allocator, filter_node); - } - - const path_tok = try self.expect(.path); - const path_val = if (path_tok.val.getString()) |s| mem.trim(u8, s, " \t") else null; - - node.file = .{ - .path = path_val, - .line = path_tok.loc.start.line, - .column = path_tok.loc.start.column, - .filename = self.filename, - }; - - const has_filters = node.filters.items.len > 0; - const is_pug_file = if (path_val) |p| (mem.endsWith(u8, p, ".jade") or mem.endsWith(u8, p, ".pug")) else false; - - if (is_pug_file and !has_filters) { - // Pug include with block - if (self.peek().type == .indent) { - const block = try self.block_(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - } - } else { - // Raw include - node.type = .RawInclude; - if (self.peek().type == .indent) { - self.setError(.RAW_INCLUDE_BLOCK, "Raw inclusion cannot contain a block", self.peek()); - return error.RawIncludeBlock; - } - } - - return node; - } - - // ======================================================================== - // Mixin/Call Parsing - // ======================================================================== - - fn parseCall(self: *Parser) !*Node { - const tok = try self.expect(.call); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Mixin, - .name = tok.val.getString(), - .args = tok.args.getString(), - .call = true, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - - try self.tag_(node, true); - - // If code was added, move it to block - // (simplified - the JS version has special handling for mixin.code) - - // If block is empty, set to null (matching JS behavior) - if (node.nodes.items.len == 0) { - // Keep empty block as is - JS sets block to null but we don't have optional block - } - - return node; - } - - fn parseMixin(self: *Parser) !*Node { - const tok = try self.expect(.mixin); - - if (self.peek().type == .indent) { - self.in_mixin += 1; - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Mixin, - .name = tok.val.getString(), - .args = tok.args.getString(), - .call = false, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - const block = try self.block_(); - for (block.nodes.items) |n| { - try node.addNode(self.allocator, n); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - self.in_mixin -= 1; - return node; - } else { - self.setError(.MIXIN_WITHOUT_BODY, "Mixin declared without body", tok); - return error.MixinWithoutBody; - } - } - - // ======================================================================== - // Block (indent/outdent) - // ======================================================================== - - fn block_(self: *Parser) anyerror!*Node { - const tok = try self.expect(.indent); - var block = try self.emptyBlock(tok.loc.start.line); - - while (self.peek().type != .outdent) { - if (self.peek().type == .newline) { - _ = self.advance(); - } else if (self.peek().type == .text_html) { - var html_nodes = try self.parseTextHtml(); - for (html_nodes.items) |node| { - try block.addNode(self.allocator, node); - } - html_nodes.deinit(self.allocator); - } else { - const expr = try self.parseExpr(); - if (expr.type == .Block) { - for (expr.nodes.items) |node| { - try block.addNode(self.allocator, node); - } - expr.nodes.clearAndFree(self.allocator); - self.allocator.destroy(expr); - } else { - try block.addNode(self.allocator, expr); - } - } - } - _ = try self.expect(.outdent); - return block; - } - - // ======================================================================== - // Interpolation/Tag Parsing - // ======================================================================== - - fn parseInterpolation(self: *Parser) !*Node { - const tok = self.advance(); - const node = try self.allocator.create(Node); - node.* = .{ - .type = .InterpolatedTag, - .expr = tok.val.getString(), - .self_closing = false, - .is_inline = false, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - try self.tag_(node, true); - return node; - } - - fn parseTag(self: *Parser) !*Node { - const tok = self.advance(); - const tag_name = tok.val.getString() orelse "div"; - const node = try self.allocator.create(Node); - node.* = .{ - .type = .Tag, - .name = tag_name, - .self_closing = false, - .is_inline = isInlineTag(tag_name), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }; - try self.tag_(node, true); - return node; - } - - fn tag_(self: *Parser, tag: *Node, self_closing_allowed: bool) !void { - var seen_attrs = false; - var attribute_names = std.ArrayListUnmanaged([]const u8){}; - defer attribute_names.deinit(self.allocator); - - // (attrs | class | id)* - outer: while (true) { - switch (self.peek().type) { - .id, .class => { - const tok = self.advance(); - if (tok.type == .id) { - // Check for duplicate id - for (attribute_names.items) |name| { - if (mem.eql(u8, name, "id")) { - self.setError(.DUPLICATE_ID, "Duplicate attribute \"id\" is not allowed.", tok); - return error.DuplicateId; - } - } - try attribute_names.append(self.allocator, "id"); - } - // Create quoted value - const val_str = tok.val.getString() orelse ""; - var quoted_val = std.ArrayListUnmanaged(u8){}; - defer quoted_val.deinit(self.allocator); - try quoted_val.append(self.allocator, '\''); - try quoted_val.appendSlice(self.allocator, val_str); - try quoted_val.append(self.allocator, '\''); - const final_val = try self.allocator.dupe(u8, quoted_val.items); - - try tag.attrs.append(self.allocator, .{ - .name = if (tok.type == .id) "id" else "class", - .val = final_val, - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - .must_escape = false, - .val_owned = true, // We allocated this string - }); - }, - .start_attributes => { - if (seen_attrs) { - // Warning: multiple attributes - but continue - } - seen_attrs = true; - var new_attrs = try self.attrs(&attribute_names); - for (new_attrs.items) |attr| { - try tag.attrs.append(self.allocator, attr); - } - new_attrs.deinit(self.allocator); - }, - .@"&attributes" => { - const tok = self.advance(); - try tag.attribute_blocks.append(self.allocator, .{ - .val = tok.val.getString() orelse "", - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - }); - }, - else => break :outer, - } - } - - // Check for textOnly (.) - if (self.peek().type == .dot) { - tag.text_only = true; - _ = self.advance(); - } - - // (text | code | ':')? - switch (self.peek().type) { - .text, .interpolated_code => { - const text = try self.parseText(false); - if (text.type == .Block) { - for (text.nodes.items) |node| { - try tag.addNode(self.allocator, node); - } - text.nodes.deinit(self.allocator); - self.allocator.destroy(text); - } else { - try tag.addNode(self.allocator, text); - } - }, - .code => { - const code_node = try self.parseCode(true); - try tag.addNode(self.allocator, code_node); - }, - .colon => { - _ = self.advance(); - const expr = try self.parseExpr(); - if (expr.type == .Block) { - for (expr.nodes.items) |node| { - try tag.addNode(self.allocator, node); - } - expr.nodes.deinit(self.allocator); - self.allocator.destroy(expr); - } else { - try tag.addNode(self.allocator, expr); - } - }, - .newline, .indent, .outdent, .eos, .start_pipeless_text, .end_pug_interpolation => {}, - .slash => { - if (self_closing_allowed) { - _ = self.advance(); - tag.self_closing = true; - } else { - self.setError(.INVALID_TOKEN, "Unexpected token", self.peek()); - return error.InvalidToken; - } - }, - else => { - // Accept other tokens without error for now - }, - } - - // newline* - while (self.peek().type == .newline) { - _ = self.advance(); - } - - // block? - if (tag.text_only) { - if (self.parseTextBlock()) |block| { - for (block.nodes.items) |node| { - try tag.addNode(self.allocator, node); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - } - } else if (self.peek().type == .indent) { - const block = try self.block_(); - for (block.nodes.items) |node| { - try tag.addNode(self.allocator, node); - } - block.nodes.deinit(self.allocator); - self.allocator.destroy(block); - } - } - - fn attrs(self: *Parser, attribute_names: ?*std.ArrayListUnmanaged([]const u8)) !std.ArrayListUnmanaged(Attribute) { - _ = try self.expect(.start_attributes); - - var result = std.ArrayListUnmanaged(Attribute){}; - var tok = self.advance(); - - while (tok.type == .attribute) { - const attr_name = tok.name.getString() orelse ""; - - // Check for duplicates (except class) - if (!mem.eql(u8, attr_name, "class")) { - if (attribute_names) |names| { - for (names.items) |name| { - if (mem.eql(u8, name, attr_name)) { - self.setError(.DUPLICATE_ATTRIBUTE, "Duplicate attribute is not allowed.", tok); - return error.DuplicateAttribute; - } - } - try names.append(self.allocator, attr_name); - } - } - - try result.append(self.allocator, .{ - .name = attr_name, - .val = tok.val.getString(), - .line = tok.loc.start.line, - .column = tok.loc.start.column, - .filename = self.filename, - .must_escape = tok.shouldEscape(), - }); - tok = self.advance(); - } - - try self.defer_token(tok); - _ = try self.expect(.end_attributes); - - return result; - } -}; - -// ============================================================================ -// Tests -// ============================================================================ - -test "parser basic" { - const allocator = std.testing.allocator; - - // Simulate tokens for: html\n body\n h1 Title - var tokens = [_]Token{ - .{ .type = .tag, .val = .{ .string = "html" }, .loc = .{ .start = .{ .line = 1, .column = 1 } } }, - .{ .type = .indent, .val = .{ .string = "2" }, .loc = .{ .start = .{ .line = 2, .column = 1 } } }, - .{ .type = .tag, .val = .{ .string = "body" }, .loc = .{ .start = .{ .line = 2, .column = 3 } } }, - .{ .type = .indent, .val = .{ .string = "4" }, .loc = .{ .start = .{ .line = 3, .column = 1 } } }, - .{ .type = .tag, .val = .{ .string = "h1" }, .loc = .{ .start = .{ .line = 3, .column = 5 } } }, - .{ .type = .text, .val = .{ .string = "Title" }, .loc = .{ .start = .{ .line = 3, .column = 8 } } }, - .{ .type = .outdent, .loc = .{ .start = .{ .line = 3, .column = 13 } } }, - .{ .type = .outdent, .loc = .{ .start = .{ .line = 3, .column = 13 } } }, - .{ .type = .eos, .loc = .{ .start = .{ .line = 3, .column = 13 } } }, - }; - - var parser = Parser.init(allocator, &tokens, "test.pug", null); - defer parser.deinit(); - - const ast = try parser.parse(); - defer { - ast.deinit(allocator); - allocator.destroy(ast); - } - - try std.testing.expectEqual(NodeType.Block, ast.type); - try std.testing.expectEqual(@as(usize, 1), ast.nodes.items.len); - - const html_tag = ast.nodes.items[0]; - try std.testing.expectEqual(NodeType.Tag, html_tag.type); - try std.testing.expectEqualStrings("html", html_tag.name.?); -} - -test "parser doctype" { - const allocator = std.testing.allocator; - - var tokens = [_]Token{ - .{ .type = .doctype, .val = .{ .string = "html" }, .loc = .{ .start = .{ .line = 1, .column = 1 } } }, - .{ .type = .eos, .loc = .{ .start = .{ .line = 1, .column = 13 } } }, - }; - - var parser = Parser.init(allocator, &tokens, "test.pug", null); - defer parser.deinit(); - - const ast = try parser.parse(); - defer { - ast.deinit(allocator); - allocator.destroy(ast); - } - - try std.testing.expectEqual(@as(usize, 1), ast.nodes.items.len); - try std.testing.expectEqual(NodeType.Doctype, ast.nodes.items[0].type); - try std.testing.expectEqualStrings("html", ast.nodes.items[0].val.?); -} diff --git a/src/v1/pug.zig b/src/v1/pug.zig deleted file mode 100644 index 8d9704d..0000000 --- a/src/v1/pug.zig +++ /dev/null @@ -1,457 +0,0 @@ -// pug.zig - Main entry point for Pug template engine in Zig -// -// This is the main module that ties together all the Pug compilation stages: -// 1. Lexer - tokenizes the source -// 2. Parser - builds the AST -// 3. Strip Comments - removes comment tokens -// 4. Load - loads includes and extends -// 5. Linker - resolves template inheritance -// 6. Codegen - generates HTML output - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const mem = std.mem; - -// ============================================================================ -// Module Exports -// ============================================================================ - -pub const lexer = @import("lexer.zig"); -pub const parser = @import("parser.zig"); -pub const runtime = @import("runtime.zig"); -pub const pug_error = @import("error.zig"); -pub const walk = @import("walk.zig"); -pub const strip_comments = @import("strip_comments.zig"); -pub const load = @import("load.zig"); -pub const linker = @import("linker.zig"); -pub const codegen = @import("codegen.zig"); - -// Re-export commonly used types -pub const Token = lexer.Token; -pub const TokenType = lexer.TokenType; -pub const Lexer = lexer.Lexer; -pub const Parser = parser.Parser; -pub const Node = parser.Node; -pub const NodeType = parser.NodeType; -pub const PugError = pug_error.PugError; -pub const Compiler = codegen.Compiler; - -// ============================================================================ -// Compile Options -// ============================================================================ - -pub const CompileOptions = struct { - /// Source filename for error messages - filename: ?[]const u8 = null, - /// Base directory for absolute includes - basedir: ?[]const u8 = null, - /// Pretty print output with indentation - pretty: bool = false, - /// Strip unbuffered comments - strip_unbuffered_comments: bool = true, - /// Strip buffered comments - strip_buffered_comments: bool = false, - /// Include debug information - debug: bool = false, - /// Doctype to use - doctype: ?[]const u8 = null, -}; - -// ============================================================================ -// Compile Result -// ============================================================================ - -pub const CompileResult = struct { - html: []const u8, - err: ?PugError = null, - - pub fn deinit(self: *CompileResult, allocator: Allocator) void { - allocator.free(self.html); - if (self.err) |*e| { - e.deinit(); - } - } -}; - -// ============================================================================ -// Compilation Errors -// ============================================================================ - -pub const CompileError = error{ - OutOfMemory, - LexerError, - ParserError, - LoadError, - LinkerError, - CodegenError, - FileNotFound, - AccessDenied, - InvalidUtf8, -}; - -// ============================================================================ -// Main Compilation Functions -// ============================================================================ - -/// Compile a Pug template string to HTML -pub fn compile( - allocator: Allocator, - source: []const u8, - options: CompileOptions, -) CompileError!CompileResult { - var result = CompileResult{ - .html = &[_]u8{}, - }; - - // Stage 1: Lex the source - var lex_inst = Lexer.init(allocator, source, .{ - .filename = options.filename, - }) catch { - return error.LexerError; - }; - defer lex_inst.deinit(); - - const tokens = lex_inst.getTokens() catch { - if (lex_inst.last_error) |err| { - // Try to create detailed error, fall back to basic error if allocation fails - result.err = pug_error.makeError( - allocator, - "PUG:LEXER_ERROR", - err.message, - .{ - .line = err.line, - .column = err.column, - .filename = options.filename, - .src = source, - }, - ) catch blk: { - // If error creation fails, create minimal error without source context - break :blk pug_error.makeError(allocator, "PUG:LEXER_ERROR", err.message, .{ - .line = err.line, - .column = err.column, - .filename = options.filename, - .src = null, // Skip source to reduce allocation - }) catch null; - }; - } - return error.LexerError; - }; - - // Stage 2: Strip comments - var stripped = strip_comments.stripComments( - allocator, - tokens, - .{ - .strip_unbuffered = options.strip_unbuffered_comments, - .strip_buffered = options.strip_buffered_comments, - .filename = options.filename, - }, - ) catch { - return error.LexerError; - }; - defer stripped.deinit(allocator); - - // Stage 3: Parse tokens to AST - var parse = Parser.init(allocator, stripped.tokens.items, options.filename, source); - defer parse.deinit(); - - const ast = parse.parse() catch { - if (parse.err) |err| { - // Try to create detailed error, fall back to basic error if allocation fails - result.err = pug_error.makeError( - allocator, - "PUG:PARSER_ERROR", - err.message, - .{ - .line = err.line, - .column = err.column, - .filename = options.filename, - .src = source, - }, - ) catch blk: { - // If error creation fails, create minimal error without source context - break :blk pug_error.makeError(allocator, "PUG:PARSER_ERROR", err.message, .{ - .line = err.line, - .column = err.column, - .filename = options.filename, - .src = null, - }) catch null; - }; - } - return error.ParserError; - }; - defer { - ast.deinit(allocator); - allocator.destroy(ast); - } - - // Stage 4: Link (resolve extends/blocks) - var link_result = linker.link(allocator, ast) catch { - return error.LinkerError; - }; - defer link_result.deinit(allocator); - - // Stage 5: Generate HTML - var compiler = Compiler.init(allocator, .{ - .pretty = options.pretty, - .doctype = options.doctype, - .debug = options.debug, - }); - defer compiler.deinit(); - - const html = compiler.compile(link_result.ast) catch { - return error.CodegenError; - }; - - result.html = html; - return result; -} - -/// Compile a Pug file to HTML -pub fn compileFile( - allocator: Allocator, - filename: []const u8, - options: CompileOptions, -) CompileError!CompileResult { - // Read the file - const file = std.fs.cwd().openFile(filename, .{}) catch |err| { - return switch (err) { - error.FileNotFound => error.FileNotFound, - error.AccessDenied => error.AccessDenied, - else => error.FileNotFound, - }; - }; - defer file.close(); - - const source = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch { - return error.OutOfMemory; - }; - defer allocator.free(source); - - // Compile with filename set - var file_options = options; - file_options.filename = filename; - - return compile(allocator, source, file_options); -} - -/// Render a Pug template string to HTML (convenience function) -pub fn render( - allocator: Allocator, - source: []const u8, -) CompileError![]const u8 { - var result = try compile(allocator, source, .{}); - if (result.err) |*e| { - e.deinit(); - } - return result.html; -} - -/// Render a Pug template string to pretty-printed HTML -pub fn renderPretty( - allocator: Allocator, - source: []const u8, -) CompileError![]const u8 { - var result = try compile(allocator, source, .{ .pretty = true }); - if (result.err) |*e| { - e.deinit(); - } - return result.html; -} - -/// Render a Pug file to HTML -pub fn renderFile( - allocator: Allocator, - filename: []const u8, -) CompileError![]const u8 { - var result = try compileFile(allocator, filename, .{}); - if (result.err) |*e| { - e.deinit(); - } - return result.html; -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "compile - simple text" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "| Hello, World!", .{}); - defer result.deinit(allocator); - - try std.testing.expectEqualStrings("Hello, World!", result.html); -} - -test "compile - simple tag" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "div", .{}); - defer result.deinit(allocator); - - try std.testing.expectEqualStrings("
", result.html); -} - -test "compile - tag with text" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "p Hello", .{}); - defer result.deinit(allocator); - - try std.testing.expectEqualStrings("

Hello

", result.html); -} - -test "compile - tag with class shorthand" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "div.container", .{}); - defer result.deinit(allocator); - - // Parser stores class values with quotes, verify class attribute is present - try std.testing.expect(mem.indexOf(u8, result.html, "class=") != null); - try std.testing.expect(mem.indexOf(u8, result.html, "container") != null); -} - -test "compile - tag with id shorthand" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "div#main", .{}); - defer result.deinit(allocator); - - // Parser stores id values with quotes, verify id attribute is present - try std.testing.expect(mem.indexOf(u8, result.html, "id=") != null); - try std.testing.expect(mem.indexOf(u8, result.html, "main") != null); -} - -test "compile - tag with attributes" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "a(href=\"/home\") Home", .{}); - defer result.deinit(allocator); - - // Parser stores attribute values with quotes, verify attribute is present - try std.testing.expect(mem.indexOf(u8, result.html, "href=") != null); - try std.testing.expect(mem.indexOf(u8, result.html, "/home") != null); - try std.testing.expect(mem.indexOf(u8, result.html, "Home") != null); -} - -test "compile - nested tags" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "div\n span Hello", .{}); - defer result.deinit(allocator); - - try std.testing.expectEqualStrings("
Hello
", result.html); -} - -test "compile - self-closing tag" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "br", .{}); - defer result.deinit(allocator); - - try std.testing.expectEqualStrings("
", result.html); -} - -test "compile - doctype" { - const allocator = std.testing.allocator; - - var result = try compile(allocator, "doctype html\nhtml", .{}); - defer result.deinit(allocator); - - try std.testing.expect(mem.startsWith(u8, result.html, "")); -} - -test "compile - unbuffered comment stripped" { - const allocator = std.testing.allocator; - - // Unbuffered comments (//-) are stripped by default - var result = try compile(allocator, "//- This is stripped\ndiv", .{}); - defer result.deinit(allocator); - - // The comment text should not appear - try std.testing.expect(mem.indexOf(u8, result.html, "stripped") == null); - // But the div should - try std.testing.expect(mem.indexOf(u8, result.html, "
") != null); -} - -test "compile - buffered comment visible" { - const allocator = std.testing.allocator; - - // Buffered comments (//) are kept by default - var result = try compile(allocator, "// This is visible", .{}); - defer result.deinit(allocator); - - // Buffered comments should be in output - try std.testing.expect(mem.indexOf(u8, result.html, ""); -} - -fn renderBlockComment(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), comment: *Node, data: anytype) Allocator.Error!void { - if (!comment.buffer) return; - try output.appendSlice(allocator, ""); -} - -fn renderDoctype(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), doctype: *Node) Allocator.Error!void { - if (doctype.val) |val| { - if (std.mem.eql(u8, val, "html")) { - try output.appendSlice(allocator, ""); - } else if (std.mem.eql(u8, val, "xml")) { - try output.appendSlice(allocator, ""); - } else { - try output.appendSlice(allocator, ""); - } - } else { - try output.appendSlice(allocator, ""); - } -} - -/// Process interpolation #{expr} in text -fn processInterpolation(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: []const u8, escape: bool, data: anytype) Allocator.Error!void { - var i: usize = 0; - while (i < text.len) { - // Look for #{ - if (i + 1 < text.len and text[i] == '#' and text[i + 1] == '{') { - // Find closing } - var j = i + 2; - var brace_count: usize = 1; - while (j < text.len and brace_count > 0) { - if (text[j] == '{') brace_count += 1; - if (text[j] == '}') brace_count -= 1; - j += 1; - } - if (brace_count == 0) { - // Extract expression - const expr = std.mem.trim(u8, text[i + 2 .. j - 1], " \t"); - if (getFieldValue(data, expr)) |value| { - if (escape) { - try appendEscaped(allocator, output, value); - } else { - try output.appendSlice(allocator, value); - } - } - i = j; - continue; - } - } - // Regular character - if (escape) { - if (runtime.escapeChar(text[i])) |esc| { - try output.appendSlice(allocator, esc); - } else { - try output.append(allocator, text[i]); - } - } else { - try output.append(allocator, text[i]); - } - i += 1; - } -} - -/// Get a field value from the data struct by name -fn getFieldValue(data: anytype, name: []const u8) ?[]const u8 { - const T = @TypeOf(data); - const info = @typeInfo(T); - - if (info != .@"struct") return null; - - inline for (info.@"struct".fields) |field| { - if (std.mem.eql(u8, field.name, name)) { - const value = @field(data, field.name); - const ValueType = @TypeOf(value); - - // Handle []const u8 - if (ValueType == []const u8) { - return value; - } - - // Handle string literals (*const [N:0]u8) - const value_info = @typeInfo(ValueType); - if (value_info == .pointer) { - const ptr = value_info.pointer; - if (ptr.size == .one) { - const child_info = @typeInfo(ptr.child); - if (child_info == .array and child_info.array.child == u8) { - return value; - } - } - } - } - } - return null; -} - -fn appendEscaped(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), str: []const u8) Allocator.Error!void { - for (str) |c| { - if (runtime.escapeChar(c)) |esc| { - try output.appendSlice(allocator, esc); - } else { - try output.append(allocator, c); - } - } -} - -fn isSelfClosing(name: []const u8) bool { - const self_closing_tags = [_][]const u8{ - "area", "base", "br", "col", "embed", "hr", "img", "input", - "link", "meta", "param", "source", "track", "wbr", - }; - for (self_closing_tags) |tag| { - if (std.mem.eql(u8, name, tag)) return true; - } - return false; -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "simple interpolation" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, "p Hello, #{name}!", .{ .name = "World" }); - defer allocator.free(html); - - try std.testing.expectEqualStrings("

Hello, World!

", html); -} - -test "multiple interpolations" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, "p #{greeting}, #{name}!", .{ - .greeting = "Hello", - .name = "World", - }); - defer allocator.free(html); - - try std.testing.expectEqualStrings("

Hello, World!

", html); -} - -test "attribute with data" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, "a(href=url) Click", .{ .url = "/home" }); - defer allocator.free(html); - - try std.testing.expectEqualStrings("Click", html); -} - -test "buffered code" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, "p= message", .{ .message = "Hello" }); - defer allocator.free(html); - - try std.testing.expectEqualStrings("

Hello

", html); -} - -test "escape html" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, "p #{content}", .{ .content = "bold" }); - defer allocator.free(html); - - try std.testing.expectEqualStrings("

<b>bold</b>

", html); -} - -test "no data - static template" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, "p Hello, World!", .{}); - defer allocator.free(html); - - try std.testing.expectEqualStrings("

Hello, World!

", html); -} - -test "nested tags with data" { - const allocator = std.testing.allocator; - - const html = try renderWithData(allocator, - \\div - \\ h1 #{title} - \\ p #{body} - , .{ - .title = "Welcome", - .body = "Hello there!", - }); - defer allocator.free(html); - - try std.testing.expectEqualStrings("

Welcome

Hello there!

", html); -} diff --git a/src/v1/walk.zig b/src/v1/walk.zig deleted file mode 100644 index ed0cdff..0000000 --- a/src/v1/walk.zig +++ /dev/null @@ -1,901 +0,0 @@ -// walk.zig - Zig port of pug-walk -// -// AST traversal utility with visitor pattern for Pug AST nodes. -// Provides before/after callbacks for each node with optional replacement. - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -// Import AST types from parser -const parser = @import("parser.zig"); -pub const Node = parser.Node; -pub const NodeType = parser.NodeType; - -// ============================================================================ -// Walk Options -// ============================================================================ - -pub const WalkOptions = struct { - /// Include dependencies (traverse into FileReference.ast if present) - include_dependencies: bool = false, - /// Parent node stack (managed internally during walk) - parents: std.ArrayListUnmanaged(*Node) = .{}, - - pub fn deinit(self: *WalkOptions, allocator: Allocator) void { - self.parents.deinit(allocator); - } - - /// Initialize with pre-allocated capacity for expected tree depth - /// Reduces allocations during deep AST traversal - pub fn initWithCapacity(allocator: Allocator, capacity: usize) !WalkOptions { - var opts = WalkOptions{}; - try opts.parents.ensureTotalCapacity(allocator, capacity); - return opts; - } -}; - -// ============================================================================ -// Replace Result -// ============================================================================ - -/// Result of a replace operation -pub const ReplaceResult = union(enum) { - /// Keep the current node unchanged - keep, - /// Replace with a single node - single: *Node, - /// Replace with multiple nodes (only valid in Block/NamedBlock contexts) - multiple: []*Node, - /// Remove the node (replace with nothing) - remove, -}; - -// ============================================================================ -// Callback Types -// ============================================================================ - -/// Before callback - called before visiting children -/// Return false to skip traversing children, null to continue -/// Can use ReplaceResult to replace the current node -pub const BeforeCallback = *const fn ( - node: *Node, - replace_allowed: bool, - ctx: *WalkContext, -) WalkError!?ReplaceResult; - -/// After callback - called after visiting children -pub const AfterCallback = *const fn ( - node: *Node, - replace_allowed: bool, - ctx: *WalkContext, -) WalkError!?ReplaceResult; - -// ============================================================================ -// Walk Context -// ============================================================================ - -pub const WalkContext = struct { - allocator: Allocator, - options: *WalkOptions, - user_data: ?*anyopaque = null, - - /// Get parent at index (0 = immediate parent, 1 = grandparent, etc.) - /// Uses reverse indexing since parents are stored with oldest first (stack-like append/pop) - pub fn getParent(self: *WalkContext, index: usize) ?*Node { - const len = self.options.parents.items.len; - if (index >= len) return null; - // Reverse index: 0 = last item (immediate parent), 1 = second-to-last, etc. - return self.options.parents.items[len - 1 - index]; - } - - /// Get the immediate parent node - pub fn parent(self: *WalkContext) ?*Node { - const items = self.options.parents.items; - if (items.len == 0) return null; - return items[items.len - 1]; - } - - /// Get number of parents in the stack - pub fn depth(self: *WalkContext) usize { - return self.options.parents.items.len; - } -}; - -// ============================================================================ -// Walk Errors -// ============================================================================ - -pub const WalkError = error{ - OutOfMemory, - ArrayReplaceNotAllowed, - UnexpectedNodeType, -}; - -// ============================================================================ -// Walk Implementation -// ============================================================================ - -/// Walk the AST tree, calling before/after callbacks for each node -pub fn walkAST( - allocator: Allocator, - ast: *Node, - before: ?BeforeCallback, - after: ?AfterCallback, - options: *WalkOptions, -) WalkError!*Node { - return walkASTWithUserData(allocator, ast, before, after, options, null); -} - -/// Walk the AST tree with user-provided context data -pub fn walkASTWithUserData( - allocator: Allocator, - ast: *Node, - before: ?BeforeCallback, - after: ?AfterCallback, - options: *WalkOptions, - user_data: ?*anyopaque, -) WalkError!*Node { - var current = ast; - - // Check if array replacement is allowed based on parent context - const replace_allowed = isArrayReplaceAllowed(options, current); - - var ctx = WalkContext{ - .allocator = allocator, - .options = options, - .user_data = user_data, - }; - - // Call before callback - if (before) |before_fn| { - if (try before_fn(current, replace_allowed, &ctx)) |result| { - switch (result) { - .keep => {}, - .single => |replacement| { - current = replacement; - }, - .multiple => { - // Array replacement - return the original node as marker - // The caller (walkAndMergeNodes) will handle expansion - return current; - }, - .remove => { - // Return null marker - handled by caller - return current; - }, - } - } - } - - // Push current node to parents stack (O(1) append instead of O(n) insert at 0) - try options.parents.append(allocator, current); - defer _ = options.parents.pop(); - - // Visit children based on node type - try visitChildren(allocator, current, before, after, options, user_data); - - // Call after callback - if (after) |after_fn| { - if (try after_fn(current, replace_allowed, &ctx)) |result| { - switch (result) { - .keep => {}, - .single => |replacement| { - current = replacement; - }, - .multiple, .remove => { - // Handled by caller - }, - } - } - } - - return current; -} - -/// Check if array replacement is allowed for the current context -fn isArrayReplaceAllowed(options: *WalkOptions, node: *Node) bool { - const items = options.parents.items; - if (items.len == 0) return false; - - // Get immediate parent (last item in stack) - const parent_node = items[items.len - 1]; - - // Array replacement allowed in Block/NamedBlock - if (parent_node.type == .Block or parent_node.type == .NamedBlock) { - return true; - } - - // Also allowed for IncludeFilter in RawInclude - if (parent_node.type == .RawInclude and node.type == .IncludeFilter) { - return true; - } - - return false; -} - -/// Visit children of a node based on its type -fn visitChildren( - allocator: Allocator, - node: *Node, - before: ?BeforeCallback, - after: ?AfterCallback, - options: *WalkOptions, - user_data: ?*anyopaque, -) WalkError!void { - switch (node.type) { - .NamedBlock, .Block => { - // Walk and merge nodes - try walkAndMergeNodes(allocator, &node.nodes, before, after, options, user_data); - }, - - .Case, .Filter, .Mixin, .Tag, .InterpolatedTag, .When, .Code, .While => { - // Walk block if present (represented by non-empty nodes) - if (node.nodes.items.len > 0) { - // Find the block node (first child that is a Block) - for (node.nodes.items, 0..) |child, i| { - if (child.type == .Block or child.type == .NamedBlock) { - node.nodes.items[i] = try walkASTWithUserData( - allocator, - child, - before, - after, - options, - user_data, - ); - } - } - } - }, - - .Each => { - // Walk block - if (node.nodes.items.len > 0) { - for (node.nodes.items, 0..) |child, i| { - if (child.type == .Block or child.type == .NamedBlock) { - node.nodes.items[i] = try walkASTWithUserData( - allocator, - child, - before, - after, - options, - user_data, - ); - } - } - } - // Walk alternate - if (node.alternate) |alt| { - node.alternate = try walkASTWithUserData( - allocator, - alt, - before, - after, - options, - user_data, - ); - } - }, - - .EachOf => { - // Walk block only - if (node.nodes.items.len > 0) { - for (node.nodes.items, 0..) |child, i| { - if (child.type == .Block or child.type == .NamedBlock) { - node.nodes.items[i] = try walkASTWithUserData( - allocator, - child, - before, - after, - options, - user_data, - ); - } - } - } - }, - - .Conditional => { - // Walk consequent - if (node.consequent) |cons| { - node.consequent = try walkASTWithUserData( - allocator, - cons, - before, - after, - options, - user_data, - ); - } - // Walk alternate - if (node.alternate) |alt| { - node.alternate = try walkASTWithUserData( - allocator, - alt, - before, - after, - options, - user_data, - ); - } - }, - - .Include => { - // Walk block (represented as child nodes) - try walkAndMergeNodes(allocator, &node.nodes, before, after, options, user_data); - // Note: file is a FileReference struct, not a Node, so we don't walk it - }, - - .Extends => { - // Note: file is a FileReference struct, not a Node - }, - - .RawInclude => { - // Walk filters - try walkAndMergeNodes(allocator, &node.filters, before, after, options, user_data); - // Note: file is a FileReference struct - }, - - .FileReference => { - // Walk into ast if includeDependencies is set - // Note: In our implementation, FileReference doesn't hold a nested AST directly - // This would need to be handled by the loader - _ = options.include_dependencies; - }, - - // Leaf nodes - no children to visit - .AttributeBlock, - .BlockComment, - .Comment, - .Doctype, - .IncludeFilter, - .MixinBlock, - .YieldBlock, - .Text, - => {}, - } -} - -/// Walk a list of nodes and merge results (handling array replacements) -fn walkAndMergeNodes( - allocator: Allocator, - nodes: *std.ArrayListUnmanaged(*Node), - before: ?BeforeCallback, - after: ?AfterCallback, - options: *WalkOptions, - user_data: ?*anyopaque, -) WalkError!void { - var i: usize = 0; - while (i < nodes.items.len) { - const result = try walkASTWithUserData( - allocator, - nodes.items[i], - before, - after, - options, - user_data, - ); - - // Update the node in place - nodes.items[i] = result; - i += 1; - } -} - -// ============================================================================ -// Convenience Functions -// ============================================================================ - -/// Simple walk that just calls a callback for each node (no replacement) -pub fn walk( - allocator: Allocator, - ast: *Node, - callback: *const fn (node: *Node, ctx: *WalkContext) WalkError!void, -) WalkError!void { - const Wrapper = struct { - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const cb: *const fn (*Node, *WalkContext) WalkError!void = @ptrCast(@alignCast(ctx.user_data.?)); - try cb(node, ctx); - return null; - } - }; - - var options = WalkOptions{}; - defer options.deinit(allocator); - - _ = try walkASTWithUserData( - allocator, - ast, - Wrapper.before, - null, - &options, - @ptrCast(@constCast(callback)), - ); -} - -/// Count nodes of a specific type -pub fn countNodes(allocator: Allocator, ast: *Node, node_type: NodeType) WalkError!usize { - const Counter = struct { - count: usize = 0, - target_type: NodeType, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - if (node.type == self.target_type) { - self.count += 1; - } - return null; - } - }; - - var counter = Counter{ .target_type = node_type }; - var options = WalkOptions{}; - defer options.deinit(allocator); - - _ = try walkASTWithUserData( - allocator, - ast, - Counter.before, - null, - &options, - &counter, - ); - - return counter.count; -} - -/// Find the first node matching a predicate -pub fn findNode( - allocator: Allocator, - ast: *Node, - predicate: *const fn (node: *Node) bool, -) WalkError!?*Node { - const Finder = struct { - found: ?*Node = null, - pred: *const fn (*Node) bool, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - if (self.found == null and self.pred(node)) { - self.found = node; - } - return null; - } - }; - - var finder = Finder{ .pred = predicate }; - var options = WalkOptions{}; - defer options.deinit(allocator); - - _ = try walkASTWithUserData( - allocator, - ast, - Finder.before, - null, - &options, - &finder, - ); - - return finder.found; -} - -/// Collect all nodes of a specific type -pub fn collectNodes( - allocator: Allocator, - ast: *Node, - node_type: NodeType, -) WalkError!std.ArrayListUnmanaged(*Node) { - const Collector = struct { - collected: std.ArrayListUnmanaged(*Node) = .{}, - alloc: Allocator, - target_type: NodeType, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - if (node.type == self.target_type) { - self.collected.append(self.alloc, node) catch return error.OutOfMemory; - } - return null; - } - }; - - var collector = Collector{ - .alloc = allocator, - .target_type = node_type, - }; - var options = WalkOptions{}; - defer options.deinit(allocator); - - _ = try walkASTWithUserData( - allocator, - ast, - Collector.before, - null, - &options, - &collector, - ); - - return collector.collected; -} - -// ============================================================================ -// Tests -// ============================================================================ - -test "walkAST - basic traversal" { - const allocator = std.testing.allocator; - - // Create a simple AST: Block -> Tag -> Text - const text_node = try allocator.create(Node); - text_node.* = Node{ - .type = .Text, - .val = "Hello", - .line = 1, - .column = 1, - }; - - var tag_block = try allocator.create(Node); - tag_block.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try tag_block.nodes.append(allocator, text_node); - - var tag_node = try allocator.create(Node); - tag_node.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - try tag_node.nodes.append(allocator, tag_block); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag_node); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Count nodes - const count = try countNodes(allocator, root, .Tag); - try std.testing.expectEqual(@as(usize, 1), count); - - const block_count = try countNodes(allocator, root, .Block); - try std.testing.expectEqual(@as(usize, 2), block_count); - - const text_count = try countNodes(allocator, root, .Text); - try std.testing.expectEqual(@as(usize, 1), text_count); -} - -test "walkAST - conditional traversal" { - const allocator = std.testing.allocator; - - // Create AST with conditional: if (test) then else - const then_block = try allocator.create(Node); - then_block.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - - const else_block = try allocator.create(Node); - else_block.* = Node{ - .type = .Block, - .line = 2, - .column = 1, - }; - - const cond_node = try allocator.create(Node); - cond_node.* = Node{ - .type = .Conditional, - .test_expr = "true", - .consequent = then_block, - .alternate = else_block, - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, cond_node); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Count all blocks (root + then + else = 3) - const block_count = try countNodes(allocator, root, .Block); - try std.testing.expectEqual(@as(usize, 3), block_count); - - // Count conditionals - const cond_count = try countNodes(allocator, root, .Conditional); - try std.testing.expectEqual(@as(usize, 1), cond_count); -} - -test "walkAST - each with alternate" { - const allocator = std.testing.allocator; - - // Create Each node with block and alternate - const loop_block = try allocator.create(Node); - loop_block.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - - const alt_block = try allocator.create(Node); - alt_block.* = Node{ - .type = .Block, - .line = 2, - .column = 1, - }; - - var each_node = try allocator.create(Node); - each_node.* = Node{ - .type = .Each, - .val = "item", - .obj = "items", - .alternate = alt_block, - .line = 1, - .column = 1, - }; - try each_node.nodes.append(allocator, loop_block); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, each_node); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Count blocks (root + loop_block + alt_block = 3) - const block_count = try countNodes(allocator, root, .Block); - try std.testing.expectEqual(@as(usize, 3), block_count); - - // Count each nodes - const each_count = try countNodes(allocator, root, .Each); - try std.testing.expectEqual(@as(usize, 1), each_count); -} - -test "walkAST - findNode" { - const allocator = std.testing.allocator; - - // Create a simple AST with multiple tags - const tag1 = try allocator.create(Node); - tag1.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - - const tag2 = try allocator.create(Node); - tag2.* = Node{ - .type = .Tag, - .name = "span", - .line = 2, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag1); - try root.nodes.append(allocator, tag2); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Find span tag - const isSpan = struct { - fn check(node: *Node) bool { - return node.type == .Tag and - node.name != null and - std.mem.eql(u8, node.name.?, "span"); - } - }.check; - - const found = try findNode(allocator, root, isSpan); - try std.testing.expect(found != null); - try std.testing.expectEqualStrings("span", found.?.name.?); -} - -test "walkAST - collectNodes" { - const allocator = std.testing.allocator; - - // Create AST with multiple text nodes - const text1 = try allocator.create(Node); - text1.* = Node{ - .type = .Text, - .val = "Hello", - .line = 1, - .column = 1, - }; - - const text2 = try allocator.create(Node); - text2.* = Node{ - .type = .Text, - .val = "World", - .line = 2, - .column = 1, - }; - - const tag = try allocator.create(Node); - tag.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, text1); - try root.nodes.append(allocator, tag); - try root.nodes.append(allocator, text2); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Collect all text nodes - var collected = try collectNodes(allocator, root, .Text); - defer collected.deinit(allocator); - - try std.testing.expectEqual(@as(usize, 2), collected.items.len); - try std.testing.expectEqualStrings("Hello", collected.items[0].val.?); - try std.testing.expectEqualStrings("World", collected.items[1].val.?); -} - -test "walkAST - parent tracking" { - const allocator = std.testing.allocator; - - // Create nested structure - const inner_text = try allocator.create(Node); - inner_text.* = Node{ - .type = .Text, - .val = "nested", - .line = 1, - .column = 1, - }; - - var inner_block = try allocator.create(Node); - inner_block.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try inner_block.nodes.append(allocator, inner_text); - - var tag = try allocator.create(Node); - tag.* = Node{ - .type = .Tag, - .name = "div", - .line = 1, - .column = 1, - }; - try tag.nodes.append(allocator, inner_block); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, tag); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Track parent depths for text node - const ParentTracker = struct { - text_depth: usize = 0, - text_parent_type: ?NodeType = null, - - fn before(node: *Node, _: bool, ctx: *WalkContext) WalkError!?ReplaceResult { - const self: *@This() = @ptrCast(@alignCast(ctx.user_data.?)); - if (node.type == .Text) { - self.text_depth = ctx.depth(); - if (ctx.parent()) |p| { - self.text_parent_type = p.type; - } - } - return null; - } - }; - - var tracker = ParentTracker{}; - var options = WalkOptions{}; - defer options.deinit(allocator); - - _ = try walkASTWithUserData( - allocator, - root, - ParentTracker.before, - null, - &options, - &tracker, - ); - - // Text node should have depth 3 (root Block -> Tag -> inner Block -> Text) - // Parent should be the inner Block - try std.testing.expectEqual(@as(usize, 3), tracker.text_depth); - try std.testing.expectEqual(NodeType.Block, tracker.text_parent_type.?); -} - -test "walkAST - RawInclude with filters" { - const allocator = std.testing.allocator; - - // Create RawInclude with filters - const filter1 = try allocator.create(Node); - filter1.* = Node{ - .type = .IncludeFilter, - .name = "markdown", - .line = 1, - .column = 1, - }; - - const filter2 = try allocator.create(Node); - filter2.* = Node{ - .type = .IncludeFilter, - .name = "escape", - .line = 1, - .column = 1, - }; - - var raw_include = try allocator.create(Node); - raw_include.* = Node{ - .type = .RawInclude, - .line = 1, - .column = 1, - }; - try raw_include.filters.append(allocator, filter1); - try raw_include.filters.append(allocator, filter2); - - var root = try allocator.create(Node); - root.* = Node{ - .type = .Block, - .line = 1, - .column = 1, - }; - try root.nodes.append(allocator, raw_include); - - defer { - root.deinit(allocator); - allocator.destroy(root); - } - - // Count IncludeFilter nodes - const filter_count = try countNodes(allocator, root, .IncludeFilter); - try std.testing.expectEqual(@as(usize, 2), filter_count); -}