From 27c4898706cc19c851d0cf37c301943d77489c19 Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Sat, 24 Jan 2026 23:53:19 +0530 Subject: [PATCH] follow PugJs --- .claude/settings.local.json | 9 - .gitignore | 1 + CLAUDE.md | 462 +- README.md | 5 +- build.zig | 93 +- examples/demo/build.zig | 8 - examples/demo/src/main.zig | 124 +- examples/demo/views/generated.zig | 45 + examples/demo/views/mixin-test.pug | 15 + src/ast.zig | 257 - src/benchmarks/bench.zig | 78 +- src/benchmarks/bench_interpreted.zig | 154 - src/build_templates.zig | 2067 -------- src/codegen.zig | 1423 +++--- src/error.zig | 403 ++ src/lexer.zig | 4238 ++++++++++------- src/linker.zig | 699 +++ src/load.zig | 412 ++ src/mixin.zig | 581 +++ src/parser.zig | 2813 ++++++----- src/playground/benchmark | Bin 0 -> 233448 bytes src/playground/benchmark.zig | 66 + src/playground/benchmark_examples.zig | 274 ++ src/playground/examples/attributes.pug | 8 + src/playground/examples/code.pug | 17 + src/playground/examples/dynamicscript.pug | 5 + src/playground/examples/each.pug | 3 + src/playground/examples/extend-layout.pug | 10 + src/playground/examples/extend.pug | 11 + src/playground/examples/form.pug | 29 + src/playground/examples/includes.pug | 7 + src/playground/examples/layout.pug | 14 + src/playground/examples/mixins.pug | 14 + src/playground/examples/pet.pug | 3 + src/playground/examples/rss.pug | 14 + src/playground/examples/text.pug | 36 + src/playground/examples/whitespace.pug | 11 + src/playground/run_js.js | 70 + src/playground/run_zig | 0 src/playground/run_zig.zig | 120 + src/pug.zig | 457 ++ src/root.zig | 82 +- src/run_playground.zig | 118 + src/runtime.zig | 3820 ++++++--------- src/strip_comments.zig | 353 ++ src/template.zig | 683 +++ src/test-data/pug-attrs/index.test.js | 301 ++ .../__snapshots__/filter-aliases.test.js.snap | 284 ++ .../test/__snapshots__/index.test.js.snap | 1074 +++++ ...ons-applied-to-nested-filters.test.js.snap | 103 + .../test/cases/filters-empty.input.json | 84 + .../test/cases/filters.cdata.input.json | 83 + .../cases/filters.coffeescript.input.json | 84 + .../test/cases/filters.custom.input.json | 101 + .../cases/filters.include.custom.input.json | 91 + .../test/cases/filters.include.custom.pug | 4 + .../test/cases/filters.include.input.json | 160 + .../test/cases/filters.inline.input.json | 56 + .../test/cases/filters.less.input.json | 113 + .../test/cases/filters.markdown.input.json | 70 + .../test/cases/filters.nested.input.json | 161 + .../test/cases/filters.stylus.input.json | 109 + .../test/cases/include-filter-coffee.coffee | 2 + src/test-data/pug-filters/test/cases/some.md | 3 + .../pug-filters/test/custom-filters.js | 9 + .../test/errors-src/dynamic-option.jade | 3 + .../test/errors/dynamic-option.input.json | 37 + .../pug-filters/test/filter-aliases.test.js | 88 + src/test-data/pug-filters/test/index.test.js | 55 + ...-options-applied-to-nested-filters.test.js | 28 + src/test-data/pug-lexer/cases/attr-es2015.pug | 3 + src/test-data/pug-lexer/cases/attrs-data.pug | 7 + src/test-data/pug-lexer/cases/attrs.js.pug | 22 + src/test-data/pug-lexer/cases/attrs.pug | 43 + .../pug-lexer/cases/attrs.unescaped.pug | 3 + src/test-data/pug-lexer/cases/basic.pug | 3 + src/test-data/pug-lexer/cases/blanks.pug | 8 + src/test-data/pug-lexer/cases/block-code.pug | 12 + .../pug-lexer/cases/block-expansion.pug | 5 + .../cases/block-expansion.shorthands.pug | 2 + src/test-data/pug-lexer/cases/blockquote.pug | 4 + .../pug-lexer/cases/blocks-in-blocks.pug | 4 + .../pug-lexer/cases/blocks-in-if.pug | 19 + src/test-data/pug-lexer/cases/case-blocks.pug | 10 + src/test-data/pug-lexer/cases/case.pug | 19 + .../pug-lexer/cases/classes-empty.pug | 3 + src/test-data/pug-lexer/cases/classes.pug | 14 + .../pug-lexer/cases/code.conditionals.pug | 43 + src/test-data/pug-lexer/cases/code.escape.pug | 2 + .../pug-lexer/cases/code.iteration.pug | 35 + src/test-data/pug-lexer/cases/code.pug | 10 + .../pug-lexer/cases/comments-in-case.pug | 10 + src/test-data/pug-lexer/cases/comments.pug | 29 + .../pug-lexer/cases/comments.source.pug | 9 + .../pug-lexer/cases/doctype.custom.pug | 1 + .../pug-lexer/cases/doctype.default.pug | 4 + .../pug-lexer/cases/doctype.keyword.pug | 1 + src/test-data/pug-lexer/cases/each.else.pug | 52 + .../pug-lexer/cases/escape-chars.pug | 2 + src/test-data/pug-lexer/cases/escape-test.pug | 8 + .../cases/escaping-class-attribute.pug | 6 + .../pug-lexer/cases/filter-in-include.pug | 1 + .../pug-lexer/cases/filters-empty.pug | 6 + .../pug-lexer/cases/filters.coffeescript.pug | 6 + .../pug-lexer/cases/filters.custom.pug | 7 + .../cases/filters.include.custom.pug | 4 + .../pug-lexer/cases/filters.include.pug | 7 + .../pug-lexer/cases/filters.inline.pug | 1 + .../pug-lexer/cases/filters.less.pug | 8 + .../pug-lexer/cases/filters.markdown.pug | 5 + .../pug-lexer/cases/filters.nested.pug | 10 + .../pug-lexer/cases/filters.stylus.pug | 7 + .../pug-lexer/cases/filters.verbatim.pug | 6 + src/test-data/pug-lexer/cases/html.pug | 13 + src/test-data/pug-lexer/cases/html5.pug | 4 + .../cases/include-extends-from-root.pug | 1 + .../include-extends-of-common-template.pug | 2 + .../cases/include-extends-relative.pug | 1 + .../cases/include-only-text-body.pug | 3 + .../pug-lexer/cases/include-only-text.pug | 5 + .../cases/include-with-text-head.pug | 3 + .../pug-lexer/cases/include-with-text.pug | 4 + .../pug-lexer/cases/include.script.pug | 2 + .../pug-lexer/cases/include.yield.nested.pug | 4 + .../pug-lexer/cases/includes-with-ext-js.pug | 3 + src/test-data/pug-lexer/cases/includes.pug | 10 + .../cases/inheritance.alert-dialog.pug | 6 + .../pug-lexer/cases/inheritance.defaults.pug | 6 + .../cases/inheritance.extend.include.pug | 13 + .../cases/inheritance.extend.mixins.block.pug | 4 + .../cases/inheritance.extend.mixins.pug | 11 + .../pug-lexer/cases/inheritance.extend.pug | 9 + .../cases/inheritance.extend.recursive.pug | 4 + .../cases/inheritance.extend.whitespace.pug | 13 + src/test-data/pug-lexer/cases/inheritance.pug | 9 + .../pug-lexer/cases/inline-block-comment.pug | 3 + src/test-data/pug-lexer/cases/inline-tag.pug | 19 + .../pug-lexer/cases/intepolated-elements.pug | 3 + .../pug-lexer/cases/interpolated-mixin.pug | 4 + .../pug-lexer/cases/interpolation.escape.pug | 6 + .../pug-lexer/cases/javascript-new-lines.js | 1 + .../pug-lexer/cases/layout.append.pug | 6 + .../cases/layout.append.without-block.pug | 6 + .../layout.multi.append.prepend.block.pug | 19 + .../pug-lexer/cases/layout.prepend.pug | 6 + .../cases/layout.prepend.without-block.pug | 6 + .../pug-lexer/cases/mixin-at-end-of-file.pug | 4 + .../cases/mixin-block-with-space.pug | 6 + src/test-data/pug-lexer/cases/mixin-hoist.pug | 7 + .../pug-lexer/cases/mixin-via-include.pug | 5 + src/test-data/pug-lexer/cases/mixin.attrs.pug | 59 + .../cases/mixin.block-tag-behaviour.pug | 24 + .../pug-lexer/cases/mixin.blocks.pug | 44 + src/test-data/pug-lexer/cases/mixin.merge.pug | 15 + .../pug-lexer/cases/mixins-unused.pug | 3 + src/test-data/pug-lexer/cases/mixins.pug | 32 + .../pug-lexer/cases/mixins.rest-args.pug | 6 + src/test-data/pug-lexer/cases/namespaces.pug | 2 + src/test-data/pug-lexer/cases/nesting.pug | 8 + .../pug-lexer/cases/pipeless-comments.pug | 4 + .../pug-lexer/cases/pipeless-filters.pug | 4 + .../pug-lexer/cases/pipeless-tag.pug | 3 + src/test-data/pug-lexer/cases/pre.pug | 10 + src/test-data/pug-lexer/cases/quotes.pug | 2 + .../pug-lexer/cases/regression.1794.pug | 4 + .../pug-lexer/cases/regression.784.pug | 2 + .../pug-lexer/cases/script.whitespace.pug | 6 + .../pug-lexer/cases/scripts.non-js.pug | 9 + src/test-data/pug-lexer/cases/scripts.pug | 8 + .../pug-lexer/cases/self-closing-html.pug | 4 + .../pug-lexer/cases/single-period.pug | 1 + src/test-data/pug-lexer/cases/source.pug | 4 + src/test-data/pug-lexer/cases/styles.pug | 19 + src/test-data/pug-lexer/cases/tag-blocks.pug | 5 + .../pug-lexer/cases/tag.interpolation.pug | 22 + .../pug-lexer/cases/tags.self-closing.pug | 19 + src/test-data/pug-lexer/cases/template.pug | 9 + src/test-data/pug-lexer/cases/text-block.pug | 6 + src/test-data/pug-lexer/cases/text.pug | 46 + src/test-data/pug-lexer/cases/utf8bom.pug | 1 + src/test-data/pug-lexer/cases/vars.pug | 3 + src/test-data/pug-lexer/cases/while.pug | 5 + src/test-data/pug-lexer/cases/xml.pug | 3 + .../cases/yield-before-conditional-head.pug | 5 + .../cases/yield-before-conditional.pug | 5 + src/test-data/pug-lexer/cases/yield-head.pug | 4 + .../pug-lexer/cases/yield-title-head.pug | 5 + src/test-data/pug-lexer/cases/yield-title.pug | 4 + src/test-data/pug-lexer/cases/yield.pug | 5 + .../errors/attribute-invalid-expression.pug | 3 + .../errors/case-with-invalid-expression.pug | 1 + .../errors/case-with-no-expression.pug | 1 + .../errors/default-with-expression.pug | 1 + .../pug-lexer/errors/else-with-condition.pug | 4 + .../pug-lexer/errors/extends-no-path.pug | 1 + .../errors/include-filter-no-path-2.pug | 1 + .../errors/include-filter-no-path.pug | 1 + .../errors/include-filter-no-space.pug | 1 + .../pug-lexer/errors/include-no-path.pug | 1 + .../errors/inconsistent-indentation.pug | 4 + .../pug-lexer/errors/interpolated-call.pug | 5 + .../pug-lexer/errors/invalid-class-name-1.pug | 1 + .../pug-lexer/errors/invalid-class-name-2.pug | 1 + .../pug-lexer/errors/invalid-class-name-3.pug | 1 + src/test-data/pug-lexer/errors/invalid-id.pug | 1 + .../pug-lexer/errors/malformed-each.pug | 1 + .../pug-lexer/errors/malformed-extend.pug | 1 + .../pug-lexer/errors/malformed-include.pug | 1 + .../errors/mismatched-inline-tag.pug | 2 + .../errors/mismatched-tag-interpolation.pug | 2 + .../errors/multi-line-interpolation.pug | 4 + .../pug-lexer/errors/old-prefixed-each.pug | 3 + .../pug-lexer/errors/open-interpolation.pug | 1 + .../errors/when-with-no-expression.pug | 1 + .../errors/while-with-no-expression.pug | 1 + .../test/__snapshots__/index.test.js.snap | 4155 ++++++++++++++++ .../test/cases-src/auxiliary/1794-extends.pug | 1 + .../test/cases-src/auxiliary/1794-include.pug | 4 + .../auxiliary/blocks-in-blocks-layout.pug | 8 + .../test/cases-src/auxiliary/dialog.pug | 6 + .../test/cases-src/auxiliary/empty-block.pug | 2 + .../test/cases-src/auxiliary/escapes.html | 3 + .../auxiliary/extends-empty-block-1.pug | 5 + .../auxiliary/extends-empty-block-2.pug | 5 + .../cases-src/auxiliary/extends-from-root.pug | 4 + .../cases-src/auxiliary/extends-relative.pug | 4 + .../cases-src/auxiliary/filter-in-include.pug | 8 + .../test/cases-src/auxiliary/includable.js | 8 + .../cases-src/auxiliary/include-from-root.pug | 1 + .../inheritance.extend.mixin.block.pug | 11 + ...nce.extend.recursive-grand-grandparent.pug | 2 + ...heritance.extend.recursive-grandparent.pug | 6 + .../inheritance.extend.recursive-parent.pug | 5 + .../cases-src/auxiliary/layout.include.pug | 7 + .../test/cases-src/auxiliary/layout.pug | 6 + .../auxiliary/mixin-at-end-of-file.pug | 3 + .../test/cases-src/auxiliary/mixins.pug | 3 + .../test/cases-src/auxiliary/pet.pug | 3 + .../test/cases-src/auxiliary/smile.html | 1 + .../test/cases-src/auxiliary/window.pug | 4 + .../test/cases-src/auxiliary/yield-nested.pug | 10 + .../cases-src/include-extends-from-root.pug | 1 + .../include-extends-of-common-template.pug | 2 + .../cases-src/include-extends-relative.pug | 1 + .../cases-src/include-filter-coffee.coffee | 2 + .../test/cases-src/include-filter-stylus.pug | 2 + .../test/cases-src/include-filter.pug | 7 + .../test/cases-src/include-only-text-body.pug | 3 + .../test/cases-src/include-only-text.pug | 5 + .../test/cases-src/include-with-text-head.pug | 3 + .../test/cases-src/include-with-text.pug | 4 + .../test/cases-src/include.script.pug | 2 + .../test/cases-src/include.yield.nested.pug | 4 + .../test/cases-src/includes-with-ext-js.pug | 3 + .../pug-linker/test/cases-src/includes.pug | 10 + .../test/cases-src/javascript-new-lines.js | 1 + .../test/cases-src/layout.append.pug | 6 + .../cases-src/layout.append.without-block.pug | 6 + .../layout.multi.append.prepend.block.pug | 19 + .../test/cases-src/layout.prepend.pug | 6 + .../layout.prepend.without-block.pug | 6 + .../test/cases-src/some-included.styl | 2 + .../pug-linker/test/cases-src/some.md | 3 + .../pug-linker/test/cases-src/some.styl | 1 + .../include-extends-from-root.input.json | 201 + ...lude-extends-of-common-template.input.json | 179 + .../cases/include-extends-relative.input.json | 166 + .../cases/include-filter-stylus.input.json | 52 + .../test/cases/include-filter.input.json | 153 + .../cases/include-only-text-body.input.json | 24 + .../test/cases/include-only-text.input.json | 125 + .../cases/include-with-text-head.input.json | 53 + .../test/cases/include-with-text.input.json | 141 + .../test/cases/include.script.input.json | 130 + .../cases/include.yield.nested.input.json | 260 + .../cases/includes-with-ext-js.input.json | 55 + .../pug-linker/test/cases/includes.input.json | 172 + .../test/cases/layout.append.input.json | 226 + .../layout.append.without-block.input.json | 226 + ...yout.multi.append.prepend.block.input.json | 365 ++ .../test/cases/layout.prepend.input.json | 226 + .../layout.prepend.without-block.input.json | 226 + .../test/errors-src/child-with-tags.pug | 6 + .../test/errors-src/extends-not-first.pug | 4 + .../test/errors-src/unexpected-block.pug | 4 + .../test/errors/child-with-tags.input.json | 163 + .../test/errors/extends-not-first.input.json | 140 + .../test/errors/unexpected-block.input.json | 58 + .../append-without-block/app-layout.pug | 5 + .../fixtures/append-without-block/layout.pug | 7 + .../fixtures/append-without-block/page.pug | 6 + .../test/fixtures/append/app-layout.pug | 5 + .../test/fixtures/append/layout.pug | 7 + .../pug-linker/test/fixtures/append/page.html | 9 + .../pug-linker/test/fixtures/append/page.pug | 6 + .../pug-linker/test/fixtures/empty.pug | 0 .../pug-linker/test/fixtures/layout.pug | 8 + .../pug-linker/test/fixtures/mixins.pug | 2 + .../multi-append-prepend-block/redefine.pug | 5 + .../multi-append-prepend-block/root.pug | 5 + .../prepend-without-block/app-layout.pug | 5 + .../fixtures/prepend-without-block/layout.pug | 7 + .../fixtures/prepend-without-block/page.html | 9 + .../fixtures/prepend-without-block/page.pug | 6 + .../test/fixtures/prepend/app-layout.pug | 5 + .../test/fixtures/prepend/layout.pug | 7 + .../test/fixtures/prepend/page.html | 9 + .../pug-linker/test/fixtures/prepend/page.pug | 6 + src/test-data/pug-linker/test/index.test.js | 46 + .../special-cases-src/extending-empty.pug | 1 + .../special-cases-src/extending-include.pug | 5 + .../test/special-cases-src/root-mixin.pug | 9 + .../special-cases/extending-empty.input.json | 26 + .../extending-include.input.json | 204 + .../test/special-cases/root-mixin.input.json | 212 + .../test/__snapshots__/index.test.js.snap | 166 + src/test-data/pug-load/test/bar.pug | 1 + src/test-data/pug-load/test/bing.pug | 1 + src/test-data/pug-load/test/foo.pug | 6 + src/test-data/pug-load/test/index.test.js | 30 + src/test-data/pug-load/test/script.js | 1 + .../pug-parser/cases/attr-es2015.tokens.json | 9 + .../pug-parser/cases/attrs-data.tokens.json | 33 + .../pug-parser/cases/attrs.js.tokens.json | 78 + .../pug-parser/cases/attrs.tokens.json | 180 + .../cases/attrs.unescaped.tokens.json | 15 + .../pug-parser/cases/basic.tokens.json | 9 + .../pug-parser/cases/blanks.tokens.json | 13 + .../pug-parser/cases/block-code.tokens.json | 28 + .../block-expansion.shorthands.tokens.json | 11 + .../cases/block-expansion.tokens.json | 21 + .../pug-parser/cases/blockquote.tokens.json | 10 + .../cases/blocks-in-blocks.tokens.json | 9 + .../pug-parser/cases/blocks-in-if.tokens.json | 44 + .../pug-parser/cases/case-blocks.tokens.json | 29 + .../pug-parser/cases/case.tokens.json | 61 + .../cases/classes-empty.tokens.json | 15 + .../pug-parser/cases/classes.tokens.json | 27 + .../cases/code.conditionals.tokens.json | 86 + .../pug-parser/cases/code.escape.tokens.json | 6 + .../cases/code.iteration.tokens.json | 71 + .../pug-parser/cases/code.tokens.json | 40 + .../cases/comments-in-case.tokens.json | 26 + .../cases/comments.source.tokens.json | 18 + .../pug-parser/cases/comments.tokens.json | 61 + .../cases/doctype.custom.tokens.json | 2 + .../cases/doctype.default.tokens.json | 11 + .../cases/doctype.keyword.tokens.json | 2 + .../pug-parser/cases/each.else.tokens.json | 88 + .../pug-parser/cases/escape-chars.tokens.json | 6 + .../pug-parser/cases/escape-test.tokens.json | 20 + .../escaping-class-attribute.tokens.json | 31 + .../cases/filter-in-include.tokens.json | 4 + .../cases/filters-empty.tokens.json | 16 + .../cases/filters.coffeescript.tokens.json | 21 + .../cases/filters.custom.tokens.json | 21 + .../cases/filters.include.custom.tokens.json | 17 + .../cases/filters.include.tokens.json | 30 + .../cases/filters.inline.tokens.json | 8 + .../pug-parser/cases/filters.less.tokens.json | 23 + .../cases/filters.markdown.tokens.json | 13 + .../cases/filters.nested.tokens.json | 26 + .../cases/filters.stylus.tokens.json | 20 + .../cases/filters.verbatim.tokens.json | 13 + .../pug-parser/cases/html.tokens.json | 26 + .../pug-parser/cases/html5.tokens.json | 20 + .../include-extends-from-root.tokens.json | 4 + ...ude-extends-of-common-template.tokens.json | 7 + .../include-extends-relative.tokens.json | 4 + .../cases/include-only-text-body.tokens.json | 7 + .../cases/include-only-text.tokens.json | 16 + .../cases/include-with-text-head.tokens.json | 12 + .../cases/include-with-text.tokens.json | 17 + .../cases/include.script.tokens.json | 10 + .../cases/include.yield.nested.tokens.json | 11 + .../cases/includes-with-ext-js.tokens.json | 9 + .../pug-parser/cases/includes.tokens.json | 25 + .../inheritance.alert-dialog.tokens.json | 13 + .../cases/inheritance.defaults.tokens.json | 24 + .../inheritance.extend.include.tokens.json | 28 + ...nheritance.extend.mixins.block.tokens.json | 9 + .../inheritance.extend.mixins.tokens.json | 22 + .../inheritance.extend.recursive.tokens.json | 9 + .../cases/inheritance.extend.tokens.json | 20 + .../inheritance.extend.whitespace.tokens.json | 20 + .../pug-parser/cases/inheritance.tokens.json | 20 + .../cases/inline-block-comment.tokens.json | 10 + .../pug-parser/cases/inline-tag.tokens.json | 77 + .../cases/intepolated-elements.tokens.json | 38 + .../cases/interpolated-mixin.tokens.json | 15 + .../cases/interpolation.escape.tokens.json | 15 + .../cases/layout.append.tokens.json | 17 + .../layout.append.without-block.tokens.json | 17 + ...out.multi.append.prepend.block.tokens.json | 46 + .../cases/layout.prepend.tokens.json | 17 + .../layout.prepend.without-block.tokens.json | 17 + .../cases/mixin-at-end-of-file.tokens.json | 9 + .../cases/mixin-block-with-space.tokens.json | 12 + .../pug-parser/cases/mixin-hoist.tokens.json | 14 + .../cases/mixin-via-include.tokens.json | 7 + .../pug-parser/cases/mixin.attrs.tokens.json | 161 + .../mixin.block-tag-behaviour.tokens.json | 56 + .../pug-parser/cases/mixin.blocks.tokens.json | 108 + .../pug-parser/cases/mixin.merge.tokens.json | 58 + .../cases/mixins-unused.tokens.json | 7 + .../cases/mixins.rest-args.tokens.json | 14 + .../pug-parser/cases/mixins.tokens.json | 65 + .../pug-parser/cases/namespaces.tokens.json | 8 + .../pug-parser/cases/nesting.tokens.json | 23 + .../cases/pipeless-comments.tokens.json | 9 + .../cases/pipeless-filters.tokens.json | 9 + .../pug-parser/cases/pipeless-tag.tokens.json | 14 + .../pug-parser/cases/pre.tokens.json | 25 + .../pug-parser/cases/quotes.tokens.json | 6 + .../cases/regression.1794.tokens.json | 9 + .../cases/regression.784.tokens.json | 5 + .../cases/script.whitespace.tokens.json | 14 + .../cases/scripts.non-js.tokens.json | 29 + .../pug-parser/cases/scripts.tokens.json | 20 + .../cases/self-closing-html.tokens.json | 11 + .../cases/single-period.tokens.json | 3 + .../pug-parser/cases/source.tokens.json | 21 + .../pug-parser/cases/styles.tokens.json | 64 + .../pug-parser/cases/tag-blocks.tokens.json | 16 + .../cases/tag.interpolation.tokens.json | 66 + .../cases/tags.self-closing.tokens.json | 60 + .../pug-parser/cases/template.tokens.json | 27 + .../pug-parser/cases/text-block.tokens.json | 20 + .../pug-parser/cases/text.tokens.json | 91 + .../pug-parser/cases/utf8bom.tokens.json | 4 + .../pug-parser/cases/vars.tokens.json | 10 + .../pug-parser/cases/while.tokens.json | 13 + .../pug-parser/cases/xml.tokens.json | 11 + .../yield-before-conditional-head.tokens.json | 18 + .../yield-before-conditional.tokens.json | 20 + .../pug-parser/cases/yield-head.tokens.json | 15 + .../cases/yield-title-head.tokens.json | 17 + .../pug-parser/cases/yield-title.tokens.json | 12 + .../pug-parser/cases/yield.tokens.json | 20 + src/test-data/pug-runtime/test/index.test.js | 270 ++ .../test/__snapshots__/index.test.js.snap | 1173 +++++ .../test/cases/comments-in-case.input.json | 26 + .../test/cases/comments.input.json | 76 + .../test/cases/comments.source.input.json | 19 + .../test/errors/comment-in-comment.input.json | 3 + .../test/errors/end.input.json | 4 + .../test/errors/startstart.input.json | 6 + .../pug-strip-comments/test/index.test.js | 51 + src/test-data/pug-walk/.gitignore | 14 + src/test-data/pug-walk/.travis.yml | 17 + src/test-data/pug-walk/HISTORY.md | 4 + src/test-data/pug-walk/LICENSE | 19 + src/test-data/pug-walk/README.md | 138 + src/test-data/pug-walk/index.js | 120 + src/test-data/pug-walk/package.json | 21 + src/test-data/pug-walk/test/index.test.js | 178 + src/test-data/pug/examples/README.md | 5 + src/test-data/pug/examples/attributes.js | 10 + src/test-data/pug/examples/attributes.pug | 8 + src/test-data/pug/examples/code.js | 15 + src/test-data/pug/examples/code.pug | 17 + src/test-data/pug/examples/dynamicscript.js | 15 + src/test-data/pug/examples/dynamicscript.pug | 5 + src/test-data/pug/examples/each.js | 15 + src/test-data/pug/examples/each.pug | 3 + src/test-data/pug/examples/extend-layout.pug | 10 + src/test-data/pug/examples/extend.js | 19 + src/test-data/pug/examples/extend.pug | 11 + src/test-data/pug/examples/form.js | 17 + src/test-data/pug/examples/form.pug | 29 + src/test-data/pug/examples/includes.js | 10 + src/test-data/pug/examples/includes.pug | 7 + src/test-data/pug/examples/includes/foot.pug | 2 + src/test-data/pug/examples/includes/head.pug | 6 + .../pug/examples/includes/scripts.pug | 2 + src/test-data/pug/examples/includes/style.css | 5 + src/test-data/pug/examples/layout-debug.js | 10 + src/test-data/pug/examples/layout.js | 10 + src/test-data/pug/examples/layout.pug | 14 + src/test-data/pug/examples/mixins.js | 15 + src/test-data/pug/examples/mixins.pug | 14 + src/test-data/pug/examples/mixins/dialog.pug | 15 + src/test-data/pug/examples/mixins/profile.pug | 10 + src/test-data/pug/examples/pet.pug | 3 + src/test-data/pug/examples/rss.js | 28 + src/test-data/pug/examples/rss.pug | 14 + src/test-data/pug/examples/text.js | 10 + src/test-data/pug/examples/text.pug | 36 + src/test-data/pug/examples/whitespace.js | 10 + src/test-data/pug/examples/whitespace.pug | 11 + src/test-data/pug/test/README.md | 15 + .../pug/test/__snapshots__/pug.test.js.snap | 110 + .../pug/test/anti-cases/attrs.unescaped.pug | 3 + .../pug/test/anti-cases/case-when.pug | 4 + .../pug/test/anti-cases/case-without-with.pug | 2 + .../pug/test/anti-cases/else-condition.pug | 4 + .../pug/test/anti-cases/else-without-if.pug | 2 + .../inlining-a-mixin-after-a-tag.pug | 1 + .../test/anti-cases/key-char-ending-badly.pug | 1 + .../pug/test/anti-cases/key-ending-badly.pug | 1 + .../test/anti-cases/mismatched-inline-tag.pug | 2 + .../anti-cases/mixin-args-syntax-error.pug | 2 + .../anti-cases/mixins-blocks-with-bodies.pug | 3 + .../multiple-non-nested-tags-on-a-line.pug | 1 + .../test/anti-cases/non-existant-filter.pug | 2 + .../pug/test/anti-cases/non-mixin-block.pug | 2 + .../anti-cases/open-brace-in-attributes.pug | 1 + src/test-data/pug/test/anti-cases/readme.md | 1 + .../self-closing-tag-with-block.pug | 2 + .../anti-cases/self-closing-tag-with-body.pug | 1 + .../anti-cases/self-closing-tag-with-code.pug | 1 + .../pug/test/anti-cases/tabs-and-spaces.pug | 3 + .../anti-cases/unclosed-interpolated-call.pug | 1 + .../anti-cases/unclosed-interpolated-tag.pug | 4 + .../anti-cases/unclosed-interpolation.pug | 1 + src/test-data/pug/test/browser/index.html | 10 + src/test-data/pug/test/browser/index.pug | 20 + src/test-data/pug/test/cases-es2015/attr.html | 1 + src/test-data/pug/test/cases-es2015/attr.pug | 3 + src/test-data/pug/test/cases/attrs-data.html | 6 + src/test-data/pug/test/cases/attrs-data.pug | 7 + src/test-data/pug/test/cases/attrs.colon.html | 1 + src/test-data/pug/test/cases/attrs.colon.pug | 9 + src/test-data/pug/test/cases/attrs.html | 20 + src/test-data/pug/test/cases/attrs.js.html | 5 + src/test-data/pug/test/cases/attrs.js.pug | 17 + src/test-data/pug/test/cases/attrs.pug | 43 + .../pug/test/cases/attrs.unescaped.html | 5 + .../pug/test/cases/attrs.unescaped.pug | 3 + .../pug/test/cases/auxiliary/1794-extends.pug | 1 + .../pug/test/cases/auxiliary/1794-include.pug | 4 + .../auxiliary/blocks-in-blocks-layout.pug | 8 + .../pug/test/cases/auxiliary/dialog.pug | 6 + .../pug/test/cases/auxiliary/empty-block.pug | 2 + .../pug/test/cases/auxiliary/escapes.html | 3 + .../cases/auxiliary/extends-empty-block-1.pug | 5 + .../cases/auxiliary/extends-empty-block-2.pug | 5 + .../cases/auxiliary/extends-from-root.pug | 4 + .../test/cases/auxiliary/extends-relative.pug | 4 + .../cases/auxiliary/filter-in-include.pug | 8 + .../pug/test/cases/auxiliary/includable.js | 8 + .../cases/auxiliary/include-from-root.pug | 1 + .../inheritance.extend.mixin.block.pug | 11 + ...nce.extend.recursive-grand-grandparent.pug | 2 + ...heritance.extend.recursive-grandparent.pug | 6 + .../inheritance.extend.recursive-parent.pug | 5 + .../test/cases/auxiliary/layout.include.pug | 7 + .../pug/test/cases/auxiliary/layout.pug | 6 + .../cases/auxiliary/mixin-at-end-of-file.pug | 3 + .../pug/test/cases/auxiliary/mixins.pug | 3 + .../pug/test/cases/auxiliary/pet.pug | 3 + .../pug/test/cases/auxiliary/smile.html | 1 + .../pug/test/cases/auxiliary/window.pug | 4 + .../pug/test/cases/auxiliary/yield-nested.pug | 10 + src/test-data/pug/test/cases/basic.html | 5 + src/test-data/pug/test/cases/basic.pug | 3 + src/test-data/pug/test/cases/blanks.html | 5 + src/test-data/pug/test/cases/blanks.pug | 8 + src/test-data/pug/test/cases/block-code.html | 7 + src/test-data/pug/test/cases/block-code.pug | 12 + .../pug/test/cases/block-expansion.html | 5 + .../pug/test/cases/block-expansion.pug | 5 + .../cases/block-expansion.shorthands.html | 7 + .../test/cases/block-expansion.shorthands.pug | 2 + src/test-data/pug/test/cases/blockquote.html | 4 + src/test-data/pug/test/cases/blockquote.pug | 4 + .../pug/test/cases/blocks-in-blocks.html | 9 + .../pug/test/cases/blocks-in-blocks.pug | 4 + .../pug/test/cases/blocks-in-if.html | 1 + src/test-data/pug/test/cases/blocks-in-if.pug | 19 + src/test-data/pug/test/cases/case-blocks.html | 5 + src/test-data/pug/test/cases/case-blocks.pug | 10 + src/test-data/pug/test/cases/case.html | 8 + src/test-data/pug/test/cases/case.pug | 19 + .../pug/test/cases/classes-empty.html | 1 + .../pug/test/cases/classes-empty.pug | 3 + src/test-data/pug/test/cases/classes.html | 1 + src/test-data/pug/test/cases/classes.pug | 11 + .../pug/test/cases/code.conditionals.html | 11 + .../pug/test/cases/code.conditionals.pug | 43 + src/test-data/pug/test/cases/code.escape.html | 2 + src/test-data/pug/test/cases/code.escape.pug | 2 + src/test-data/pug/test/cases/code.html | 10 + .../pug/test/cases/code.iteration.html | 36 + .../pug/test/cases/code.iteration.pug | 35 + src/test-data/pug/test/cases/code.pug | 10 + .../pug/test/cases/comments-in-case.html | 6 + .../pug/test/cases/comments-in-case.pug | 10 + src/test-data/pug/test/cases/comments.html | 32 + src/test-data/pug/test/cases/comments.pug | 29 + .../pug/test/cases/comments.source.html | 0 .../pug/test/cases/comments.source.pug | 9 + .../pug/test/cases/doctype.custom.html | 1 + .../pug/test/cases/doctype.custom.pug | 1 + .../pug/test/cases/doctype.default.html | 6 + .../pug/test/cases/doctype.default.pug | 4 + .../pug/test/cases/doctype.keyword.html | 1 + .../pug/test/cases/doctype.keyword.pug | 1 + src/test-data/pug/test/cases/each.else.html | 17 + src/test-data/pug/test/cases/each.else.pug | 43 + .../pug/test/cases/escape-chars.html | 1 + src/test-data/pug/test/cases/escape-chars.pug | 2 + src/test-data/pug/test/cases/escape-test.html | 9 + src/test-data/pug/test/cases/escape-test.pug | 8 + .../test/cases/escaping-class-attribute.html | 6 + .../test/cases/escaping-class-attribute.pug | 6 + .../pug/test/cases/filter-in-include.html | 7 + .../pug/test/cases/filter-in-include.pug | 1 + .../pug/test/cases/filters-empty.html | 4 + .../pug/test/cases/filters-empty.pug | 6 + .../pug/test/cases/filters.coffeescript.html | 9 + .../pug/test/cases/filters.coffeescript.pug | 6 + .../pug/test/cases/filters.custom.html | 8 + .../pug/test/cases/filters.custom.pug | 7 + .../test/cases/filters.include.custom.html | 10 + .../pug/test/cases/filters.include.custom.pug | 4 + .../pug/test/cases/filters.include.html | 19 + .../pug/test/cases/filters.include.pug | 7 + .../pug/test/cases/filters.inline.html | 3 + .../pug/test/cases/filters.inline.pug | 1 + .../pug/test/cases/filters.less.html | 7 + src/test-data/pug/test/cases/filters.less.pug | 8 + .../pug/test/cases/filters.markdown.html | 5 + .../pug/test/cases/filters.markdown.pug | 5 + .../pug/test/cases/filters.nested.html | 2 + .../pug/test/cases/filters.nested.pug | 10 + .../pug/test/cases/filters.stylus.html | 8 + .../pug/test/cases/filters.stylus.pug | 7 + src/test-data/pug/test/cases/html.html | 9 + src/test-data/pug/test/cases/html.pug | 13 + src/test-data/pug/test/cases/html5.html | 4 + src/test-data/pug/test/cases/html5.pug | 4 + .../test/cases/include-extends-from-root.html | 8 + .../test/cases/include-extends-from-root.pug | 1 + .../include-extends-of-common-template.html | 2 + .../include-extends-of-common-template.pug | 2 + .../test/cases/include-extends-relative.html | 8 + .../test/cases/include-extends-relative.pug | 1 + .../test/cases/include-filter-coffee.coffee | 2 + .../test/cases/include-only-text-body.html | 1 + .../pug/test/cases/include-only-text-body.pug | 3 + .../pug/test/cases/include-only-text.html | 5 + .../pug/test/cases/include-only-text.pug | 5 + .../test/cases/include-with-text-head.html | 3 + .../pug/test/cases/include-with-text-head.pug | 3 + .../pug/test/cases/include-with-text.html | 7 + .../pug/test/cases/include-with-text.pug | 4 + .../pug/test/cases/include.script.html | 6 + .../pug/test/cases/include.script.pug | 2 + .../pug/test/cases/include.yield.nested.html | 17 + .../pug/test/cases/include.yield.nested.pug | 4 + .../pug/test/cases/includes-with-ext-js.html | 2 + .../pug/test/cases/includes-with-ext-js.pug | 3 + src/test-data/pug/test/cases/includes.html | 18 + src/test-data/pug/test/cases/includes.pug | 10 + .../test/cases/inheritance.alert-dialog.html | 6 + .../test/cases/inheritance.alert-dialog.pug | 6 + .../pug/test/cases/inheritance.defaults.html | 7 + .../pug/test/cases/inheritance.defaults.pug | 6 + .../pug/test/cases/inheritance.extend.html | 10 + .../cases/inheritance.extend.include.html | 14 + .../test/cases/inheritance.extend.include.pug | 13 + .../inheritance.extend.mixins.block.html | 10 + .../cases/inheritance.extend.mixins.block.pug | 4 + .../test/cases/inheritance.extend.mixins.html | 9 + .../test/cases/inheritance.extend.mixins.pug | 11 + .../pug/test/cases/inheritance.extend.pug | 9 + .../cases/inheritance.extend.recursive.html | 4 + .../cases/inheritance.extend.recursive.pug | 4 + .../cases/inheritance.extend.whitespace.html | 10 + .../cases/inheritance.extend.whitespace.pug | 13 + src/test-data/pug/test/cases/inheritance.html | 10 + src/test-data/pug/test/cases/inheritance.pug | 9 + src/test-data/pug/test/cases/inline-tag.html | 21 + src/test-data/pug/test/cases/inline-tag.pug | 19 + .../pug/test/cases/intepolated-elements.html | 4 + .../pug/test/cases/intepolated-elements.pug | 3 + .../pug/test/cases/interpolated-mixin.html | 3 + .../pug/test/cases/interpolated-mixin.pug | 4 + .../pug/test/cases/interpolation.escape.html | 6 + .../pug/test/cases/interpolation.escape.pug | 7 + .../pug/test/cases/javascript-new-lines.js | 1 + .../pug/test/cases/layout.append.html | 9 + .../pug/test/cases/layout.append.pug | 6 + .../cases/layout.append.without-block.html | 9 + .../cases/layout.append.without-block.pug | 6 + .../layout.multi.append.prepend.block.html | 8 + .../layout.multi.append.prepend.block.pug | 19 + .../pug/test/cases/layout.prepend.html | 9 + .../pug/test/cases/layout.prepend.pug | 6 + .../cases/layout.prepend.without-block.html | 9 + .../cases/layout.prepend.without-block.pug | 6 + .../pug/test/cases/mixin-at-end-of-file.html | 3 + .../pug/test/cases/mixin-at-end-of-file.pug | 4 + .../test/cases/mixin-block-with-space.html | 3 + .../pug/test/cases/mixin-block-with-space.pug | 6 + src/test-data/pug/test/cases/mixin-hoist.html | 5 + src/test-data/pug/test/cases/mixin-hoist.pug | 7 + .../pug/test/cases/mixin-via-include.html | 1 + .../pug/test/cases/mixin-via-include.pug | 5 + src/test-data/pug/test/cases/mixin.attrs.html | 32 + src/test-data/pug/test/cases/mixin.attrs.pug | 59 + .../test/cases/mixin.block-tag-behaviour.html | 22 + .../test/cases/mixin.block-tag-behaviour.pug | 24 + .../pug/test/cases/mixin.blocks.html | 34 + src/test-data/pug/test/cases/mixin.blocks.pug | 44 + src/test-data/pug/test/cases/mixin.merge.html | 34 + src/test-data/pug/test/cases/mixin.merge.pug | 15 + .../pug/test/cases/mixins-unused.html | 1 + .../pug/test/cases/mixins-unused.pug | 3 + src/test-data/pug/test/cases/mixins.html | 23 + src/test-data/pug/test/cases/mixins.pug | 32 + .../pug/test/cases/mixins.rest-args.html | 6 + .../pug/test/cases/mixins.rest-args.pug | 6 + src/test-data/pug/test/cases/namespaces.html | 2 + src/test-data/pug/test/cases/namespaces.pug | 2 + src/test-data/pug/test/cases/nesting.html | 11 + src/test-data/pug/test/cases/nesting.pug | 8 + .../pug/test/cases/pipeless-comments.html | 6 + .../pug/test/cases/pipeless-comments.pug | 4 + .../pug/test/cases/pipeless-filters.html | 2 + .../pug/test/cases/pipeless-filters.pug | 4 + .../pug/test/cases/pipeless-tag.html | 3 + src/test-data/pug/test/cases/pipeless-tag.pug | 3 + src/test-data/pug/test/cases/pre.html | 7 + src/test-data/pug/test/cases/pre.pug | 10 + src/test-data/pug/test/cases/quotes.html | 2 + src/test-data/pug/test/cases/quotes.pug | 2 + .../pug/test/cases/regression.1794.html | 1 + .../pug/test/cases/regression.1794.pug | 4 + .../pug/test/cases/regression.784.html | 1 + .../pug/test/cases/regression.784.pug | 2 + .../pug/test/cases/script.whitespace.html | 7 + .../pug/test/cases/script.whitespace.pug | 6 + src/test-data/pug/test/cases/scripts.html | 9 + .../pug/test/cases/scripts.non-js.html | 11 + .../pug/test/cases/scripts.non-js.pug | 9 + src/test-data/pug/test/cases/scripts.pug | 8 + .../pug/test/cases/self-closing-html.html | 4 + .../pug/test/cases/self-closing-html.pug | 4 + .../pug/test/cases/single-period.html | 1 + .../pug/test/cases/single-period.pug | 1 + .../pug/test/cases/some-included.styl | 2 + src/test-data/pug/test/cases/some.md | 3 + src/test-data/pug/test/cases/some.styl | 1 + src/test-data/pug/test/cases/source.html | 6 + src/test-data/pug/test/cases/source.pug | 4 + src/test-data/pug/test/cases/styles.html | 20 + src/test-data/pug/test/cases/styles.pug | 19 + .../pug/test/cases/tag.interpolation.html | 9 + .../pug/test/cases/tag.interpolation.pug | 22 + .../pug/test/cases/tags.self-closing.html | 14 + .../pug/test/cases/tags.self-closing.pug | 19 + src/test-data/pug/test/cases/template.html | 11 + src/test-data/pug/test/cases/template.pug | 9 + src/test-data/pug/test/cases/text-block.html | 6 + src/test-data/pug/test/cases/text-block.pug | 6 + src/test-data/pug/test/cases/text.html | 36 + src/test-data/pug/test/cases/text.pug | 46 + src/test-data/pug/test/cases/utf8bom.html | 1 + src/test-data/pug/test/cases/utf8bom.pug | 1 + src/test-data/pug/test/cases/vars.html | 1 + src/test-data/pug/test/cases/vars.pug | 3 + src/test-data/pug/test/cases/while.html | 11 + src/test-data/pug/test/cases/while.pug | 5 + src/test-data/pug/test/cases/xml.html | 3 + src/test-data/pug/test/cases/xml.pug | 3 + .../cases/yield-before-conditional-head.html | 3 + .../cases/yield-before-conditional-head.pug | 5 + .../test/cases/yield-before-conditional.html | 9 + .../test/cases/yield-before-conditional.pug | 5 + src/test-data/pug/test/cases/yield-head.html | 4 + src/test-data/pug/test/cases/yield-head.pug | 4 + .../pug/test/cases/yield-title-head.html | 5 + .../pug/test/cases/yield-title-head.pug | 5 + src/test-data/pug/test/cases/yield-title.html | 9 + src/test-data/pug/test/cases/yield-title.pug | 4 + src/test-data/pug/test/cases/yield.html | 10 + src/test-data/pug/test/cases/yield.pug | 5 + .../pug/test/dependencies/dependency1.pug | 1 + .../pug/test/dependencies/dependency2.pug | 1 + .../pug/test/dependencies/dependency3.pug | 1 + .../pug/test/dependencies/extends1.pug | 1 + .../pug/test/dependencies/extends2.pug | 1 + .../pug/test/dependencies/include1.pug | 1 + .../pug/test/dependencies/include2.pug | 1 + .../__snapshots__/index.test.js.snap | 5 + .../pug/test/duplicate-block/index.pug | 4 + .../pug/test/duplicate-block/index.test.js | 10 + .../layout-with-duplicate-block.pug | 8 + .../eachOf/__snapshots__/index.test.js.snap | 5 + .../pug/test/eachOf/error/left-side.pug | 3 + .../pug/test/eachOf/error/no-brackets.pug | 3 + .../pug/test/eachOf/error/one-val.pug | 3 + .../pug/test/eachOf/error/right-side.pug | 3 + src/test-data/pug/test/eachOf/index.test.js | 44 + .../pug/test/eachOf/passing/brackets.pug | 3 + .../pug/test/eachOf/passing/no-brackets.pug | 3 + .../pug/test/error.reporting.test.js | 260 + src/test-data/pug/test/examples.test.js | 23 + .../test/extends-not-top-level/default.pug | 2 + .../test/extends-not-top-level/duplicate.pug | 2 + .../pug/test/extends-not-top-level/index.pug | 10 + .../test/extends-not-top-level/index.test.js | 15 + .../append-without-block/app-layout.pug | 5 + .../fixtures/append-without-block/layout.pug | 7 + .../fixtures/append-without-block/page.pug | 6 + .../pug/test/fixtures/append/app-layout.pug | 5 + .../pug/test/fixtures/append/layout.pug | 7 + .../pug/test/fixtures/append/page.html | 9 + .../pug/test/fixtures/append/page.pug | 6 + .../compile.with.include.locals.error.pug | 1 + .../compile.with.include.syntax.error.pug | 1 + .../compile.with.layout.locals.error.pug | 1 + .../compile.with.layout.syntax.error.pug | 1 + ....with.layout.with.include.locals.error.pug | 1 + ....with.layout.with.include.syntax.error.pug | 1 + .../element-with-multiple-attributes.pug | 1 + .../test/fixtures/include.locals.error.pug | 2 + .../test/fixtures/include.syntax.error.pug | 2 + .../fixtures/invalid-block-in-extends.pug | 7 + .../fixtures/issue-1593/include-layout.pug | 2 + .../pug/test/fixtures/issue-1593/include.pug | 4 + .../pug/test/fixtures/issue-1593/index.pug | 7 + .../pug/test/fixtures/issue-1593/layout.pug | 3 + .../pug/test/fixtures/layout.locals.error.pug | 2 + src/test-data/pug/test/fixtures/layout.pug | 6 + .../pug/test/fixtures/layout.syntax.error.pug | 2 + .../fixtures/layout.with.runtime.error.pug | 5 + .../pug/test/fixtures/mixin-include.pug | 5 + .../pug/test/fixtures/mixin.error.pug | 2 + .../multi-append-prepend-block/redefine.pug | 5 + .../multi-append-prepend-block/root.pug | 5 + src/test-data/pug/test/fixtures/perf.pug | 32 + .../prepend-without-block/app-layout.pug | 5 + .../fixtures/prepend-without-block/layout.pug | 7 + .../fixtures/prepend-without-block/page.html | 9 + .../fixtures/prepend-without-block/page.pug | 6 + .../pug/test/fixtures/prepend/app-layout.pug | 5 + .../pug/test/fixtures/prepend/layout.pug | 7 + .../pug/test/fixtures/prepend/page.html | 9 + .../pug/test/fixtures/prepend/page.pug | 6 + .../pug/test/fixtures/runtime.error.pug | 1 + .../test/fixtures/runtime.layout.error.pug | 3 + .../fixtures/runtime.with.mixin.error.pug | 3 + src/test-data/pug/test/fixtures/scripts.pug | 2 + src/test-data/pug/test/markdown-it/comment.md | 1 + .../pug/test/markdown-it/index.test.js | 13 + .../markdown-it/layout-markdown-include.pug | 1 + .../markdown-it/layout-markdown-inline.pug | 2 + .../pug/test/output-es2015/attr.html | 2 + src/test-data/pug/test/plugins.test.js | 18 + src/test-data/pug/test/pug.test.js | 1510 ++++++ .../__snapshots__/index.test.js.snap | 17 + .../pug/test/regression-2436/index.test.js | 11 + .../pug/test/regression-2436/issue1.pug | 7 + .../pug/test/regression-2436/issue2.pug | 7 + .../pug/test/regression-2436/layout.pug | 6 + .../pug/test/regression-2436/other1.pug | 4 + .../pug/test/regression-2436/other2.pug | 4 + .../pug/test/regression-2436/other_layout.pug | 4 + src/test-data/pug/test/run-es2015.test.js | 21 + .../pug/test/run-syntax-errors.test.js | 43 + src/test-data/pug/test/run-utils.js | 154 + src/test-data/pug/test/run.test.js | 20 + .../__snapshots__/index.test.js.snap | 5 + .../pug/test/shadowed-block/base.pug | 4 + .../pug/test/shadowed-block/index.pug | 4 + .../pug/test/shadowed-block/index.test.js | 10 + .../pug/test/shadowed-block/layout.pug | 6 + .../pug/test/temp/input-compileFile.pug | 1 + .../pug/test/temp/input-compileFileClient.pug | 1 + .../pug/test/temp/input-renderFile.pug | 1 + src/tests/check_list/source.html | 8 +- src/tests/check_list_test.zig | 65 +- src/tests/general_test.zig | 927 ++-- src/tests/helper.zig | 67 +- src/tests/inheritance_test.zig | 378 -- src/tests/mixin_debug_test.zig | 33 +- src/tests/parser_test.zig | 490 ++ src/v1/codegen.zig | 815 ++++ src/v1/error.zig | 403 ++ src/{v => v1}/lexer.zig | 865 +++- src/v1/linker.zig | 699 +++ src/v1/load.zig | 412 ++ src/v1/parser.zig | 1646 +++++++ src/v1/pug.zig | 457 ++ src/v1/runtime.zig | 1504 ++++++ src/v1/strip_comments.zig | 353 ++ src/v1/template.zig | 364 ++ src/v1/walk.zig | 901 ++++ src/view_engine.zig | 372 +- src/walk.zig | 901 ++++ 893 files changed, 44597 insertions(+), 10484 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 examples/demo/views/mixin-test.pug delete mode 100644 src/ast.zig delete mode 100644 src/benchmarks/bench_interpreted.zig delete mode 100644 src/build_templates.zig create mode 100644 src/error.zig create mode 100644 src/linker.zig create mode 100644 src/load.zig create mode 100644 src/mixin.zig create mode 100755 src/playground/benchmark create mode 100644 src/playground/benchmark.zig create mode 100644 src/playground/benchmark_examples.zig create mode 100644 src/playground/examples/attributes.pug create mode 100644 src/playground/examples/code.pug create mode 100644 src/playground/examples/dynamicscript.pug create mode 100644 src/playground/examples/each.pug create mode 100644 src/playground/examples/extend-layout.pug create mode 100644 src/playground/examples/extend.pug create mode 100644 src/playground/examples/form.pug create mode 100644 src/playground/examples/includes.pug create mode 100644 src/playground/examples/layout.pug create mode 100644 src/playground/examples/mixins.pug create mode 100644 src/playground/examples/pet.pug create mode 100644 src/playground/examples/rss.pug create mode 100644 src/playground/examples/text.pug create mode 100644 src/playground/examples/whitespace.pug create mode 100644 src/playground/run_js.js create mode 100755 src/playground/run_zig create mode 100644 src/playground/run_zig.zig create mode 100644 src/pug.zig create mode 100644 src/run_playground.zig create mode 100644 src/strip_comments.zig create mode 100644 src/template.zig create mode 100644 src/test-data/pug-attrs/index.test.js create mode 100644 src/test-data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap create mode 100644 src/test-data/pug-filters/test/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap create mode 100644 src/test-data/pug-filters/test/cases/filters-empty.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.cdata.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.coffeescript.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.custom.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.include.custom.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.include.custom.pug create mode 100644 src/test-data/pug-filters/test/cases/filters.include.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.inline.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.less.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.markdown.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.nested.input.json create mode 100644 src/test-data/pug-filters/test/cases/filters.stylus.input.json create mode 100644 src/test-data/pug-filters/test/cases/include-filter-coffee.coffee create mode 100644 src/test-data/pug-filters/test/cases/some.md create mode 100644 src/test-data/pug-filters/test/custom-filters.js create mode 100644 src/test-data/pug-filters/test/errors-src/dynamic-option.jade create mode 100644 src/test-data/pug-filters/test/errors/dynamic-option.input.json create mode 100644 src/test-data/pug-filters/test/filter-aliases.test.js create mode 100644 src/test-data/pug-filters/test/index.test.js create mode 100644 src/test-data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js create mode 100644 src/test-data/pug-lexer/cases/attr-es2015.pug create mode 100644 src/test-data/pug-lexer/cases/attrs-data.pug create mode 100644 src/test-data/pug-lexer/cases/attrs.js.pug create mode 100644 src/test-data/pug-lexer/cases/attrs.pug create mode 100644 src/test-data/pug-lexer/cases/attrs.unescaped.pug create mode 100644 src/test-data/pug-lexer/cases/basic.pug create mode 100644 src/test-data/pug-lexer/cases/blanks.pug create mode 100644 src/test-data/pug-lexer/cases/block-code.pug create mode 100644 src/test-data/pug-lexer/cases/block-expansion.pug create mode 100644 src/test-data/pug-lexer/cases/block-expansion.shorthands.pug create mode 100644 src/test-data/pug-lexer/cases/blockquote.pug create mode 100644 src/test-data/pug-lexer/cases/blocks-in-blocks.pug create mode 100644 src/test-data/pug-lexer/cases/blocks-in-if.pug create mode 100644 src/test-data/pug-lexer/cases/case-blocks.pug create mode 100644 src/test-data/pug-lexer/cases/case.pug create mode 100644 src/test-data/pug-lexer/cases/classes-empty.pug create mode 100644 src/test-data/pug-lexer/cases/classes.pug create mode 100644 src/test-data/pug-lexer/cases/code.conditionals.pug create mode 100644 src/test-data/pug-lexer/cases/code.escape.pug create mode 100644 src/test-data/pug-lexer/cases/code.iteration.pug create mode 100644 src/test-data/pug-lexer/cases/code.pug create mode 100644 src/test-data/pug-lexer/cases/comments-in-case.pug create mode 100644 src/test-data/pug-lexer/cases/comments.pug create mode 100644 src/test-data/pug-lexer/cases/comments.source.pug create mode 100644 src/test-data/pug-lexer/cases/doctype.custom.pug create mode 100644 src/test-data/pug-lexer/cases/doctype.default.pug create mode 100644 src/test-data/pug-lexer/cases/doctype.keyword.pug create mode 100644 src/test-data/pug-lexer/cases/each.else.pug create mode 100644 src/test-data/pug-lexer/cases/escape-chars.pug create mode 100644 src/test-data/pug-lexer/cases/escape-test.pug create mode 100644 src/test-data/pug-lexer/cases/escaping-class-attribute.pug create mode 100644 src/test-data/pug-lexer/cases/filter-in-include.pug create mode 100644 src/test-data/pug-lexer/cases/filters-empty.pug create mode 100644 src/test-data/pug-lexer/cases/filters.coffeescript.pug create mode 100644 src/test-data/pug-lexer/cases/filters.custom.pug create mode 100644 src/test-data/pug-lexer/cases/filters.include.custom.pug create mode 100644 src/test-data/pug-lexer/cases/filters.include.pug create mode 100644 src/test-data/pug-lexer/cases/filters.inline.pug create mode 100644 src/test-data/pug-lexer/cases/filters.less.pug create mode 100644 src/test-data/pug-lexer/cases/filters.markdown.pug create mode 100644 src/test-data/pug-lexer/cases/filters.nested.pug create mode 100644 src/test-data/pug-lexer/cases/filters.stylus.pug create mode 100644 src/test-data/pug-lexer/cases/filters.verbatim.pug create mode 100644 src/test-data/pug-lexer/cases/html.pug create mode 100644 src/test-data/pug-lexer/cases/html5.pug create mode 100644 src/test-data/pug-lexer/cases/include-extends-from-root.pug create mode 100644 src/test-data/pug-lexer/cases/include-extends-of-common-template.pug create mode 100644 src/test-data/pug-lexer/cases/include-extends-relative.pug create mode 100644 src/test-data/pug-lexer/cases/include-only-text-body.pug create mode 100644 src/test-data/pug-lexer/cases/include-only-text.pug create mode 100644 src/test-data/pug-lexer/cases/include-with-text-head.pug create mode 100644 src/test-data/pug-lexer/cases/include-with-text.pug create mode 100644 src/test-data/pug-lexer/cases/include.script.pug create mode 100644 src/test-data/pug-lexer/cases/include.yield.nested.pug create mode 100644 src/test-data/pug-lexer/cases/includes-with-ext-js.pug create mode 100644 src/test-data/pug-lexer/cases/includes.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.alert-dialog.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.defaults.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.extend.include.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.extend.mixins.block.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.extend.mixins.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.extend.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.extend.recursive.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.extend.whitespace.pug create mode 100644 src/test-data/pug-lexer/cases/inheritance.pug create mode 100644 src/test-data/pug-lexer/cases/inline-block-comment.pug create mode 100644 src/test-data/pug-lexer/cases/inline-tag.pug create mode 100644 src/test-data/pug-lexer/cases/intepolated-elements.pug create mode 100644 src/test-data/pug-lexer/cases/interpolated-mixin.pug create mode 100644 src/test-data/pug-lexer/cases/interpolation.escape.pug create mode 100644 src/test-data/pug-lexer/cases/javascript-new-lines.js create mode 100644 src/test-data/pug-lexer/cases/layout.append.pug create mode 100644 src/test-data/pug-lexer/cases/layout.append.without-block.pug create mode 100644 src/test-data/pug-lexer/cases/layout.multi.append.prepend.block.pug create mode 100644 src/test-data/pug-lexer/cases/layout.prepend.pug create mode 100644 src/test-data/pug-lexer/cases/layout.prepend.without-block.pug create mode 100644 src/test-data/pug-lexer/cases/mixin-at-end-of-file.pug create mode 100644 src/test-data/pug-lexer/cases/mixin-block-with-space.pug create mode 100644 src/test-data/pug-lexer/cases/mixin-hoist.pug create mode 100644 src/test-data/pug-lexer/cases/mixin-via-include.pug create mode 100644 src/test-data/pug-lexer/cases/mixin.attrs.pug create mode 100644 src/test-data/pug-lexer/cases/mixin.block-tag-behaviour.pug create mode 100644 src/test-data/pug-lexer/cases/mixin.blocks.pug create mode 100644 src/test-data/pug-lexer/cases/mixin.merge.pug create mode 100644 src/test-data/pug-lexer/cases/mixins-unused.pug create mode 100644 src/test-data/pug-lexer/cases/mixins.pug create mode 100644 src/test-data/pug-lexer/cases/mixins.rest-args.pug create mode 100644 src/test-data/pug-lexer/cases/namespaces.pug create mode 100644 src/test-data/pug-lexer/cases/nesting.pug create mode 100644 src/test-data/pug-lexer/cases/pipeless-comments.pug create mode 100644 src/test-data/pug-lexer/cases/pipeless-filters.pug create mode 100644 src/test-data/pug-lexer/cases/pipeless-tag.pug create mode 100644 src/test-data/pug-lexer/cases/pre.pug create mode 100644 src/test-data/pug-lexer/cases/quotes.pug create mode 100644 src/test-data/pug-lexer/cases/regression.1794.pug create mode 100644 src/test-data/pug-lexer/cases/regression.784.pug create mode 100644 src/test-data/pug-lexer/cases/script.whitespace.pug create mode 100644 src/test-data/pug-lexer/cases/scripts.non-js.pug create mode 100644 src/test-data/pug-lexer/cases/scripts.pug create mode 100644 src/test-data/pug-lexer/cases/self-closing-html.pug create mode 100644 src/test-data/pug-lexer/cases/single-period.pug create mode 100644 src/test-data/pug-lexer/cases/source.pug create mode 100644 src/test-data/pug-lexer/cases/styles.pug create mode 100644 src/test-data/pug-lexer/cases/tag-blocks.pug create mode 100644 src/test-data/pug-lexer/cases/tag.interpolation.pug create mode 100644 src/test-data/pug-lexer/cases/tags.self-closing.pug create mode 100644 src/test-data/pug-lexer/cases/template.pug create mode 100644 src/test-data/pug-lexer/cases/text-block.pug create mode 100644 src/test-data/pug-lexer/cases/text.pug create mode 100644 src/test-data/pug-lexer/cases/utf8bom.pug create mode 100644 src/test-data/pug-lexer/cases/vars.pug create mode 100644 src/test-data/pug-lexer/cases/while.pug create mode 100644 src/test-data/pug-lexer/cases/xml.pug create mode 100644 src/test-data/pug-lexer/cases/yield-before-conditional-head.pug create mode 100644 src/test-data/pug-lexer/cases/yield-before-conditional.pug create mode 100644 src/test-data/pug-lexer/cases/yield-head.pug create mode 100644 src/test-data/pug-lexer/cases/yield-title-head.pug create mode 100644 src/test-data/pug-lexer/cases/yield-title.pug create mode 100644 src/test-data/pug-lexer/cases/yield.pug create mode 100644 src/test-data/pug-lexer/errors/attribute-invalid-expression.pug create mode 100644 src/test-data/pug-lexer/errors/case-with-invalid-expression.pug create mode 100644 src/test-data/pug-lexer/errors/case-with-no-expression.pug create mode 100644 src/test-data/pug-lexer/errors/default-with-expression.pug create mode 100644 src/test-data/pug-lexer/errors/else-with-condition.pug create mode 100644 src/test-data/pug-lexer/errors/extends-no-path.pug create mode 100644 src/test-data/pug-lexer/errors/include-filter-no-path-2.pug create mode 100644 src/test-data/pug-lexer/errors/include-filter-no-path.pug create mode 100644 src/test-data/pug-lexer/errors/include-filter-no-space.pug create mode 100644 src/test-data/pug-lexer/errors/include-no-path.pug create mode 100644 src/test-data/pug-lexer/errors/inconsistent-indentation.pug create mode 100644 src/test-data/pug-lexer/errors/interpolated-call.pug create mode 100644 src/test-data/pug-lexer/errors/invalid-class-name-1.pug create mode 100644 src/test-data/pug-lexer/errors/invalid-class-name-2.pug create mode 100644 src/test-data/pug-lexer/errors/invalid-class-name-3.pug create mode 100644 src/test-data/pug-lexer/errors/invalid-id.pug create mode 100644 src/test-data/pug-lexer/errors/malformed-each.pug create mode 100644 src/test-data/pug-lexer/errors/malformed-extend.pug create mode 100644 src/test-data/pug-lexer/errors/malformed-include.pug create mode 100644 src/test-data/pug-lexer/errors/mismatched-inline-tag.pug create mode 100644 src/test-data/pug-lexer/errors/mismatched-tag-interpolation.pug create mode 100644 src/test-data/pug-lexer/errors/multi-line-interpolation.pug create mode 100644 src/test-data/pug-lexer/errors/old-prefixed-each.pug create mode 100644 src/test-data/pug-lexer/errors/open-interpolation.pug create mode 100644 src/test-data/pug-lexer/errors/when-with-no-expression.pug create mode 100644 src/test-data/pug-lexer/errors/while-with-no-expression.pug create mode 100644 src/test-data/pug-linker/test/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/1794-extends.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/1794-include.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/dialog.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/empty-block.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/escapes.html create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/extends-relative.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/includable.js create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/include-from-root.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/layout.include.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/layout.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/mixins.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/pet.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/smile.html create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/window.pug create mode 100644 src/test-data/pug-linker/test/cases-src/auxiliary/yield-nested.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-extends-from-root.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-extends-of-common-template.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-extends-relative.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-filter-coffee.coffee create mode 100644 src/test-data/pug-linker/test/cases-src/include-filter-stylus.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-filter.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-only-text-body.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-only-text.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-with-text-head.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include-with-text.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include.script.pug create mode 100644 src/test-data/pug-linker/test/cases-src/include.yield.nested.pug create mode 100644 src/test-data/pug-linker/test/cases-src/includes-with-ext-js.pug create mode 100644 src/test-data/pug-linker/test/cases-src/includes.pug create mode 100644 src/test-data/pug-linker/test/cases-src/javascript-new-lines.js create mode 100644 src/test-data/pug-linker/test/cases-src/layout.append.pug create mode 100644 src/test-data/pug-linker/test/cases-src/layout.append.without-block.pug create mode 100644 src/test-data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug create mode 100644 src/test-data/pug-linker/test/cases-src/layout.prepend.pug create mode 100644 src/test-data/pug-linker/test/cases-src/layout.prepend.without-block.pug create mode 100644 src/test-data/pug-linker/test/cases-src/some-included.styl create mode 100644 src/test-data/pug-linker/test/cases-src/some.md create mode 100644 src/test-data/pug-linker/test/cases-src/some.styl create mode 100644 src/test-data/pug-linker/test/cases/include-extends-from-root.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-extends-of-common-template.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-extends-relative.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-filter-stylus.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-filter.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-only-text-body.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-only-text.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-with-text-head.input.json create mode 100644 src/test-data/pug-linker/test/cases/include-with-text.input.json create mode 100644 src/test-data/pug-linker/test/cases/include.script.input.json create mode 100644 src/test-data/pug-linker/test/cases/include.yield.nested.input.json create mode 100644 src/test-data/pug-linker/test/cases/includes-with-ext-js.input.json create mode 100644 src/test-data/pug-linker/test/cases/includes.input.json create mode 100644 src/test-data/pug-linker/test/cases/layout.append.input.json create mode 100644 src/test-data/pug-linker/test/cases/layout.append.without-block.input.json create mode 100644 src/test-data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json create mode 100644 src/test-data/pug-linker/test/cases/layout.prepend.input.json create mode 100644 src/test-data/pug-linker/test/cases/layout.prepend.without-block.input.json create mode 100644 src/test-data/pug-linker/test/errors-src/child-with-tags.pug create mode 100644 src/test-data/pug-linker/test/errors-src/extends-not-first.pug create mode 100644 src/test-data/pug-linker/test/errors-src/unexpected-block.pug create mode 100644 src/test-data/pug-linker/test/errors/child-with-tags.input.json create mode 100644 src/test-data/pug-linker/test/errors/extends-not-first.input.json create mode 100644 src/test-data/pug-linker/test/errors/unexpected-block.input.json create mode 100644 src/test-data/pug-linker/test/fixtures/append-without-block/app-layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/append-without-block/layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/append-without-block/page.pug create mode 100644 src/test-data/pug-linker/test/fixtures/append/app-layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/append/layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/append/page.html create mode 100644 src/test-data/pug-linker/test/fixtures/append/page.pug create mode 100644 src/test-data/pug-linker/test/fixtures/empty.pug create mode 100644 src/test-data/pug-linker/test/fixtures/layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/mixins.pug create mode 100644 src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug create mode 100644 src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug create mode 100644 src/test-data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/prepend-without-block/layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/prepend-without-block/page.html create mode 100644 src/test-data/pug-linker/test/fixtures/prepend-without-block/page.pug create mode 100644 src/test-data/pug-linker/test/fixtures/prepend/app-layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/prepend/layout.pug create mode 100644 src/test-data/pug-linker/test/fixtures/prepend/page.html create mode 100644 src/test-data/pug-linker/test/fixtures/prepend/page.pug create mode 100644 src/test-data/pug-linker/test/index.test.js create mode 100644 src/test-data/pug-linker/test/special-cases-src/extending-empty.pug create mode 100644 src/test-data/pug-linker/test/special-cases-src/extending-include.pug create mode 100644 src/test-data/pug-linker/test/special-cases-src/root-mixin.pug create mode 100644 src/test-data/pug-linker/test/special-cases/extending-empty.input.json create mode 100644 src/test-data/pug-linker/test/special-cases/extending-include.input.json create mode 100644 src/test-data/pug-linker/test/special-cases/root-mixin.input.json create mode 100644 src/test-data/pug-load/test/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug-load/test/bar.pug create mode 100644 src/test-data/pug-load/test/bing.pug create mode 100644 src/test-data/pug-load/test/foo.pug create mode 100644 src/test-data/pug-load/test/index.test.js create mode 100644 src/test-data/pug-load/test/script.js create mode 100644 src/test-data/pug-parser/cases/attr-es2015.tokens.json create mode 100644 src/test-data/pug-parser/cases/attrs-data.tokens.json create mode 100644 src/test-data/pug-parser/cases/attrs.js.tokens.json create mode 100644 src/test-data/pug-parser/cases/attrs.tokens.json create mode 100644 src/test-data/pug-parser/cases/attrs.unescaped.tokens.json create mode 100644 src/test-data/pug-parser/cases/basic.tokens.json create mode 100644 src/test-data/pug-parser/cases/blanks.tokens.json create mode 100644 src/test-data/pug-parser/cases/block-code.tokens.json create mode 100644 src/test-data/pug-parser/cases/block-expansion.shorthands.tokens.json create mode 100644 src/test-data/pug-parser/cases/block-expansion.tokens.json create mode 100644 src/test-data/pug-parser/cases/blockquote.tokens.json create mode 100644 src/test-data/pug-parser/cases/blocks-in-blocks.tokens.json create mode 100644 src/test-data/pug-parser/cases/blocks-in-if.tokens.json create mode 100644 src/test-data/pug-parser/cases/case-blocks.tokens.json create mode 100644 src/test-data/pug-parser/cases/case.tokens.json create mode 100644 src/test-data/pug-parser/cases/classes-empty.tokens.json create mode 100644 src/test-data/pug-parser/cases/classes.tokens.json create mode 100644 src/test-data/pug-parser/cases/code.conditionals.tokens.json create mode 100644 src/test-data/pug-parser/cases/code.escape.tokens.json create mode 100644 src/test-data/pug-parser/cases/code.iteration.tokens.json create mode 100644 src/test-data/pug-parser/cases/code.tokens.json create mode 100644 src/test-data/pug-parser/cases/comments-in-case.tokens.json create mode 100644 src/test-data/pug-parser/cases/comments.source.tokens.json create mode 100644 src/test-data/pug-parser/cases/comments.tokens.json create mode 100644 src/test-data/pug-parser/cases/doctype.custom.tokens.json create mode 100644 src/test-data/pug-parser/cases/doctype.default.tokens.json create mode 100644 src/test-data/pug-parser/cases/doctype.keyword.tokens.json create mode 100644 src/test-data/pug-parser/cases/each.else.tokens.json create mode 100644 src/test-data/pug-parser/cases/escape-chars.tokens.json create mode 100644 src/test-data/pug-parser/cases/escape-test.tokens.json create mode 100644 src/test-data/pug-parser/cases/escaping-class-attribute.tokens.json create mode 100644 src/test-data/pug-parser/cases/filter-in-include.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters-empty.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.coffeescript.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.custom.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.include.custom.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.include.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.inline.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.less.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.markdown.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.nested.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.stylus.tokens.json create mode 100644 src/test-data/pug-parser/cases/filters.verbatim.tokens.json create mode 100644 src/test-data/pug-parser/cases/html.tokens.json create mode 100644 src/test-data/pug-parser/cases/html5.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-extends-from-root.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-extends-of-common-template.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-extends-relative.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-only-text-body.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-only-text.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-with-text-head.tokens.json create mode 100644 src/test-data/pug-parser/cases/include-with-text.tokens.json create mode 100644 src/test-data/pug-parser/cases/include.script.tokens.json create mode 100644 src/test-data/pug-parser/cases/include.yield.nested.tokens.json create mode 100644 src/test-data/pug-parser/cases/includes-with-ext-js.tokens.json create mode 100644 src/test-data/pug-parser/cases/includes.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.alert-dialog.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.defaults.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.extend.include.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.extend.mixins.block.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.extend.mixins.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.extend.recursive.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.extend.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.extend.whitespace.tokens.json create mode 100644 src/test-data/pug-parser/cases/inheritance.tokens.json create mode 100644 src/test-data/pug-parser/cases/inline-block-comment.tokens.json create mode 100644 src/test-data/pug-parser/cases/inline-tag.tokens.json create mode 100644 src/test-data/pug-parser/cases/intepolated-elements.tokens.json create mode 100644 src/test-data/pug-parser/cases/interpolated-mixin.tokens.json create mode 100644 src/test-data/pug-parser/cases/interpolation.escape.tokens.json create mode 100644 src/test-data/pug-parser/cases/layout.append.tokens.json create mode 100644 src/test-data/pug-parser/cases/layout.append.without-block.tokens.json create mode 100644 src/test-data/pug-parser/cases/layout.multi.append.prepend.block.tokens.json create mode 100644 src/test-data/pug-parser/cases/layout.prepend.tokens.json create mode 100644 src/test-data/pug-parser/cases/layout.prepend.without-block.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin-at-end-of-file.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin-block-with-space.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin-hoist.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin-via-include.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin.attrs.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin.block-tag-behaviour.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin.blocks.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixin.merge.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixins-unused.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixins.rest-args.tokens.json create mode 100644 src/test-data/pug-parser/cases/mixins.tokens.json create mode 100644 src/test-data/pug-parser/cases/namespaces.tokens.json create mode 100644 src/test-data/pug-parser/cases/nesting.tokens.json create mode 100644 src/test-data/pug-parser/cases/pipeless-comments.tokens.json create mode 100644 src/test-data/pug-parser/cases/pipeless-filters.tokens.json create mode 100644 src/test-data/pug-parser/cases/pipeless-tag.tokens.json create mode 100644 src/test-data/pug-parser/cases/pre.tokens.json create mode 100644 src/test-data/pug-parser/cases/quotes.tokens.json create mode 100644 src/test-data/pug-parser/cases/regression.1794.tokens.json create mode 100644 src/test-data/pug-parser/cases/regression.784.tokens.json create mode 100644 src/test-data/pug-parser/cases/script.whitespace.tokens.json create mode 100644 src/test-data/pug-parser/cases/scripts.non-js.tokens.json create mode 100644 src/test-data/pug-parser/cases/scripts.tokens.json create mode 100644 src/test-data/pug-parser/cases/self-closing-html.tokens.json create mode 100644 src/test-data/pug-parser/cases/single-period.tokens.json create mode 100644 src/test-data/pug-parser/cases/source.tokens.json create mode 100644 src/test-data/pug-parser/cases/styles.tokens.json create mode 100644 src/test-data/pug-parser/cases/tag-blocks.tokens.json create mode 100644 src/test-data/pug-parser/cases/tag.interpolation.tokens.json create mode 100644 src/test-data/pug-parser/cases/tags.self-closing.tokens.json create mode 100644 src/test-data/pug-parser/cases/template.tokens.json create mode 100644 src/test-data/pug-parser/cases/text-block.tokens.json create mode 100644 src/test-data/pug-parser/cases/text.tokens.json create mode 100644 src/test-data/pug-parser/cases/utf8bom.tokens.json create mode 100644 src/test-data/pug-parser/cases/vars.tokens.json create mode 100644 src/test-data/pug-parser/cases/while.tokens.json create mode 100644 src/test-data/pug-parser/cases/xml.tokens.json create mode 100644 src/test-data/pug-parser/cases/yield-before-conditional-head.tokens.json create mode 100644 src/test-data/pug-parser/cases/yield-before-conditional.tokens.json create mode 100644 src/test-data/pug-parser/cases/yield-head.tokens.json create mode 100644 src/test-data/pug-parser/cases/yield-title-head.tokens.json create mode 100644 src/test-data/pug-parser/cases/yield-title.tokens.json create mode 100644 src/test-data/pug-parser/cases/yield.tokens.json create mode 100644 src/test-data/pug-runtime/test/index.test.js create mode 100644 src/test-data/pug-strip-comments/test/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug-strip-comments/test/cases/comments-in-case.input.json create mode 100644 src/test-data/pug-strip-comments/test/cases/comments.input.json create mode 100644 src/test-data/pug-strip-comments/test/cases/comments.source.input.json create mode 100644 src/test-data/pug-strip-comments/test/errors/comment-in-comment.input.json create mode 100644 src/test-data/pug-strip-comments/test/errors/end.input.json create mode 100644 src/test-data/pug-strip-comments/test/errors/startstart.input.json create mode 100644 src/test-data/pug-strip-comments/test/index.test.js create mode 100644 src/test-data/pug-walk/.gitignore create mode 100644 src/test-data/pug-walk/.travis.yml create mode 100644 src/test-data/pug-walk/HISTORY.md create mode 100644 src/test-data/pug-walk/LICENSE create mode 100644 src/test-data/pug-walk/README.md create mode 100644 src/test-data/pug-walk/index.js create mode 100644 src/test-data/pug-walk/package.json create mode 100644 src/test-data/pug-walk/test/index.test.js create mode 100644 src/test-data/pug/examples/README.md create mode 100644 src/test-data/pug/examples/attributes.js create mode 100644 src/test-data/pug/examples/attributes.pug create mode 100644 src/test-data/pug/examples/code.js create mode 100644 src/test-data/pug/examples/code.pug create mode 100644 src/test-data/pug/examples/dynamicscript.js create mode 100644 src/test-data/pug/examples/dynamicscript.pug create mode 100644 src/test-data/pug/examples/each.js create mode 100644 src/test-data/pug/examples/each.pug create mode 100644 src/test-data/pug/examples/extend-layout.pug create mode 100644 src/test-data/pug/examples/extend.js create mode 100644 src/test-data/pug/examples/extend.pug create mode 100644 src/test-data/pug/examples/form.js create mode 100644 src/test-data/pug/examples/form.pug create mode 100644 src/test-data/pug/examples/includes.js create mode 100644 src/test-data/pug/examples/includes.pug create mode 100644 src/test-data/pug/examples/includes/foot.pug create mode 100644 src/test-data/pug/examples/includes/head.pug create mode 100644 src/test-data/pug/examples/includes/scripts.pug create mode 100644 src/test-data/pug/examples/includes/style.css create mode 100644 src/test-data/pug/examples/layout-debug.js create mode 100644 src/test-data/pug/examples/layout.js create mode 100644 src/test-data/pug/examples/layout.pug create mode 100644 src/test-data/pug/examples/mixins.js create mode 100644 src/test-data/pug/examples/mixins.pug create mode 100644 src/test-data/pug/examples/mixins/dialog.pug create mode 100644 src/test-data/pug/examples/mixins/profile.pug create mode 100644 src/test-data/pug/examples/pet.pug create mode 100644 src/test-data/pug/examples/rss.js create mode 100644 src/test-data/pug/examples/rss.pug create mode 100644 src/test-data/pug/examples/text.js create mode 100644 src/test-data/pug/examples/text.pug create mode 100644 src/test-data/pug/examples/whitespace.js create mode 100644 src/test-data/pug/examples/whitespace.pug create mode 100644 src/test-data/pug/test/README.md create mode 100644 src/test-data/pug/test/__snapshots__/pug.test.js.snap create mode 100644 src/test-data/pug/test/anti-cases/attrs.unescaped.pug create mode 100644 src/test-data/pug/test/anti-cases/case-when.pug create mode 100644 src/test-data/pug/test/anti-cases/case-without-with.pug create mode 100644 src/test-data/pug/test/anti-cases/else-condition.pug create mode 100644 src/test-data/pug/test/anti-cases/else-without-if.pug create mode 100644 src/test-data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug create mode 100644 src/test-data/pug/test/anti-cases/key-char-ending-badly.pug create mode 100644 src/test-data/pug/test/anti-cases/key-ending-badly.pug create mode 100644 src/test-data/pug/test/anti-cases/mismatched-inline-tag.pug create mode 100644 src/test-data/pug/test/anti-cases/mixin-args-syntax-error.pug create mode 100644 src/test-data/pug/test/anti-cases/mixins-blocks-with-bodies.pug create mode 100644 src/test-data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug create mode 100644 src/test-data/pug/test/anti-cases/non-existant-filter.pug create mode 100644 src/test-data/pug/test/anti-cases/non-mixin-block.pug create mode 100644 src/test-data/pug/test/anti-cases/open-brace-in-attributes.pug create mode 100644 src/test-data/pug/test/anti-cases/readme.md create mode 100644 src/test-data/pug/test/anti-cases/self-closing-tag-with-block.pug create mode 100644 src/test-data/pug/test/anti-cases/self-closing-tag-with-body.pug create mode 100644 src/test-data/pug/test/anti-cases/self-closing-tag-with-code.pug create mode 100644 src/test-data/pug/test/anti-cases/tabs-and-spaces.pug create mode 100644 src/test-data/pug/test/anti-cases/unclosed-interpolated-call.pug create mode 100644 src/test-data/pug/test/anti-cases/unclosed-interpolated-tag.pug create mode 100644 src/test-data/pug/test/anti-cases/unclosed-interpolation.pug create mode 100644 src/test-data/pug/test/browser/index.html create mode 100644 src/test-data/pug/test/browser/index.pug create mode 100644 src/test-data/pug/test/cases-es2015/attr.html create mode 100644 src/test-data/pug/test/cases-es2015/attr.pug create mode 100644 src/test-data/pug/test/cases/attrs-data.html create mode 100644 src/test-data/pug/test/cases/attrs-data.pug create mode 100644 src/test-data/pug/test/cases/attrs.colon.html create mode 100644 src/test-data/pug/test/cases/attrs.colon.pug create mode 100644 src/test-data/pug/test/cases/attrs.html create mode 100644 src/test-data/pug/test/cases/attrs.js.html create mode 100644 src/test-data/pug/test/cases/attrs.js.pug create mode 100644 src/test-data/pug/test/cases/attrs.pug create mode 100644 src/test-data/pug/test/cases/attrs.unescaped.html create mode 100644 src/test-data/pug/test/cases/attrs.unescaped.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/1794-extends.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/1794-include.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/dialog.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/empty-block.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/escapes.html create mode 100644 src/test-data/pug/test/cases/auxiliary/extends-empty-block-1.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/extends-empty-block-2.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/extends-from-root.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/extends-relative.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/filter-in-include.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/includable.js create mode 100644 src/test-data/pug/test/cases/auxiliary/include-from-root.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/layout.include.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/layout.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/mixins.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/pet.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/smile.html create mode 100644 src/test-data/pug/test/cases/auxiliary/window.pug create mode 100644 src/test-data/pug/test/cases/auxiliary/yield-nested.pug create mode 100644 src/test-data/pug/test/cases/basic.html create mode 100644 src/test-data/pug/test/cases/basic.pug create mode 100644 src/test-data/pug/test/cases/blanks.html create mode 100644 src/test-data/pug/test/cases/blanks.pug create mode 100644 src/test-data/pug/test/cases/block-code.html create mode 100644 src/test-data/pug/test/cases/block-code.pug create mode 100644 src/test-data/pug/test/cases/block-expansion.html create mode 100644 src/test-data/pug/test/cases/block-expansion.pug create mode 100644 src/test-data/pug/test/cases/block-expansion.shorthands.html create mode 100644 src/test-data/pug/test/cases/block-expansion.shorthands.pug create mode 100644 src/test-data/pug/test/cases/blockquote.html create mode 100644 src/test-data/pug/test/cases/blockquote.pug create mode 100644 src/test-data/pug/test/cases/blocks-in-blocks.html create mode 100644 src/test-data/pug/test/cases/blocks-in-blocks.pug create mode 100644 src/test-data/pug/test/cases/blocks-in-if.html create mode 100644 src/test-data/pug/test/cases/blocks-in-if.pug create mode 100644 src/test-data/pug/test/cases/case-blocks.html create mode 100644 src/test-data/pug/test/cases/case-blocks.pug create mode 100644 src/test-data/pug/test/cases/case.html create mode 100644 src/test-data/pug/test/cases/case.pug create mode 100644 src/test-data/pug/test/cases/classes-empty.html create mode 100644 src/test-data/pug/test/cases/classes-empty.pug create mode 100644 src/test-data/pug/test/cases/classes.html create mode 100644 src/test-data/pug/test/cases/classes.pug create mode 100644 src/test-data/pug/test/cases/code.conditionals.html create mode 100644 src/test-data/pug/test/cases/code.conditionals.pug create mode 100644 src/test-data/pug/test/cases/code.escape.html create mode 100644 src/test-data/pug/test/cases/code.escape.pug create mode 100644 src/test-data/pug/test/cases/code.html create mode 100644 src/test-data/pug/test/cases/code.iteration.html create mode 100644 src/test-data/pug/test/cases/code.iteration.pug create mode 100644 src/test-data/pug/test/cases/code.pug create mode 100644 src/test-data/pug/test/cases/comments-in-case.html create mode 100644 src/test-data/pug/test/cases/comments-in-case.pug create mode 100644 src/test-data/pug/test/cases/comments.html create mode 100644 src/test-data/pug/test/cases/comments.pug create mode 100644 src/test-data/pug/test/cases/comments.source.html create mode 100644 src/test-data/pug/test/cases/comments.source.pug create mode 100644 src/test-data/pug/test/cases/doctype.custom.html create mode 100644 src/test-data/pug/test/cases/doctype.custom.pug create mode 100644 src/test-data/pug/test/cases/doctype.default.html create mode 100644 src/test-data/pug/test/cases/doctype.default.pug create mode 100644 src/test-data/pug/test/cases/doctype.keyword.html create mode 100644 src/test-data/pug/test/cases/doctype.keyword.pug create mode 100644 src/test-data/pug/test/cases/each.else.html create mode 100644 src/test-data/pug/test/cases/each.else.pug create mode 100644 src/test-data/pug/test/cases/escape-chars.html create mode 100644 src/test-data/pug/test/cases/escape-chars.pug create mode 100644 src/test-data/pug/test/cases/escape-test.html create mode 100644 src/test-data/pug/test/cases/escape-test.pug create mode 100644 src/test-data/pug/test/cases/escaping-class-attribute.html create mode 100644 src/test-data/pug/test/cases/escaping-class-attribute.pug create mode 100644 src/test-data/pug/test/cases/filter-in-include.html create mode 100644 src/test-data/pug/test/cases/filter-in-include.pug create mode 100644 src/test-data/pug/test/cases/filters-empty.html create mode 100644 src/test-data/pug/test/cases/filters-empty.pug create mode 100644 src/test-data/pug/test/cases/filters.coffeescript.html create mode 100644 src/test-data/pug/test/cases/filters.coffeescript.pug create mode 100644 src/test-data/pug/test/cases/filters.custom.html create mode 100644 src/test-data/pug/test/cases/filters.custom.pug create mode 100644 src/test-data/pug/test/cases/filters.include.custom.html create mode 100644 src/test-data/pug/test/cases/filters.include.custom.pug create mode 100644 src/test-data/pug/test/cases/filters.include.html create mode 100644 src/test-data/pug/test/cases/filters.include.pug create mode 100644 src/test-data/pug/test/cases/filters.inline.html create mode 100644 src/test-data/pug/test/cases/filters.inline.pug create mode 100644 src/test-data/pug/test/cases/filters.less.html create mode 100644 src/test-data/pug/test/cases/filters.less.pug create mode 100644 src/test-data/pug/test/cases/filters.markdown.html create mode 100644 src/test-data/pug/test/cases/filters.markdown.pug create mode 100644 src/test-data/pug/test/cases/filters.nested.html create mode 100644 src/test-data/pug/test/cases/filters.nested.pug create mode 100644 src/test-data/pug/test/cases/filters.stylus.html create mode 100644 src/test-data/pug/test/cases/filters.stylus.pug create mode 100644 src/test-data/pug/test/cases/html.html create mode 100644 src/test-data/pug/test/cases/html.pug create mode 100644 src/test-data/pug/test/cases/html5.html create mode 100644 src/test-data/pug/test/cases/html5.pug create mode 100644 src/test-data/pug/test/cases/include-extends-from-root.html create mode 100644 src/test-data/pug/test/cases/include-extends-from-root.pug create mode 100644 src/test-data/pug/test/cases/include-extends-of-common-template.html create mode 100644 src/test-data/pug/test/cases/include-extends-of-common-template.pug create mode 100644 src/test-data/pug/test/cases/include-extends-relative.html create mode 100644 src/test-data/pug/test/cases/include-extends-relative.pug create mode 100644 src/test-data/pug/test/cases/include-filter-coffee.coffee create mode 100644 src/test-data/pug/test/cases/include-only-text-body.html create mode 100644 src/test-data/pug/test/cases/include-only-text-body.pug create mode 100644 src/test-data/pug/test/cases/include-only-text.html create mode 100644 src/test-data/pug/test/cases/include-only-text.pug create mode 100644 src/test-data/pug/test/cases/include-with-text-head.html create mode 100644 src/test-data/pug/test/cases/include-with-text-head.pug create mode 100644 src/test-data/pug/test/cases/include-with-text.html create mode 100644 src/test-data/pug/test/cases/include-with-text.pug create mode 100644 src/test-data/pug/test/cases/include.script.html create mode 100644 src/test-data/pug/test/cases/include.script.pug create mode 100644 src/test-data/pug/test/cases/include.yield.nested.html create mode 100644 src/test-data/pug/test/cases/include.yield.nested.pug create mode 100644 src/test-data/pug/test/cases/includes-with-ext-js.html create mode 100644 src/test-data/pug/test/cases/includes-with-ext-js.pug create mode 100644 src/test-data/pug/test/cases/includes.html create mode 100644 src/test-data/pug/test/cases/includes.pug create mode 100644 src/test-data/pug/test/cases/inheritance.alert-dialog.html create mode 100644 src/test-data/pug/test/cases/inheritance.alert-dialog.pug create mode 100644 src/test-data/pug/test/cases/inheritance.defaults.html create mode 100644 src/test-data/pug/test/cases/inheritance.defaults.pug create mode 100644 src/test-data/pug/test/cases/inheritance.extend.html create mode 100644 src/test-data/pug/test/cases/inheritance.extend.include.html create mode 100644 src/test-data/pug/test/cases/inheritance.extend.include.pug create mode 100644 src/test-data/pug/test/cases/inheritance.extend.mixins.block.html create mode 100644 src/test-data/pug/test/cases/inheritance.extend.mixins.block.pug create mode 100644 src/test-data/pug/test/cases/inheritance.extend.mixins.html create mode 100644 src/test-data/pug/test/cases/inheritance.extend.mixins.pug create mode 100644 src/test-data/pug/test/cases/inheritance.extend.pug create mode 100644 src/test-data/pug/test/cases/inheritance.extend.recursive.html create mode 100644 src/test-data/pug/test/cases/inheritance.extend.recursive.pug create mode 100644 src/test-data/pug/test/cases/inheritance.extend.whitespace.html create mode 100644 src/test-data/pug/test/cases/inheritance.extend.whitespace.pug create mode 100644 src/test-data/pug/test/cases/inheritance.html create mode 100644 src/test-data/pug/test/cases/inheritance.pug create mode 100644 src/test-data/pug/test/cases/inline-tag.html create mode 100644 src/test-data/pug/test/cases/inline-tag.pug create mode 100644 src/test-data/pug/test/cases/intepolated-elements.html create mode 100644 src/test-data/pug/test/cases/intepolated-elements.pug create mode 100644 src/test-data/pug/test/cases/interpolated-mixin.html create mode 100644 src/test-data/pug/test/cases/interpolated-mixin.pug create mode 100644 src/test-data/pug/test/cases/interpolation.escape.html create mode 100644 src/test-data/pug/test/cases/interpolation.escape.pug create mode 100644 src/test-data/pug/test/cases/javascript-new-lines.js create mode 100644 src/test-data/pug/test/cases/layout.append.html create mode 100644 src/test-data/pug/test/cases/layout.append.pug create mode 100644 src/test-data/pug/test/cases/layout.append.without-block.html create mode 100644 src/test-data/pug/test/cases/layout.append.without-block.pug create mode 100644 src/test-data/pug/test/cases/layout.multi.append.prepend.block.html create mode 100644 src/test-data/pug/test/cases/layout.multi.append.prepend.block.pug create mode 100644 src/test-data/pug/test/cases/layout.prepend.html create mode 100644 src/test-data/pug/test/cases/layout.prepend.pug create mode 100644 src/test-data/pug/test/cases/layout.prepend.without-block.html create mode 100644 src/test-data/pug/test/cases/layout.prepend.without-block.pug create mode 100644 src/test-data/pug/test/cases/mixin-at-end-of-file.html create mode 100644 src/test-data/pug/test/cases/mixin-at-end-of-file.pug create mode 100644 src/test-data/pug/test/cases/mixin-block-with-space.html create mode 100644 src/test-data/pug/test/cases/mixin-block-with-space.pug create mode 100644 src/test-data/pug/test/cases/mixin-hoist.html create mode 100644 src/test-data/pug/test/cases/mixin-hoist.pug create mode 100644 src/test-data/pug/test/cases/mixin-via-include.html create mode 100644 src/test-data/pug/test/cases/mixin-via-include.pug create mode 100644 src/test-data/pug/test/cases/mixin.attrs.html create mode 100644 src/test-data/pug/test/cases/mixin.attrs.pug create mode 100644 src/test-data/pug/test/cases/mixin.block-tag-behaviour.html create mode 100644 src/test-data/pug/test/cases/mixin.block-tag-behaviour.pug create mode 100644 src/test-data/pug/test/cases/mixin.blocks.html create mode 100644 src/test-data/pug/test/cases/mixin.blocks.pug create mode 100644 src/test-data/pug/test/cases/mixin.merge.html create mode 100644 src/test-data/pug/test/cases/mixin.merge.pug create mode 100644 src/test-data/pug/test/cases/mixins-unused.html create mode 100644 src/test-data/pug/test/cases/mixins-unused.pug create mode 100644 src/test-data/pug/test/cases/mixins.html create mode 100644 src/test-data/pug/test/cases/mixins.pug create mode 100644 src/test-data/pug/test/cases/mixins.rest-args.html create mode 100644 src/test-data/pug/test/cases/mixins.rest-args.pug create mode 100644 src/test-data/pug/test/cases/namespaces.html create mode 100644 src/test-data/pug/test/cases/namespaces.pug create mode 100644 src/test-data/pug/test/cases/nesting.html create mode 100644 src/test-data/pug/test/cases/nesting.pug create mode 100644 src/test-data/pug/test/cases/pipeless-comments.html create mode 100644 src/test-data/pug/test/cases/pipeless-comments.pug create mode 100644 src/test-data/pug/test/cases/pipeless-filters.html create mode 100644 src/test-data/pug/test/cases/pipeless-filters.pug create mode 100644 src/test-data/pug/test/cases/pipeless-tag.html create mode 100644 src/test-data/pug/test/cases/pipeless-tag.pug create mode 100644 src/test-data/pug/test/cases/pre.html create mode 100644 src/test-data/pug/test/cases/pre.pug create mode 100644 src/test-data/pug/test/cases/quotes.html create mode 100644 src/test-data/pug/test/cases/quotes.pug create mode 100644 src/test-data/pug/test/cases/regression.1794.html create mode 100644 src/test-data/pug/test/cases/regression.1794.pug create mode 100644 src/test-data/pug/test/cases/regression.784.html create mode 100644 src/test-data/pug/test/cases/regression.784.pug create mode 100644 src/test-data/pug/test/cases/script.whitespace.html create mode 100644 src/test-data/pug/test/cases/script.whitespace.pug create mode 100644 src/test-data/pug/test/cases/scripts.html create mode 100644 src/test-data/pug/test/cases/scripts.non-js.html create mode 100644 src/test-data/pug/test/cases/scripts.non-js.pug create mode 100644 src/test-data/pug/test/cases/scripts.pug create mode 100644 src/test-data/pug/test/cases/self-closing-html.html create mode 100644 src/test-data/pug/test/cases/self-closing-html.pug create mode 100644 src/test-data/pug/test/cases/single-period.html create mode 100644 src/test-data/pug/test/cases/single-period.pug create mode 100644 src/test-data/pug/test/cases/some-included.styl create mode 100644 src/test-data/pug/test/cases/some.md create mode 100644 src/test-data/pug/test/cases/some.styl create mode 100644 src/test-data/pug/test/cases/source.html create mode 100644 src/test-data/pug/test/cases/source.pug create mode 100644 src/test-data/pug/test/cases/styles.html create mode 100644 src/test-data/pug/test/cases/styles.pug create mode 100644 src/test-data/pug/test/cases/tag.interpolation.html create mode 100644 src/test-data/pug/test/cases/tag.interpolation.pug create mode 100644 src/test-data/pug/test/cases/tags.self-closing.html create mode 100644 src/test-data/pug/test/cases/tags.self-closing.pug create mode 100644 src/test-data/pug/test/cases/template.html create mode 100644 src/test-data/pug/test/cases/template.pug create mode 100644 src/test-data/pug/test/cases/text-block.html create mode 100644 src/test-data/pug/test/cases/text-block.pug create mode 100644 src/test-data/pug/test/cases/text.html create mode 100644 src/test-data/pug/test/cases/text.pug create mode 100644 src/test-data/pug/test/cases/utf8bom.html create mode 100644 src/test-data/pug/test/cases/utf8bom.pug create mode 100644 src/test-data/pug/test/cases/vars.html create mode 100644 src/test-data/pug/test/cases/vars.pug create mode 100644 src/test-data/pug/test/cases/while.html create mode 100644 src/test-data/pug/test/cases/while.pug create mode 100644 src/test-data/pug/test/cases/xml.html create mode 100644 src/test-data/pug/test/cases/xml.pug create mode 100644 src/test-data/pug/test/cases/yield-before-conditional-head.html create mode 100644 src/test-data/pug/test/cases/yield-before-conditional-head.pug create mode 100644 src/test-data/pug/test/cases/yield-before-conditional.html create mode 100644 src/test-data/pug/test/cases/yield-before-conditional.pug create mode 100644 src/test-data/pug/test/cases/yield-head.html create mode 100644 src/test-data/pug/test/cases/yield-head.pug create mode 100644 src/test-data/pug/test/cases/yield-title-head.html create mode 100644 src/test-data/pug/test/cases/yield-title-head.pug create mode 100644 src/test-data/pug/test/cases/yield-title.html create mode 100644 src/test-data/pug/test/cases/yield-title.pug create mode 100644 src/test-data/pug/test/cases/yield.html create mode 100644 src/test-data/pug/test/cases/yield.pug create mode 100644 src/test-data/pug/test/dependencies/dependency1.pug create mode 100644 src/test-data/pug/test/dependencies/dependency2.pug create mode 100644 src/test-data/pug/test/dependencies/dependency3.pug create mode 100644 src/test-data/pug/test/dependencies/extends1.pug create mode 100644 src/test-data/pug/test/dependencies/extends2.pug create mode 100644 src/test-data/pug/test/dependencies/include1.pug create mode 100644 src/test-data/pug/test/dependencies/include2.pug create mode 100644 src/test-data/pug/test/duplicate-block/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug/test/duplicate-block/index.pug create mode 100644 src/test-data/pug/test/duplicate-block/index.test.js create mode 100644 src/test-data/pug/test/duplicate-block/layout-with-duplicate-block.pug create mode 100644 src/test-data/pug/test/eachOf/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug/test/eachOf/error/left-side.pug create mode 100644 src/test-data/pug/test/eachOf/error/no-brackets.pug create mode 100644 src/test-data/pug/test/eachOf/error/one-val.pug create mode 100644 src/test-data/pug/test/eachOf/error/right-side.pug create mode 100644 src/test-data/pug/test/eachOf/index.test.js create mode 100644 src/test-data/pug/test/eachOf/passing/brackets.pug create mode 100644 src/test-data/pug/test/eachOf/passing/no-brackets.pug create mode 100644 src/test-data/pug/test/error.reporting.test.js create mode 100644 src/test-data/pug/test/examples.test.js create mode 100644 src/test-data/pug/test/extends-not-top-level/default.pug create mode 100644 src/test-data/pug/test/extends-not-top-level/duplicate.pug create mode 100644 src/test-data/pug/test/extends-not-top-level/index.pug create mode 100644 src/test-data/pug/test/extends-not-top-level/index.test.js create mode 100644 src/test-data/pug/test/fixtures/append-without-block/app-layout.pug create mode 100644 src/test-data/pug/test/fixtures/append-without-block/layout.pug create mode 100644 src/test-data/pug/test/fixtures/append-without-block/page.pug create mode 100644 src/test-data/pug/test/fixtures/append/app-layout.pug create mode 100644 src/test-data/pug/test/fixtures/append/layout.pug create mode 100644 src/test-data/pug/test/fixtures/append/page.html create mode 100644 src/test-data/pug/test/fixtures/append/page.pug create mode 100644 src/test-data/pug/test/fixtures/compile.with.include.locals.error.pug create mode 100644 src/test-data/pug/test/fixtures/compile.with.include.syntax.error.pug create mode 100644 src/test-data/pug/test/fixtures/compile.with.layout.locals.error.pug create mode 100644 src/test-data/pug/test/fixtures/compile.with.layout.syntax.error.pug create mode 100644 src/test-data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug create mode 100644 src/test-data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug create mode 100644 src/test-data/pug/test/fixtures/element-with-multiple-attributes.pug create mode 100644 src/test-data/pug/test/fixtures/include.locals.error.pug create mode 100644 src/test-data/pug/test/fixtures/include.syntax.error.pug create mode 100644 src/test-data/pug/test/fixtures/invalid-block-in-extends.pug create mode 100644 src/test-data/pug/test/fixtures/issue-1593/include-layout.pug create mode 100644 src/test-data/pug/test/fixtures/issue-1593/include.pug create mode 100644 src/test-data/pug/test/fixtures/issue-1593/index.pug create mode 100644 src/test-data/pug/test/fixtures/issue-1593/layout.pug create mode 100644 src/test-data/pug/test/fixtures/layout.locals.error.pug create mode 100644 src/test-data/pug/test/fixtures/layout.pug create mode 100644 src/test-data/pug/test/fixtures/layout.syntax.error.pug create mode 100644 src/test-data/pug/test/fixtures/layout.with.runtime.error.pug create mode 100644 src/test-data/pug/test/fixtures/mixin-include.pug create mode 100644 src/test-data/pug/test/fixtures/mixin.error.pug create mode 100644 src/test-data/pug/test/fixtures/multi-append-prepend-block/redefine.pug create mode 100644 src/test-data/pug/test/fixtures/multi-append-prepend-block/root.pug create mode 100644 src/test-data/pug/test/fixtures/perf.pug create mode 100644 src/test-data/pug/test/fixtures/prepend-without-block/app-layout.pug create mode 100644 src/test-data/pug/test/fixtures/prepend-without-block/layout.pug create mode 100644 src/test-data/pug/test/fixtures/prepend-without-block/page.html create mode 100644 src/test-data/pug/test/fixtures/prepend-without-block/page.pug create mode 100644 src/test-data/pug/test/fixtures/prepend/app-layout.pug create mode 100644 src/test-data/pug/test/fixtures/prepend/layout.pug create mode 100644 src/test-data/pug/test/fixtures/prepend/page.html create mode 100644 src/test-data/pug/test/fixtures/prepend/page.pug create mode 100644 src/test-data/pug/test/fixtures/runtime.error.pug create mode 100644 src/test-data/pug/test/fixtures/runtime.layout.error.pug create mode 100644 src/test-data/pug/test/fixtures/runtime.with.mixin.error.pug create mode 100644 src/test-data/pug/test/fixtures/scripts.pug create mode 100644 src/test-data/pug/test/markdown-it/comment.md create mode 100644 src/test-data/pug/test/markdown-it/index.test.js create mode 100644 src/test-data/pug/test/markdown-it/layout-markdown-include.pug create mode 100644 src/test-data/pug/test/markdown-it/layout-markdown-inline.pug create mode 100644 src/test-data/pug/test/output-es2015/attr.html create mode 100644 src/test-data/pug/test/plugins.test.js create mode 100644 src/test-data/pug/test/pug.test.js create mode 100644 src/test-data/pug/test/regression-2436/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug/test/regression-2436/index.test.js create mode 100644 src/test-data/pug/test/regression-2436/issue1.pug create mode 100644 src/test-data/pug/test/regression-2436/issue2.pug create mode 100644 src/test-data/pug/test/regression-2436/layout.pug create mode 100644 src/test-data/pug/test/regression-2436/other1.pug create mode 100644 src/test-data/pug/test/regression-2436/other2.pug create mode 100644 src/test-data/pug/test/regression-2436/other_layout.pug create mode 100644 src/test-data/pug/test/run-es2015.test.js create mode 100644 src/test-data/pug/test/run-syntax-errors.test.js create mode 100644 src/test-data/pug/test/run-utils.js create mode 100644 src/test-data/pug/test/run.test.js create mode 100644 src/test-data/pug/test/shadowed-block/__snapshots__/index.test.js.snap create mode 100644 src/test-data/pug/test/shadowed-block/base.pug create mode 100644 src/test-data/pug/test/shadowed-block/index.pug create mode 100644 src/test-data/pug/test/shadowed-block/index.test.js create mode 100644 src/test-data/pug/test/shadowed-block/layout.pug create mode 100644 src/test-data/pug/test/temp/input-compileFile.pug create mode 100644 src/test-data/pug/test/temp/input-compileFileClient.pug create mode 100644 src/test-data/pug/test/temp/input-renderFile.pug delete mode 100644 src/tests/inheritance_test.zig create mode 100644 src/tests/parser_test.zig create mode 100644 src/v1/codegen.zig create mode 100644 src/v1/error.zig rename src/{v => v1}/lexer.zig (66%) create mode 100644 src/v1/linker.zig create mode 100644 src/v1/load.zig create mode 100644 src/v1/parser.zig create mode 100644 src/v1/pug.zig create mode 100644 src/v1/runtime.zig create mode 100644 src/v1/strip_comments.zig create mode 100644 src/v1/template.zig create mode 100644 src/v1/walk.zig create mode 100644 src/walk.zig diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 801bfe3..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__acp__Bash", - "mcp__acp__Write", - "mcp__acp__Edit" - ] - } -} diff --git a/.gitignore b/.gitignore index 4d574c5..ae9c3b7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ zig-out/ zig-cache/ .zig-cache/ .pugz-cache/ +.claude node_modules # compiled template file diff --git a/CLAUDE.md b/CLAUDE.md index 4f40325..b5b0a85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Purpose -Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering. +Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It compiles Pug templates directly to HTML (unlike JS pug which compiles to JavaScript functions). It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering. ## Rules - Do not auto commit, user will do it. @@ -16,119 +16,142 @@ Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug - `zig build` - Build the project (output in `zig-out/`) - `zig build test` - Run all tests -- `zig build bench-compiled` - Run compiled templates benchmark (compare with Pug.js) -- `zig build bench-interpreted` - Inpterpret trmplates +- `zig build bench-v1` - Run v1 template benchmark +- `zig build bench-interpreted` - Run interpreted templates benchmark ## Architecture Overview -The template engine supports two rendering modes: +### Compilation Pipeline -### 1. Runtime Rendering (Interpreted) ``` -Source → Lexer → Tokens → Parser → AST → Runtime → HTML +Source → Lexer → Tokens → StripComments → Parser → AST → Linker → Codegen → HTML ``` -### 2. Build-Time Compilation (Compiled) -``` -Source → Lexer → Tokens → Parser → AST → build_templates.zig → generated.zig → Native Zig Code -``` +### Two Rendering Modes -The compiled mode is **~3x faster** than Pug.js. +1. **Static compilation** (`pug.compile`): Outputs HTML directly +2. **Data binding** (`template.renderWithData`): Supports `#{field}` interpolation with Zig structs ### Core Modules -| Module | Purpose | -|--------|---------| -| **src/lexer.zig** | Tokenizes Pug source into tokens. Handles indentation tracking, raw text blocks, interpolation. | -| **src/parser.zig** | Converts token stream into AST. Handles nesting via indent/dedent tokens. | -| **src/ast.zig** | AST node definitions (Element, Text, Conditional, Each, Mixin, etc.) | -| **src/runtime.zig** | Evaluates AST with data context, produces final HTML. Handles variable interpolation, conditionals, loops, mixins. | -| **src/build_templates.zig** | Build-time template compiler. Generates optimized Zig code from `.pug` templates. | -| **src/view_engine.zig** | High-level ViewEngine for web servers. Manages views directory, auto-loads mixins. | -| **src/root.zig** | Public library API - exports `ViewEngine`, `renderTemplate()`, `build_templates` and core types. | +| Module | File | Purpose | +|--------|------|---------| +| **Lexer** | `src/lexer.zig` | Tokenizes Pug source into tokens | +| **Parser** | `src/parser.zig` | Builds AST from tokens | +| **Runtime** | `src/runtime.zig` | Shared utilities (HTML escaping, etc.) | +| **Error** | `src/error.zig` | Error formatting with source context | +| **Walk** | `src/walk.zig` | AST traversal with visitor pattern | +| **Strip Comments** | `src/strip_comments.zig` | Token filtering for comments | +| **Load** | `src/load.zig` | File loading for includes/extends | +| **Linker** | `src/linker.zig` | Template inheritance (extends/blocks) | +| **Codegen** | `src/codegen.zig` | AST to HTML generation | +| **Template** | `src/template.zig` | Data binding renderer | +| **Pug** | `src/pug.zig` | Main entry point | +| **ViewEngine** | `src/view_engine.zig` | High-level API for web servers | +| **Root** | `src/root.zig` | Public library API exports | ### Test Files -- **src/tests/general_test.zig** - Comprehensive integration tests for all features +- **src/tests/general_test.zig** - Comprehensive integration tests - **src/tests/doctype_test.zig** - Doctype-specific tests -- **src/tests/inheritance_test.zig** - Template inheritance tests +- **src/tests/check_list_test.zig** - Template output validation tests +- **src/lexer_test.zig** - Lexer unit tests +- **src/parser_test.zig** - Parser unit tests -## Build-Time Template Compilation +## API Usage -For maximum performance, templates can be compiled to native Zig code at build time. - -### Setup in build.zig +### Static Compilation (no data) ```zig const std = @import("std"); +const pug = @import("pugz").pug; -pub fn build(b: *std.Build) void { - const pugz_dep = b.dependency("pugz", .{}); - - // Compile templates at build time - const build_templates = @import("pugz").build_templates; - const compiled_templates = build_templates.compileTemplates(b, .{ - .source_dir = "views", // Directory containing .pug files - }); +pub fn main() !void { + const allocator = std.heap.page_allocator; - const exe = b.addExecutable(.{ - .name = "myapp", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .imports = &.{ - .{ .name = "pugz", .module = pugz_dep.module("pugz") }, - .{ .name = "tpls", .module = compiled_templates }, - }, - }), - }); + var result = try pug.compile(allocator, "p Hello World", .{}); + defer result.deinit(allocator); + + std.debug.print("{s}\n", .{result.html}); //

Hello World

} ``` -### Usage in Code +### Dynamic Rendering with Data ```zig -const tpls = @import("tpls"); +const std = @import("std"); +const pugz = @import("pugz"); -pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 { - // Zero-cost template rendering - just native Zig code - return try tpls.home(allocator, .{ +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const html = try pugz.renderTemplate(arena.allocator(), + \\h1 #{title} + \\p #{message} + , .{ .title = "Welcome", - .user = .{ .name = "Alice", .email = "alice@example.com" }, - .items = &[_][]const u8{ "One", "Two", "Three" }, + .message = "Hello, World!", + }); + + std.debug.print("{s}\n", .{html}); + // Output:

Welcome

Hello, World!

+} +``` + +### Data Binding Features + +- **Interpolation**: `#{fieldName}` in text content +- **Attribute binding**: `a(href=url)` binds `url` field to href +- **Buffered code**: `p= message` outputs the `message` field +- **Auto-escaping**: HTML is escaped by default (XSS protection) + +```zig +const html = try pugz.renderTemplate(allocator, + \\a(href=url, class=style) #{text} +, .{ + .url = "https://example.com", + .style = "btn", + .text = "Click me!", +}); +// Output: Click me! +``` + +### ViewEngine (for Web Servers) + +```zig +const std = @import("std"); +const pugz = @import("pugz"); + +const engine = pugz.ViewEngine.init(.{ + .views_dir = "src/views", + .extension = ".pug", +}); + +// In request handler +pub fn handleRequest(allocator: std.mem.Allocator) ![]const u8 { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + return try engine.render(arena.allocator(), "pages/home", .{ + .title = "Home", + .user = .{ .name = "Alice" }, }); } ``` -### Generated Code Features +### Compile Options -The compiler generates optimized Zig code with: -- **Static string merging** - Consecutive static content merged into single `appendSlice` calls -- **Zero allocation for static templates** - Returns string literal directly -- **Type-safe data access** - Uses `@field(d, "name")` for compile-time checked field access -- **Automatic type conversion** - `strVal()` helper converts integers to strings -- **Optional handling** - Nullable slices handled with `orelse &.{}` -- **HTML escaping** - Lookup table for fast character escaping - -### Benchmark Results (2000 iterations) - -| Template | Pug.js | Pugz | Speedup | -|----------|--------|------|---------| -| simple-0 | 0.8ms | 0.1ms | **8x** | -| simple-1 | 1.4ms | 0.6ms | **2.3x** | -| simple-2 | 1.8ms | 0.6ms | **3x** | -| if-expression | 0.6ms | 0.2ms | **3x** | -| projects-escaped | 4.4ms | 0.6ms | **7.3x** | -| search-results | 15.2ms | 5.6ms | **2.7x** | -| friends | 153.5ms | 54.0ms | **2.8x** | -| **TOTAL** | **177.6ms** | **61.6ms** | **~3x faster** | - -Run benchmarks: -```bash -# Pugz (Zig) -zig build bench-compiled - -# Pug.js (for comparison) -cd src/benchmarks/pugjs && npm install && npm run bench +```zig +pub const CompileOptions = struct { + filename: ?[]const u8 = null, // For error messages + basedir: ?[]const u8 = null, // For absolute includes + pretty: bool = false, // Pretty print output + strip_unbuffered_comments: bool = true, + strip_buffered_comments: bool = false, + debug: bool = false, + doctype: ?[]const u8 = null, +}; ``` ## Memory Management @@ -144,46 +167,30 @@ const html = try pugz.renderTemplate(arena.allocator(), template, data); This pattern is recommended because template rendering creates many small allocations that are all freed together after the response is sent. -## Key Implementation Details +## Key Implementation Notes -### Lexer State Machine +### Lexer (`lexer.zig`) +- `Lexer.init(allocator, source, options)` - Initialize +- `Lexer.getTokens()` - Returns token slice +- `Lexer.last_error` - Check for errors after failed `getTokens()` -The lexer tracks several states for handling complex syntax: -- `in_raw_block` / `raw_block_indent` / `raw_block_started` - For dot block text (e.g., `script.`) -- `indent_stack` - Stack-based indent/dedent token generation +### Parser (`parser.zig`) +- `Parser.init(allocator, tokens, filename, source)` - Initialize +- `Parser.parse()` - Returns AST root node +- `Parser.err` - Check for errors after failed `parse()` -**Important**: The lexer distinguishes between `#id` (ID selector), `#{expr}` (interpolation), and `#[tag]` (tag interpolation) by looking ahead at the next character. +### Codegen (`codegen.zig`) +- `Compiler.init(allocator, options)` - Initialize +- `Compiler.compile(ast)` - Returns HTML string -### Token Types +### Walk (`walk.zig`) +- Uses O(1) stack operations (append/pop) not O(n) insert/remove +- `getParent(index)` uses reverse indexing (0 = immediate parent) +- `initWithCapacity()` for pre-allocation optimization -Key tokens: `tag`, `class`, `id`, `lparen`, `rparen`, `attr_name`, `attr_value`, `text`, `interp_start`, `interp_end`, `indent`, `dedent`, `dot_block`, `pipe_text`, `literal_html`, `self_close`, `mixin_call`, etc. - -### AST Node Types - -- `element` - HTML elements with tag, classes, id, attributes, children -- `text` - Text with segments (literal, escaped interpolation, unescaped interpolation, tag interpolation) -- `conditional` - if/else if/else/unless branches -- `each` - Iteration with value, optional index, else branch -- `mixin_def` / `mixin_call` - Mixin definitions and invocations -- `block` - Named blocks for template inheritance -- `include` / `extends` - File inclusion and inheritance -- `raw_text` - Literal HTML or text blocks - -### Runtime Value System - -```zig -pub const Value = union(enum) { - null, - bool: bool, - int: i64, - float: f64, - string: []const u8, - array: []const Value, - object: std.StringHashMapUnmanaged(Value), -}; -``` - -The `toValue()` function converts Zig structs to runtime Values automatically. +### Runtime (`runtime.zig`) +- `escapeChar(c)` - Shared HTML escape function +- `appendEscaped(list, allocator, str)` - Append with escaping ## Supported Pug Features @@ -222,12 +229,9 @@ p. Multi-line text block

Literal HTML

// passed through as-is - -// Interpolation-only text works too -h1.header #{title} // renders

Title Value

``` -**Security Note**: By default, `#{}` and `=` escape HTML entities (`<`, `>`, `&`, `"`, `'`) to prevent XSS attacks. Only use `!{}` or `!=` for content you fully trust (e.g., pre-sanitized HTML from your own code). Never use unescaped output for user-provided data. +**Security Note**: By default, `#{}` and `=` escape HTML entities (`<`, `>`, `&`, `"`, `'`) to prevent XSS attacks. Only use `!{}` or `!=` for content you fully trust. ### Tag Interpolation ```pug @@ -240,11 +244,6 @@ p Click #[a(href="/") here] to continue a: img(src="logo.png") // colon for inline nesting ``` -### Explicit Self-Closing -```pug -foo/ // renders as -``` - ### Conditionals ```pug if condition @@ -256,10 +255,6 @@ else unless loggedIn p Please login - -// String comparison in conditions -if status == "active" - p Active ``` ### Iteration @@ -274,16 +269,6 @@ each item in items li= item else li No items - -// Works with objects too (key as index) -each val, key in object - p #{key}: #{val} - -// Nested iteration with field access -each friend in friends - li #{friend.name} - each tag in friend.tags - span= tag ``` ### Case/When @@ -304,29 +289,6 @@ mixin button(text, type="primary") +button("Click me") +button("Submit", "success") - -// With block content -mixin card(title) - .card - h3= title - block - -+card("My Card") - p Card content here - -// Rest arguments -mixin list(id, ...items) - ul(id=id) - each item in items - li= item - -+list("mylist", "a", "b", "c") - -// Attributes pass-through -mixin link(href, text) - a(href=href)&attributes(attributes)= text - -+link("/home", "Home")(class="nav-link" data-id="1") ``` ### Includes & Inheritance @@ -336,13 +298,6 @@ include header.pug extends layout.pug block content h1 Page Title - -// Block modes -block append scripts - script(src="extra.js") - -block prepend styles - link(rel="stylesheet" href="extra.css") ``` ### Comments @@ -351,136 +306,57 @@ block prepend styles //- This is a silent comment (not in output) ``` -## Server Usage +## Benchmark Results (2000 iterations) -### Compiled Templates (Recommended for Production) +| Template | Time | +|----------|------| +| simple-0 | 0.8ms | +| simple-1 | 11.6ms | +| simple-2 | 8.2ms | +| if-expression | 7.4ms | +| projects-escaped | 7.1ms | +| search-results | 13.4ms | +| friends | 22.9ms | +| **TOTAL** | **71.3ms** | -Use build-time compilation for best performance. See "Build-Time Template Compilation" section above. +## Limitations vs JS Pug -### ViewEngine (Runtime Rendering) - -The `ViewEngine` provides runtime template rendering with lazy-loading: - -```zig -const std = @import("std"); -const pugz = @import("pugz"); - -// Initialize once at server startup -var engine = try pugz.ViewEngine.init(allocator, .{ - .views_dir = "src/views", // Root views directory - .mixins_dir = "mixins", // Mixins dir for lazy-loading (optional, default: "mixins") - .extension = ".pug", // File extension (default: .pug) - .pretty = true, // Pretty-print output (default: true) -}); -defer engine.deinit(); - -// In request handler - use arena allocator per request -pub fn handleRequest(engine: *pugz.ViewEngine, allocator: std.mem.Allocator) ![]u8 { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - // Template path is relative to views_dir, extension added automatically - return try engine.render(arena.allocator(), "pages/home", .{ - .title = "Home", - .user = .{ .name = "Alice" }, - }); -} -``` - -### Mixin Resolution (Lazy Loading) - -Mixins are resolved in the following order: -1. **Same template** - Mixins defined in the current template file -2. **Mixins directory** - If not found, searches `views/mixins/*.pug` files (lazy-loaded on first use) - -This lazy-loading approach means: -- Mixins are only parsed when first called -- No upfront loading of all mixin files at server startup -- Templates can override mixins from the mixins directory by defining them locally - -### Directory Structure - -``` -src/views/ -├── mixins/ # Lazy-loaded mixins (searched when mixin not found in template) -│ ├── buttons.pug # mixin btn(text), mixin btn-link(href, text) -│ └── cards.pug # mixin card(title), mixin card-simple(title, body) -├── layouts/ -│ └── base.pug # Base layout with blocks -├── partials/ -│ ├── header.pug -│ └── footer.pug -└── pages/ - ├── home.pug # extends layouts/base - └── about.pug # extends layouts/base -``` - -Templates can use: -- `extends layouts/base` - Paths relative to views_dir -- `include partials/header` - Paths relative to views_dir -- `+btn("Click")` - Mixins from mixins/ dir loaded on-demand - -### Low-Level API - -For inline templates or custom use cases: - -```zig -const std = @import("std"); -const pugz = @import("pugz"); - -pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - return try pugz.renderTemplate(arena.allocator(), - \\html - \\ head - \\ title= title - \\ body - \\ h1 Hello, #{name}! - \\ if showList - \\ ul - \\ each item in items - \\ li= item - , .{ - .title = "My Page", - .name = "World", - .showList = true, - .items = &[_][]const u8{ "One", "Two", "Three" }, - }); -} -``` - -## Testing - -Run tests with `zig build test`. Tests cover: -- Basic element parsing and rendering -- Class and ID shorthand syntax -- Attribute parsing (quoted, unquoted, boolean, object literals) -- Text interpolation (escaped, unescaped, tag interpolation) -- Interpolation-only text (e.g., `h1.class #{var}`) -- Conditionals (if/else if/else/unless) -- Iteration (each with index, else branch, objects, nested loops) -- Case/when statements -- Mixin definitions and calls (with defaults, rest args, block content, attributes) -- Plain text (piped, dot blocks, literal HTML) -- Self-closing tags (void elements, explicit `/`) -- Block expansion with colon -- Comments (rendered and silent) -- String comparison in conditions +1. **No JavaScript expressions**: `- var x = 1` not supported +2. **No nested field access**: `#{user.name}` not supported, only `#{name}` +3. **No filters**: `:markdown`, `:coffee` etc. not implemented +4. **String fields only**: Data binding works best with `[]const u8` fields ## Error Handling -The lexer and parser return errors for invalid syntax: -- `ParserError.UnexpectedToken` -- `ParserError.MissingCondition` -- `ParserError.MissingMixinName` -- `RuntimeError.ParseError` (wrapped for convenience API) +Uses error unions with detailed `PugError` context including line, column, and source snippet: +- `LexerError` - Tokenization errors +- `ParserError` - Syntax errors +- `ViewEngineError` - Template not found, parse errors -## Future Improvements +## File Structure -Potential areas for enhancement: -- Filter support (`:markdown`, `:stylus`, etc.) -- More complete JavaScript expression evaluation -- Source maps for debugging -- Mixin support in compiled templates +``` +src/ +├── root.zig # Public library API +├── view_engine.zig # High-level ViewEngine +├── pug.zig # Main entry point (static compilation) +├── template.zig # Data binding renderer +├── lexer.zig # Tokenizer +├── lexer_test.zig # Lexer tests +├── parser.zig # AST parser +├── parser_test.zig # Parser tests +├── runtime.zig # Shared utilities +├── error.zig # Error formatting +├── walk.zig # AST traversal +├── strip_comments.zig # Comment filtering +├── load.zig # File loading +├── linker.zig # Template inheritance +├── codegen.zig # HTML generation +├── tests/ # Integration tests +│ ├── general_test.zig +│ ├── doctype_test.zig +│ └── check_list_test.zig +└── benchmarks/ # Performance benchmarks + ├── bench_v1.zig + └── bench_interpreted.zig +``` diff --git a/README.md b/README.md index dfa53b8..bcf5eb9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -*Yet not ready to use in production, i tried to get it done using Cluade but its not quite there where i want it* - -*So i will try it by my self keeping PugJS version as a reference* +*! I am using ClaudeCode to build it* +*! Its Yet not ready for production use* # Pugz diff --git a/build.zig b/build.zig index 84a67e0..9d80855 100644 --- a/build.zig +++ b/build.zig @@ -1,8 +1,5 @@ const std = @import("std"); -// Re-export build_templates for use by dependent packages -pub const build_templates = @import("src/build_templates.zig"); - pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); @@ -46,19 +43,6 @@ pub fn build(b: *std.Build) void { }); const run_doctype_tests = b.addRunArtifact(doctype_tests); - // Integration tests - inheritance tests - const inheritance_tests = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path("src/tests/inheritance_test.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "pugz", .module = mod }, - }, - }), - }); - const run_inheritance_tests = b.addRunArtifact(inheritance_tests); - // Integration tests - check_list tests (pug files vs expected html output) const check_list_tests = b.addTest(.{ .root_module = b.createModule(.{ @@ -72,14 +56,11 @@ pub fn build(b: *std.Build) void { }); const run_check_list_tests = b.addRunArtifact(check_list_tests); - // A top level step for running all tests. dependOn can be called multiple - // times and since the two run steps do not depend on one another, this will - // make the two of them run in parallel. + // A top level step for running all tests. const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_mod_tests.step); test_step.dependOn(&run_general_tests.step); test_step.dependOn(&run_doctype_tests.step); - test_step.dependOn(&run_inheritance_tests.step); test_step.dependOn(&run_check_list_tests.step); // Individual test steps @@ -89,82 +70,28 @@ pub fn build(b: *std.Build) void { const test_doctype_step = b.step("test-doctype", "Run doctype tests"); test_doctype_step.dependOn(&run_doctype_tests.step); - const test_inheritance_step = b.step("test-inheritance", "Run inheritance tests"); - test_inheritance_step.dependOn(&run_inheritance_tests.step); - const test_unit_step = b.step("test-unit", "Run unit tests (lexer, parser, etc.)"); test_unit_step.dependOn(&run_mod_tests.step); const test_check_list_step = b.step("test-check-list", "Run check_list template tests"); test_check_list_step.dependOn(&run_check_list_tests.step); - // ───────────────────────────────────────────────────────────────────────── - // Compiled Templates Benchmark (compare with Pug.js bench.js) - // Uses auto-generated templates from src/benchmarks/templates/ - // ───────────────────────────────────────────────────────────────────────── - const mod_fast = b.addModule("pugz-fast", .{ - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = .ReleaseFast, - }); - - const bench_templates = build_templates.compileTemplates(b, .{ - .source_dir = "src/benchmarks/templates", - }); - - const bench_compiled = b.addExecutable(.{ - .name = "bench-compiled", + // Benchmark executable + const bench_exe = b.addExecutable(.{ + .name = "bench", .root_module = b.createModule(.{ .root_source_file = b.path("src/benchmarks/bench.zig"), .target = target, .optimize = .ReleaseFast, .imports = &.{ - .{ .name = "pugz", .module = mod_fast }, - .{ .name = "tpls", .module = bench_templates }, + .{ .name = "pugz", .module = mod }, }, }), }); + b.installArtifact(bench_exe); - b.installArtifact(bench_compiled); - - const run_bench_compiled = b.addRunArtifact(bench_compiled); - run_bench_compiled.step.dependOn(b.getInstallStep()); - - const bench_compiled_step = b.step("bench-compiled", "Benchmark compiled templates (compare with Pug.js)"); - bench_compiled_step.dependOn(&run_bench_compiled.step); - - // ───────────────────────────────────────────────────────────────────────── - // Interpreted (Runtime) Benchmark - // ───────────────────────────────────────────────────────────────────────── - const bench_interpreted = b.addExecutable(.{ - .name = "bench-interpreted", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/benchmarks/bench_interpreted.zig"), - .target = target, - .optimize = .ReleaseFast, - .imports = &.{ - .{ .name = "pugz", .module = mod_fast }, - }, - }), - }); - - b.installArtifact(bench_interpreted); - - const run_bench_interpreted = b.addRunArtifact(bench_interpreted); - run_bench_interpreted.step.dependOn(b.getInstallStep()); - - const bench_interpreted_step = b.step("bench-interpreted", "Benchmark interpreted (runtime) templates"); - bench_interpreted_step.dependOn(&run_bench_interpreted.step); - - // Just like flags, top level steps are also listed in the `--help` menu. - // - // The Zig build system is entirely implemented in userland, which means - // that it cannot hook into private compiler APIs. All compilation work - // orchestrated by the build system will result in other Zig compiler - // subcommands being invoked with the right flags defined. You can observe - // these invocations when one fails (or you pass a flag to increase - // verbosity) to validate assumptions and diagnose problems. - // - // Lastly, the Zig build system is relatively simple and self-contained, - // and reading its source code will allow you to master it. + const run_bench = b.addRunArtifact(bench_exe); + run_bench.setCwd(b.path(".")); + const bench_step = b.step("bench", "Run benchmark"); + bench_step.dependOn(&run_bench.step); } diff --git a/examples/demo/build.zig b/examples/demo/build.zig index 104b12c..e8bc6df 100644 --- a/examples/demo/build.zig +++ b/examples/demo/build.zig @@ -14,13 +14,6 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - // Compile templates at build time using pugz's build_templates - // Generates views/generated.zig with all templates - const build_templates = @import("pugz").build_templates; - const compiled_templates = build_templates.compileTemplates(b, .{ - .source_dir = "views", - }); - // Main executable const exe = b.addExecutable(.{ .name = "demo", @@ -31,7 +24,6 @@ pub fn build(b: *std.Build) void { .imports = &.{ .{ .name = "pugz", .module = pugz_dep.module("pugz") }, .{ .name = "httpz", .module = httpz_dep.module("httpz") }, - .{ .name = "tpls", .module = compiled_templates }, }, }), }); diff --git a/examples/demo/src/main.zig b/examples/demo/src/main.zig index 253a176..d8d2ab8 100644 --- a/examples/demo/src/main.zig +++ b/examples/demo/src/main.zig @@ -1,22 +1,16 @@ -//! Pugz Demo - Interpreted vs Compiled Templates +//! Pugz Demo - ViewEngine Template Rendering //! -//! This demo shows two approaches: -//! 1. **Interpreted** (ViewEngine) - supports extends/blocks, parsed at runtime -//! 2. **Compiled** (build-time) - 3x faster, templates compiled to Zig code +//! This demo shows how to use ViewEngine for server-side rendering. //! //! Routes: -//! GET / - Compiled home page (fast) -//! GET /users - Compiled users list (fast) -//! GET /interpreted - Interpreted with inheritance (flexible) -//! GET /page-a - Interpreted page A +//! GET / - Home page +//! GET /users - Users list +//! GET /page-a - Page with data const std = @import("std"); const httpz = @import("httpz"); const pugz = @import("pugz"); -// Compiled templates - generated at build time from views/compiled/*.pug -const tpls = @import("tpls"); - const Allocator = std.mem.Allocator; /// Application state shared across all requests @@ -42,33 +36,28 @@ pub fn main() !void { var app = App.init(allocator); - const port = 8080; + const port = 8081; var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app); defer server.deinit(); var router = try server.router(.{}); - // Compiled template routes (fast - 3x faster than Pug.js) - router.get("/", indexCompiled, .{}); - router.get("/users", usersCompiled, .{}); - - // Interpreted template routes (flexible - supports extends/blocks) - router.get("/interpreted", indexInterpreted, .{}); + router.get("/", index, .{}); + router.get("/users", users, .{}); router.get("/page-a", pageA, .{}); + router.get("/mixin-test", mixinTest, .{}); std.debug.print( \\ - \\Pugz Demo - Interpreted vs Compiled Templates - \\============================================= + \\Pugz Demo - ViewEngine Template Rendering + \\========================================== \\Server running at http://localhost:{d} \\ - \\Compiled routes (3x faster than Pug.js): - \\ GET / - Home page (compiled) - \\ GET /users - Users list (compiled) - \\ - \\Interpreted routes (supports extends/blocks): - \\ GET /interpreted - Home with ViewEngine - \\ GET /page-a - Page with inheritance + \\Routes: + \\ GET / - Home page + \\ GET /users - Users list + \\ GET /page-a - Page with data + \\ GET /mixin-test - Mixin test page \\ \\Press Ctrl+C to stop. \\ @@ -77,57 +66,10 @@ pub fn main() !void { try server.listen(); } -// ───────────────────────────────────────────────────────────────────────────── -// Compiled template handlers (fast - no parsing at runtime) -// ───────────────────────────────────────────────────────────────────────────── - -/// GET / - Compiled home page -fn indexCompiled(_: *App, _: *httpz.Request, res: *httpz.Response) !void { - const html = tpls.home(res.arena, .{ - .title = "Welcome - Compiled", - .authenticated = true, - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - -/// GET /users - Compiled users list -fn usersCompiled(_: *App, _: *httpz.Request, res: *httpz.Response) !void { - const User = struct { - name: []const u8, - email: []const u8, - }; - - const html = tpls.users(res.arena, .{ - .title = "Users - Compiled", - .users = &[_]User{ - .{ .name = "Alice", .email = "alice@example.com" }, - .{ .name = "Bob", .email = "bob@example.com" }, - .{ .name = "Charlie", .email = "charlie@example.com" }, - }, - }) catch |err| { - res.status = 500; - res.body = @errorName(err); - return; - }; - - res.content_type = .HTML; - res.body = html; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Interpreted template handlers (flexible - supports inheritance) -// ───────────────────────────────────────────────────────────────────────────── - -/// GET /interpreted - Uses ViewEngine (parsed at runtime) -fn indexInterpreted(app: *App, _: *httpz.Request, res: *httpz.Response) !void { +/// GET / - Home page +fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void { const html = app.view.render(res.arena, "index", .{ - .title = "Home - Interpreted", + .title = "Welcome", .authenticated = true, }) catch |err| { res.status = 500; @@ -139,7 +81,21 @@ fn indexInterpreted(app: *App, _: *httpz.Request, res: *httpz.Response) !void { res.body = html; } -/// GET /page-a - Demonstrates extends and block override +/// GET /users - Users list +fn users(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(res.arena, "users", .{ + .title = "Users", + }) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} + +/// GET /page-a - Page with data fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void { const html = app.view.render(res.arena, "page-a", .{ .title = "Page A - Pets", @@ -154,3 +110,15 @@ fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void { res.content_type = .HTML; res.body = html; } + +/// GET /mixin-test - Mixin test page +fn mixinTest(app: *App, _: *httpz.Request, res: *httpz.Response) !void { + const html = app.view.render(res.arena, "mixin-test", .{}) catch |err| { + res.status = 500; + res.body = @errorName(err); + return; + }; + + res.content_type = .HTML; + res.body = html; +} diff --git a/examples/demo/views/generated.zig b/examples/demo/views/generated.zig index 7c38193..d534caa 100644 --- a/examples/demo/views/generated.zig +++ b/examples/demo/views/generated.zig @@ -214,6 +214,50 @@ pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 { return o.items; } +pub fn mixin_test(a: Allocator, d: anytype) Allocator.Error![]u8 { + var o: ArrayList = .empty; + try o.appendSlice(a, "Mixin Test

Mixin Test Page

Testing button mixin:

"); + { + const text = "Click Me"; + const @"type" = "primary"; + try o.appendSlice(a, ""); + try esc(&o, a, strVal(text)); + try o.appendSlice(a, ""); + } + { + const text = "Cancel"; + const @"type" = "btn btn-secondary"; + try o.appendSlice(a, ""); + try esc(&o, a, strVal(text)); + try o.appendSlice(a, ""); + } + try o.appendSlice(a, "

Testing link mixin:

"); + { + const href = "/home"; + const text = "Go Home"; + try o.appendSlice(a, ""); + try esc(&o, a, strVal(text)); + try o.appendSlice(a, ""); + } + try o.appendSlice(a, ""); + _ = d; + return o.items; +} + pub fn page_b(a: Allocator, d: anytype) Allocator.Error![]u8 { var o: ArrayList = .empty; try o.appendSlice(a, "My Site - "); @@ -281,6 +325,7 @@ pub const template_names = [_][]const u8{ "mixins_input_text", "home", "page_a", + "mixin_test", "page_b", "layout_2", "layout", diff --git a/examples/demo/views/mixin-test.pug b/examples/demo/views/mixin-test.pug new file mode 100644 index 0000000..2d0bd9b --- /dev/null +++ b/examples/demo/views/mixin-test.pug @@ -0,0 +1,15 @@ +include mixins/buttons.pug + +doctype html +html + head + title Mixin Test + body + h1 Mixin Test Page + + p Testing button mixin: + +btn("Click Me") + +btn("Cancel", "secondary") + + p Testing link mixin: + +btn-link("/home", "Go Home") diff --git a/src/ast.zig b/src/ast.zig deleted file mode 100644 index fffbd8f..0000000 --- a/src/ast.zig +++ /dev/null @@ -1,257 +0,0 @@ -//! AST (Abstract Syntax Tree) definitions for Pug templates. -//! -//! The AST represents the hierarchical structure of a Pug document. -//! Each node type corresponds to a Pug language construct. - -const std = @import("std"); - -/// An attribute on an element: name, value, and whether it's escaped. -pub const Attribute = struct { - name: []const u8, - value: ?[]const u8, // null for boolean attributes (e.g., `checked`) - escaped: bool, // true for `=`, false for `!=` -}; - -/// A segment of text content, which may be plain text or interpolation. -pub const TextSegment = union(enum) { - /// Plain text content. - literal: []const u8, - /// Escaped interpolation: #{expr} - HTML entities escaped. - interp_escaped: []const u8, - /// Unescaped interpolation: !{expr} - raw HTML output. - interp_unescaped: []const u8, - /// Tag interpolation: #[tag text] - inline HTML element. - interp_tag: InlineTag, -}; - -/// Inline tag from tag interpolation syntax: #[em text] or #[a(href='/') link] -pub const InlineTag = struct { - /// Tag name (e.g., "em", "a", "strong"). - tag: []const u8, - /// CSS classes from `.class` syntax. - classes: []const []const u8, - /// Element ID from `#id` syntax. - id: ?[]const u8, - /// Attributes from `(attr=value)` syntax. - attributes: []Attribute, - /// Text content (may contain nested interpolations). - text_segments: []TextSegment, -}; - -/// All AST node types. -pub const Node = union(enum) { - /// Root document node containing all top-level nodes. - document: Document, - /// Doctype declaration: `doctype html`. - doctype: Doctype, - /// HTML element with optional tag, classes, id, attributes, and children. - element: Element, - /// Text content (may contain interpolations). - text: Text, - /// Buffered code output: `= expr` (escaped) or `!= expr` (unescaped). - code: Code, - /// Comment: `//` (rendered) or `//-` (silent). - comment: Comment, - /// Conditional: if/else if/else/unless chains. - conditional: Conditional, - /// Each loop: `each item in collection` or `each item, index in collection`. - each: Each, - /// While loop: `while condition`. - @"while": While, - /// Case/switch statement. - case: Case, - /// Mixin definition: `mixin name(args)`. - mixin_def: MixinDef, - /// Mixin call: `+name(args)`. - mixin_call: MixinCall, - /// Mixin block placeholder: `block` inside a mixin. - mixin_block: void, - /// Include directive: `include path`. - include: Include, - /// Extends directive: `extends path`. - extends: Extends, - /// Named block: `block name`. - block: Block, - /// Raw text block (after `.` on element). - raw_text: RawText, -}; - -/// Root document containing all top-level nodes. -pub const Document = struct { - nodes: []Node, - /// Optional extends directive (must be first if present). - extends_path: ?[]const u8 = null, -}; - -/// Doctype declaration node. -pub const Doctype = struct { - /// The doctype value (e.g., "html", "xml", "strict", or custom string). - /// Empty string means default to "html". - value: []const u8, -}; - -/// HTML element node. -pub const Element = struct { - /// Tag name (defaults to "div" if only class/id specified). - tag: []const u8, - /// CSS classes from `.class` syntax. - classes: []const []const u8, - /// Element ID from `#id` syntax. - id: ?[]const u8, - /// Attributes from `(attr=value)` syntax. - attributes: []Attribute, - /// Spread attributes from `&attributes({...})` syntax. - spread_attributes: ?[]const u8 = null, - /// Child nodes (nested elements, text, etc.). - children: []Node, - /// Whether this is a self-closing tag. - self_closing: bool, - /// Inline text content (e.g., `p Hello`). - inline_text: ?[]TextSegment, - /// Buffered code content (e.g., `p= expr` or `p!= expr`). - buffered_code: ?Code = null, - /// Whether children should be rendered inline (block expansion with `:`). - is_inline: bool = false, -}; - -/// Text content node. -pub const Text = struct { - /// Segments of text (literals and interpolations). - segments: []TextSegment, -}; - -/// Code output node: `= expr` or `!= expr`. -pub const Code = struct { - /// The expression to evaluate. - expression: []const u8, - /// Whether output is HTML-escaped. - escaped: bool, -}; - -/// Comment node. -pub const Comment = struct { - /// Comment text content. - content: []const u8, - /// Whether comment is rendered in output (`//`) or silent (`//-`). - rendered: bool, - /// Nested content (for block comments). - children: []Node, -}; - -/// Conditional node for if/else if/else/unless chains. -pub const Conditional = struct { - /// The condition branches in order. - branches: []Branch, - - pub const Branch = struct { - /// Condition expression (null for `else`). - condition: ?[]const u8, - /// Whether this is `unless` (negated condition). - is_unless: bool, - /// Child nodes for this branch. - children: []Node, - }; -}; - -/// Each loop node. -pub const Each = struct { - /// Iterator variable name. - value_name: []const u8, - /// Optional index variable name. - index_name: ?[]const u8, - /// Collection expression to iterate. - collection: []const u8, - /// Loop body nodes. - children: []Node, - /// Optional else branch (when collection is empty). - else_children: []Node, -}; - -/// While loop node. -pub const While = struct { - /// Loop condition expression. - condition: []const u8, - /// Loop body nodes. - children: []Node, -}; - -/// Case/switch node. -pub const Case = struct { - /// Expression to match against. - expression: []const u8, - /// When branches (in order, for fall-through support). - whens: []When, - /// Default branch children (if any). - default_children: []Node, - - pub const When = struct { - /// Value to match. - value: []const u8, - /// Child nodes for this case. Empty means fall-through to next case. - children: []Node, - /// Explicit break (- break) means output nothing. - has_break: bool, - }; -}; - -/// Mixin definition node. -pub const MixinDef = struct { - /// Mixin name. - name: []const u8, - /// Parameter names. - params: []const []const u8, - /// Default values for parameters (null if no default). - defaults: []?[]const u8, - /// Whether last param is rest parameter (...args). - has_rest: bool, - /// Mixin body nodes. - children: []Node, -}; - -/// Mixin call node. -pub const MixinCall = struct { - /// Mixin name to call. - name: []const u8, - /// Argument expressions. - args: []const []const u8, - /// Attributes passed to mixin. - attributes: []Attribute, - /// Block content passed to mixin. - block_children: []Node, -}; - -/// Include directive node. -pub const Include = struct { - /// Path to include. - path: []const u8, - /// Optional filter (e.g., `:markdown`). - filter: ?[]const u8, -}; - -/// Extends directive node. -pub const Extends = struct { - /// Path to parent template. - path: []const u8, -}; - -/// Named block node for template inheritance. -pub const Block = struct { - /// Block name. - name: []const u8, - /// Block mode: replace, append, or prepend. - mode: Mode, - /// Block content nodes. - children: []Node, - - pub const Mode = enum { - replace, - append, - prepend, - }; -}; - -/// Raw text block (from `.` syntax). -pub const RawText = struct { - /// Raw text content lines. - content: []const u8, -}; diff --git a/src/benchmarks/bench.zig b/src/benchmarks/bench.zig index 930004e..b6fd688 100644 --- a/src/benchmarks/bench.zig +++ b/src/benchmarks/bench.zig @@ -1,22 +1,16 @@ -//! Pugz Benchmark - Compiled Templates vs Pug.js +//! Pugz Benchmark - Template Rendering //! -//! Both Pugz and Pug.js benchmarks read from the same files: -//! src/benchmarks/templates/*.pug (templates) -//! src/benchmarks/templates/*.json (data) +//! This benchmark uses template.zig renderWithData function. //! -//! Run Pugz: zig build bench-all-compiled -//! Run Pug.js: cd src/benchmarks/pugjs && npm install && npm run bench +//! Run: zig build bench-v1 const std = @import("std"); -const tpls = @import("tpls"); +const pugz = @import("pugz"); const iterations: usize = 2000; const templates_dir = "src/benchmarks/templates"; -// ═══════════════════════════════════════════════════════════════════════════ // Data structures matching JSON files -// ═══════════════════════════════════════════════════════════════════════════ - const SubFriend = struct { id: i64, name: []const u8, @@ -58,10 +52,6 @@ const SearchRecord = struct { sizes: ?[]const []const u8, }; -// ═══════════════════════════════════════════════════════════════════════════ -// Main -// ═══════════════════════════════════════════════════════════════════════════ - pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -69,20 +59,17 @@ pub fn main() !void { std.debug.print("\n", .{}); std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{}); - std.debug.print("║ Compiled Zig Templates Benchmark ({d} iterations) ║\n", .{iterations}); + std.debug.print("║ V1 Template Benchmark ({d} iterations) ║\n", .{iterations}); std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir}); std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{}); - // ───────────────────────────────────────────────────────────────────────── // Load JSON data - // ───────────────────────────────────────────────────────────────────────── std.debug.print("\nLoading JSON data...\n", .{}); var data_arena = std.heap.ArenaAllocator.init(allocator); defer data_arena.deinit(); const data_alloc = data_arena.allocator(); - // Load all JSON files const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json"); const simple1 = try loadJson(struct { name: []const u8, @@ -108,38 +95,27 @@ pub fn main() !void { const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json"); const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json"); + // Load template sources + const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug"); + const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug"); + const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug"); + const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug"); + const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug"); + const search_tpl = try loadTemplate(data_alloc, "search-results.pug"); + const friends_tpl = try loadTemplate(data_alloc, "friends.pug"); + std.debug.print("Loaded. Starting benchmark...\n\n", .{}); var total: f64 = 0; - // ───────────────────────────────────────────────────────────────────────── - // Benchmark each template - // ───────────────────────────────────────────────────────────────────────── + total += try bench("simple-0", allocator, simple0_tpl, simple0); + total += try bench("simple-1", allocator, simple1_tpl, simple1); + total += try bench("simple-2", allocator, simple2_tpl, simple2); + total += try bench("if-expression", allocator, if_expr_tpl, if_expr); + total += try bench("projects-escaped", allocator, projects_tpl, projects); + total += try bench("search-results", allocator, search_tpl, search); + total += try bench("friends", allocator, friends_tpl, friends_data); - // simple-0 - total += try bench("simple-0", allocator, tpls.simple_0, simple0); - - // simple-1 - total += try bench("simple-1", allocator, tpls.simple_1, simple1); - - // simple-2 - total += try bench("simple-2", allocator, tpls.simple_2, simple2); - - // if-expression - total += try bench("if-expression", allocator, tpls.if_expression, if_expr); - - // projects-escaped - total += try bench("projects-escaped", allocator, tpls.projects_escaped, projects); - - // search-results - total += try bench("search-results", allocator, tpls.search_results, search); - - // friends - total += try bench("friends", allocator, tpls.friends, friends_data); - - // ───────────────────────────────────────────────────────────────────────── - // Summary - // ───────────────────────────────────────────────────────────────────────── std.debug.print("\n", .{}); std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total }); std.debug.print("\n", .{}); @@ -152,10 +128,15 @@ fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []con return parsed.value; } +fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 { + const path = templates_dir ++ "/" ++ filename; + return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024); +} + fn bench( name: []const u8, allocator: std.mem.Allocator, - comptime render_fn: anytype, + template: []const u8, data: anytype, ) !f64 { var arena = std.heap.ArenaAllocator.init(allocator); @@ -164,7 +145,10 @@ fn bench( var timer = try std.time.Timer.start(); for (0..iterations) |_| { _ = arena.reset(.retain_capacity); - _ = try render_fn(arena.allocator(), data); + _ = pugz.template.renderWithData(arena.allocator(), template, data) catch |err| { + std.debug.print(" {s:<20} => ERROR: {}\n", .{ name, err }); + return 0; + }; } const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms }); diff --git a/src/benchmarks/bench_interpreted.zig b/src/benchmarks/bench_interpreted.zig deleted file mode 100644 index e241e2d..0000000 --- a/src/benchmarks/bench_interpreted.zig +++ /dev/null @@ -1,154 +0,0 @@ -//! Pugz Benchmark - Interpreted (Runtime) Mode -//! -//! This benchmark uses the ViewEngine to render templates at runtime, -//! reading from the same template/data files as the compiled benchmark. -//! -//! Run: zig build bench-interpreted - -const std = @import("std"); -const pugz = @import("pugz"); - -const iterations: usize = 2000; -const templates_dir = "src/benchmarks/templates"; - -// Data structures matching JSON files -const SubFriend = struct { - id: i64, - name: []const u8, -}; - -const Friend = struct { - name: []const u8, - balance: []const u8, - age: i64, - address: []const u8, - picture: []const u8, - company: []const u8, - email: []const u8, - emailHref: []const u8, - about: []const u8, - tags: []const []const u8, - friends: []const SubFriend, -}; - -const Account = struct { - balance: i64, - balanceFormatted: []const u8, - status: []const u8, - negative: bool, -}; - -const Project = struct { - name: []const u8, - url: []const u8, - description: []const u8, -}; - -const SearchRecord = struct { - imgUrl: []const u8, - viewItemUrl: []const u8, - title: []const u8, - description: []const u8, - featured: bool, - sizes: ?[]const []const u8, -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - std.debug.print("\n", .{}); - std.debug.print("╔═══════════════════════════════════════════════════════════════╗\n", .{}); - std.debug.print("║ Interpreted (Runtime) Benchmark ({d} iterations) ║\n", .{iterations}); - std.debug.print("║ Templates: {s}/*.pug ║\n", .{templates_dir}); - std.debug.print("╚═══════════════════════════════════════════════════════════════╝\n", .{}); - - // Load JSON data - std.debug.print("\nLoading JSON data...\n", .{}); - - var data_arena = std.heap.ArenaAllocator.init(allocator); - defer data_arena.deinit(); - const data_alloc = data_arena.allocator(); - - const simple0 = try loadJson(struct { name: []const u8 }, data_alloc, "simple-0.json"); - const simple1 = try loadJson(struct { - name: []const u8, - messageCount: i64, - colors: []const []const u8, - primary: bool, - }, data_alloc, "simple-1.json"); - const simple2 = try loadJson(struct { - header: []const u8, - header2: []const u8, - header3: []const u8, - header4: []const u8, - header5: []const u8, - header6: []const u8, - list: []const []const u8, - }, data_alloc, "simple-2.json"); - const if_expr = try loadJson(struct { accounts: []const Account }, data_alloc, "if-expression.json"); - const projects = try loadJson(struct { - title: []const u8, - text: []const u8, - projects: []const Project, - }, data_alloc, "projects-escaped.json"); - const search = try loadJson(struct { searchRecords: []const SearchRecord }, data_alloc, "search-results.json"); - const friends_data = try loadJson(struct { friends: []const Friend }, data_alloc, "friends.json"); - - // Load template sources - const simple0_tpl = try loadTemplate(data_alloc, "simple-0.pug"); - const simple1_tpl = try loadTemplate(data_alloc, "simple-1.pug"); - const simple2_tpl = try loadTemplate(data_alloc, "simple-2.pug"); - const if_expr_tpl = try loadTemplate(data_alloc, "if-expression.pug"); - const projects_tpl = try loadTemplate(data_alloc, "projects-escaped.pug"); - const search_tpl = try loadTemplate(data_alloc, "search-results.pug"); - const friends_tpl = try loadTemplate(data_alloc, "friends.pug"); - - std.debug.print("Loaded. Starting benchmark...\n\n", .{}); - - var total: f64 = 0; - - total += try bench("simple-0", allocator, simple0_tpl, simple0); - total += try bench("simple-1", allocator, simple1_tpl, simple1); - total += try bench("simple-2", allocator, simple2_tpl, simple2); - total += try bench("if-expression", allocator, if_expr_tpl, if_expr); - total += try bench("projects-escaped", allocator, projects_tpl, projects); - total += try bench("search-results", allocator, search_tpl, search); - total += try bench("friends", allocator, friends_tpl, friends_data); - - std.debug.print("\n", .{}); - std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ "TOTAL", total }); - std.debug.print("\n", .{}); -} - -fn loadJson(comptime T: type, alloc: std.mem.Allocator, comptime filename: []const u8) !T { - const path = templates_dir ++ "/" ++ filename; - const content = try std.fs.cwd().readFileAlloc(alloc, path, 10 * 1024 * 1024); - const parsed = try std.json.parseFromSlice(T, alloc, content, .{}); - return parsed.value; -} - -fn loadTemplate(alloc: std.mem.Allocator, comptime filename: []const u8) ![]const u8 { - const path = templates_dir ++ "/" ++ filename; - return try std.fs.cwd().readFileAlloc(alloc, path, 1 * 1024 * 1024); -} - -fn bench( - name: []const u8, - allocator: std.mem.Allocator, - template: []const u8, - data: anytype, -) !f64 { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var timer = try std.time.Timer.start(); - for (0..iterations) |_| { - _ = arena.reset(.retain_capacity); - _ = try pugz.renderTemplate(arena.allocator(), template, data); - } - const ms = @as(f64, @floatFromInt(timer.read())) / 1_000_000.0; - std.debug.print(" {s:<20} => {d:>7.1}ms\n", .{ name, ms }); - return ms; -} diff --git a/src/build_templates.zig b/src/build_templates.zig deleted file mode 100644 index 76f65e3..0000000 --- a/src/build_templates.zig +++ /dev/null @@ -1,2067 +0,0 @@ -//! Pugz Build Step - Compile .pug templates to Zig code at build time. -//! -//! This module transforms .pug template files into native Zig functions during the build process. -//! The generated code runs ~3x faster than interpreted templates by eliminating runtime parsing. -//! -//! ## Architecture -//! -//! The compilation pipeline: -//! 1. `compileTemplates()` - Entry point, creates a build step that produces a Zig module -//! 2. `CompileTemplatesStep` - Build step that orchestrates template discovery and compilation -//! 3. `findTemplates()` - Recursively walks source_dir to find all .pug files -//! 4. `generateSingleFile()` - Creates generated.zig with helper functions and all templates -//! 5. `Compiler` - Core compiler that transforms AST nodes into Zig code -//! -//! ## Generated Output -//! -//! The generated.zig file contains: -//! - Shared helpers: `esc()` (HTML escaping), `truthy()` (boolean coercion), `strVal()` (type conversion) -//! - One public function per template, named after the file path (e.g., pages/home.pug -> pages_home()) -//! - Static string merging for consecutive literals (reduces allocations) -//! - Zero-allocation rendering for fully static templates -//! -//! ## Usage in build.zig: -//! ```zig -//! const build_templates = @import("pugz").build_templates; -//! const templates = build_templates.compileTemplates(b, .{ -//! .source_dir = "views", -//! }); -//! exe.root_module.addImport("templates", templates); -//! ``` -//! -//! ## Usage in code: -//! ```zig -//! const tpls = @import("templates"); -//! const html = try tpls.home(allocator, .{ .title = "Welcome" }); -//! ``` - -const std = @import("std"); -const lexer_mod = @import("lexer.zig"); -const Lexer = lexer_mod.Lexer; -const Diagnostic = lexer_mod.Diagnostic; -const Parser = @import("parser.zig").Parser; -const ast = @import("ast.zig"); - -pub const Options = struct { - /// Root directory containing .pug template files (searched recursively) - source_dir: []const u8 = "views", - /// File extension for template files - extension: []const u8 = ".pug", -}; - -/// Creates a build module containing compiled templates. -/// Call this from build.zig to integrate template compilation into your build. -/// Returns a module that can be imported as "templates" (or any name you choose). -pub fn compileTemplates(b: *std.Build, options: Options) *std.Build.Module { - const step = CompileTemplatesStep.create(b, options); - return b.createModule(.{ - .root_source_file = step.getOutput(), - }); -} - -/// Build step that discovers and compiles all .pug templates in source_dir. -/// Outputs a single generated.zig file containing all template functions. -const CompileTemplatesStep = struct { - step: std.Build.Step, - options: Options, - generated_file: std.Build.GeneratedFile, - - fn create(b: *std.Build, options: Options) *CompileTemplatesStep { - const self = b.allocator.create(CompileTemplatesStep) catch @panic("pugz failed on CompileTemplatesStep"); - self.* = .{ - .step = std.Build.Step.init(.{ - .id = .custom, - .name = "pugz-compile-templates", - .owner = b, - .makeFn = make, - }), - .options = options, - .generated_file = .{ .step = &self.step }, - }; - return self; - } - - fn getOutput(self: *CompileTemplatesStep) std.Build.LazyPath { - return .{ .generated = .{ .file = &self.generated_file } }; - } - - fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - const self: *CompileTemplatesStep = @fieldParentPtr("step", step); - const b = step.owner; - const allocator = b.allocator; - - var templates = std.ArrayList(TemplateInfo){}; - defer templates.deinit(allocator); - - try findTemplates(allocator, self.options.source_dir, "", self.options.extension, &templates); - - const out_path = try std.fs.path.join(allocator, &.{ self.options.source_dir, "generated.zig" }); - try generateSingleFile( - allocator, - self.options.source_dir, - self.options.extension, - out_path, - templates.items, - ); - - self.generated_file.path = out_path; - } -}; - -/// Metadata for a discovered template file -const TemplateInfo = struct { - /// Path relative to source_dir (e.g., "pages/home.pug") - rel_path: []const u8, - /// Valid Zig identifier derived from path (e.g., "pages_home") - zig_name: []const u8, -}; - -/// Recursively walks source_dir to discover all .pug template files. -/// Populates the templates list with path and generated function name for each file. -fn findTemplates( - allocator: std.mem.Allocator, - source_dir: []const u8, - out_path: []const u8, - extension: []const u8, - templates: *std.ArrayList(TemplateInfo), -) !void { - const full_path = if (out_path.len > 0) - try std.fs.path.join(allocator, &.{ source_dir, out_path }) - else - try allocator.dupe(u8, source_dir); - defer allocator.free(full_path); - - var dir = std.fs.cwd().openDir(full_path, .{ .iterate = true }) catch |err| { - std.log.warn("Cannot open directory {s}: {}", .{ full_path, err }); - return; - }; - defer dir.close(); - - var iter = dir.iterate(); - while (try iter.next()) |entry| { - const name = try allocator.dupe(u8, entry.name); - if (entry.kind == .directory) { - const new_sub = if (out_path.len > 0) - try std.fs.path.join(allocator, &.{ out_path, name }) - else - name; - try findTemplates(allocator, source_dir, new_sub, extension, templates); - } else if (entry.kind == .file and std.mem.endsWith(u8, name, extension)) { - const rel_path = if (out_path.len > 0) - try std.fs.path.join(allocator, &.{ out_path, name }) - else - name; - - const without_ext = rel_path[0 .. rel_path.len - extension.len]; - const zig_name = try pathToIdent(allocator, without_ext); - - try templates.append(allocator, .{ - .rel_path = rel_path, - .zig_name = zig_name, - }); - } - } -} - -/// Converts a file path to a valid Zig identifier. -/// Replaces path separators and special chars with underscores. -/// Prefixes with '_' if the path starts with a digit. -fn pathToIdent(allocator: std.mem.Allocator, path: []const u8) ![]u8 { - if (path.len == 0) return try allocator.alloc(u8, 0); - - const first_char = path[0]; - const needs_prefix = !std.ascii.isAlphabetic(first_char) and first_char != '_'; - - const result_len = if (needs_prefix) path.len + 1 else path.len; - var result = try allocator.alloc(u8, result_len); - - const offset: usize = if (needs_prefix) blk: { - result[0] = '_'; - break :blk 1; - } else 0; - - for (path, 0..) |c, i| { - result[i + offset] = switch (c) { - '/', '\\', '-', '.' => '_', - else => c, - }; - } - - return result; -} - -/// Block content from child template, used during inheritance resolution. -/// Stores the mode (replace/append/prepend) and child nodes. -const BlockDef = struct { - mode: ast.Block.Mode, - children: []const ast.Node, -}; - -/// Generates the complete generated.zig file containing all compiled templates. -/// Writes helper functions at the top, followed by each template as a public function. -fn generateSingleFile( - allocator: std.mem.Allocator, - source_dir: []const u8, - extension: []const u8, - out_path: []const u8, - templates: []const TemplateInfo, -) !void { - var out = std.ArrayList(u8){}; - defer out.deinit(allocator); - - const w = out.writer(allocator); - - // Header - try w.writeAll( - \\//! Auto-generated by pugz.compileTemplates() - \\//! Do not edit manually - regenerate by running: zig build - \\ - \\const std = @import("std"); - \\const Allocator = std.mem.Allocator; - \\const ArrayList = std.ArrayList(u8); - \\ - \\// ───────────────────────────────────────────────────────────────────────────── - \\// Helpers - \\// ───────────────────────────────────────────────────────────────────────────── - \\ - \\const esc_lut: [256]?[]const u8 = blk: { - \\ var t: [256]?[]const u8 = .{null} ** 256; - \\ t['&'] = "&"; - \\ t['<'] = "<"; - \\ t['>'] = ">"; - \\ t['"'] = """; - \\ t['\''] = "'"; - \\ break :blk t; - \\}; - \\ - \\fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void { - \\ var i: usize = 0; - \\ for (s, 0..) |c, j| { - \\ if (esc_lut[c]) |e| { - \\ if (j > i) try o.appendSlice(a, s[i..j]); - \\ try o.appendSlice(a, e); - \\ i = j + 1; - \\ } - \\ } - \\ if (i < s.len) try o.appendSlice(a, s[i..]); - \\} - \\ - \\fn truthy(v: anytype) bool { - \\ return switch (@typeInfo(@TypeOf(v))) { - \\ .bool => v, - \\ .optional => v != null, - \\ .pointer => |p| if (p.size == .slice) v.len > 0 else true, - \\ .int, .comptime_int => v != 0, - \\ else => true, - \\ }; - \\} - \\ - \\var int_buf: [32]u8 = undefined; - \\ - \\fn strVal(v: anytype) []const u8 { - \\ const T = @TypeOf(v); - \\ switch (@typeInfo(T)) { - \\ .pointer => |p| switch (p.size) { - \\ .slice => return v, - \\ .one => { - \\ // For pointer-to-array, slice it - \\ const child_info = @typeInfo(p.child); - \\ if (child_info == .array) { - \\ const arr_info = child_info.array; - \\ const ptr: [*]const arr_info.child = @ptrCast(v); - \\ return ptr[0..arr_info.len]; - \\ } - \\ return strVal(v.*); - \\ }, - \\ else => @compileError("unsupported pointer type"), - \\ }, - \\ .array => @compileError("arrays must be passed by pointer"), - \\ .int, .comptime_int => return std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0", - \\ .optional => return if (v) |val| strVal(val) else "", - \\ else => @compileError("strVal: unsupported type " ++ @typeName(T)), - \\ } - \\} - \\ - \\// ───────────────────────────────────────────────────────────────────────────── - \\// Templates - \\// ───────────────────────────────────────────────────────────────────────────── - \\ - \\ - ); - - // Generate each template - for (templates) |tpl| { - const src_path = try std.fs.path.join(allocator, &.{ source_dir, tpl.rel_path }); - defer allocator.free(src_path); - - const source = std.fs.cwd().readFileAlloc(allocator, src_path, 5 * 1024 * 1024) catch |err| { - std.log.err("Failed to read {s}: {}", .{ src_path, err }); - return err; - }; - defer allocator.free(source); - - compileTemplate(allocator, w, source_dir, extension, tpl.zig_name, source) catch |err| { - std.log.err("Failed to compile template {s}: {}", .{ tpl.rel_path, err }); - return err; - }; - } - - // Template names list - try w.writeAll("pub const template_names = [_][]const u8{\n"); - for (templates) |tpl| { - try w.print(" \"{s}\",\n", .{tpl.zig_name}); - } - try w.writeAll("};\n"); - - const file = try std.fs.cwd().createFile(out_path, .{}); - defer file.close(); - try file.writeAll(out.items); -} - -/// Logs a diagnostic error with file location in compiler-style format. -fn logDiagnostic(file_path: []const u8, diag: Diagnostic) void { - std.log.err("{s}:{d}:{d}: {s}", .{ file_path, diag.line, diag.column, diag.message }); - if (diag.source_line) |src_line| { - std.log.err(" | {s}", .{src_line}); - } - if (diag.suggestion) |hint| { - std.log.err(" = hint: {s}", .{hint}); - } -} - -/// Compiles a single .pug template into a Zig function. -/// Handles three cases: -/// - Empty templates: return "" -/// - Static-only templates: return literal string (zero allocation) -/// - Dynamic templates: use ArrayList and return o.items -fn compileTemplate( - allocator: std.mem.Allocator, - w: std.ArrayList(u8).Writer, - source_dir: []const u8, - extension: []const u8, - name: []const u8, - source: []const u8, -) !void { - var lexer = Lexer.init(allocator, source); - defer lexer.deinit(); - const tokens = lexer.tokenize() catch |err| { - if (lexer.getDiagnostic()) |diag| { - logDiagnostic(name, diag); - } else { - std.log.err("Tokenize error in '{s}': {}", .{ name, err }); - } - return err; - }; - - var parser = Parser.initWithSource(allocator, tokens, source); - const doc = parser.parse() catch |err| { - if (parser.getDiagnostic()) |diag| { - logDiagnostic(name, diag); - } else { - std.log.err("Parse error in '{s}': {}", .{ name, err }); - } - return err; - }; - - var compiler = Compiler.init(allocator, w, source_dir, extension); - - // Resolve extends/block inheritance chain before emission - const resolved_nodes = try compiler.resolveInheritance(doc); - - // Determine template characteristics for optimal code generation - var has_content = false; - for (resolved_nodes) |node| { - if (nodeHasOutput(node)) { - has_content = true; - break; - } - } - - var has_dynamic = false; - for (resolved_nodes) |node| { - if (nodeHasDynamic(node)) { - has_dynamic = true; - break; - } - } - - // Generate function signature: pub fn name(a: Allocator, d: anytype) ![]u8 - try w.print("pub fn {s}(a: Allocator, d: anytype) Allocator.Error![]u8 {{\n", .{name}); - - if (!has_content) { - // Empty template (e.g., mixin-only files) - try w.writeAll(" _ = .{ a, d };\n"); - try w.writeAll(" return \"\";\n"); - } else if (!has_dynamic) { - // Static-only: return string literal directly, no heap allocation needed - try w.writeAll(" _ = .{ a, d };\n"); - try w.writeAll(" return "); - for (resolved_nodes) |node| { - try compiler.emitNode(node); - } - try compiler.flushAsReturn(); - } else { - // Dynamic: build output incrementally with ArrayList - try w.writeAll(" var o: ArrayList = .empty;\n"); - - for (resolved_nodes) |node| { - try compiler.emitNode(node); - } - try compiler.flush(); - - // Suppress unused parameter warning if data wasn't accessed - if (!compiler.uses_data) { - try w.writeAll(" _ = d;\n"); - } - - try w.writeAll(" return o.items;\n"); - } - - try w.writeAll("}\n\n"); -} - -/// Checks if a node produces any HTML output (used to detect empty templates) -fn nodeHasOutput(node: ast.Node) bool { - return switch (node) { - .doctype, .element, .text, .raw_text, .comment => true, - .conditional => |c| blk: { - for (c.branches) |br| { - for (br.children) |child| { - if (nodeHasOutput(child)) break :blk true; - } - } - break :blk false; - }, - .each => |e| blk: { - for (e.children) |child| { - if (nodeHasOutput(child)) break :blk true; - } - break :blk false; - }, - .case => |c| blk: { - for (c.whens) |when| { - for (when.children) |child| { - if (nodeHasOutput(child)) break :blk true; - } - } - for (c.default_children) |child| { - if (nodeHasOutput(child)) break :blk true; - } - break :blk false; - }, - .mixin_call => true, // Mixin calls may produce output - .block => |b| blk: { - for (b.children) |child| { - if (nodeHasOutput(child)) break :blk true; - } - break :blk false; - }, - .include => true, // Includes may produce output - .document => |d| blk: { - for (d.nodes) |child| { - if (nodeHasOutput(child)) break :blk true; - } - break :blk false; - }, - else => false, - }; -} - -/// Checks if a node contains dynamic content requiring runtime evaluation -/// (interpolation, conditionals, loops, mixin calls) -fn nodeHasDynamic(node: ast.Node) bool { - return switch (node) { - .element => |e| blk: { - if (e.buffered_code != null) break :blk true; - if (e.inline_text) |segs| { - for (segs) |seg| { - if (seg != .literal) break :blk true; - } - } - for (e.children) |child| { - if (nodeHasDynamic(child)) break :blk true; - } - break :blk false; - }, - .text => |t| blk: { - for (t.segments) |seg| { - if (seg != .literal) break :blk true; - } - break :blk false; - }, - .conditional, .each, .case => true, - .mixin_call => true, // Mixin calls are dynamic - .block => |b| blk: { - for (b.children) |child| { - if (nodeHasDynamic(child)) break :blk true; - } - break :blk false; - }, - .include => true, // Includes may have dynamic content - .document => |d| blk: { - for (d.nodes) |child| { - if (nodeHasDynamic(child)) break :blk true; - } - break :blk false; - }, - else => false, - }; -} - -/// Checks if a mixin body references `attributes` (for &attributes pass-through). -/// Used to avoid emitting unused mixin_attrs struct in generated code. -fn mixinUsesAttributes(nodes: []const ast.Node) bool { - for (nodes) |node| { - switch (node) { - .element => |e| { - // Check spread_attributes field - if (e.spread_attributes != null) return true; - - // Check attribute values for 'attributes' reference - for (e.attributes) |attr| { - if (attr.value) |val| { - if (exprReferencesAttributes(val)) return true; - } - } - - // Check inline text for interpolated attributes reference - if (e.inline_text) |segs| { - if (textSegmentsReferenceAttributes(segs)) return true; - } - - // Check buffered code - if (e.buffered_code) |bc| { - if (exprReferencesAttributes(bc.expression)) return true; - } - - // Recurse into children - if (mixinUsesAttributes(e.children)) return true; - }, - .text => |t| { - if (textSegmentsReferenceAttributes(t.segments)) return true; - }, - .conditional => |c| { - for (c.branches) |br| { - if (mixinUsesAttributes(br.children)) return true; - } - }, - .each => |e| { - if (mixinUsesAttributes(e.children)) return true; - if (mixinUsesAttributes(e.else_children)) return true; - }, - .case => |c| { - for (c.whens) |when| { - if (mixinUsesAttributes(when.children)) return true; - } - if (mixinUsesAttributes(c.default_children)) return true; - }, - .block => |b| { - if (mixinUsesAttributes(b.children)) return true; - }, - else => {}, - } - } - return false; -} - -/// Checks if an expression string references 'attributes' (e.g., "attributes.class"). -fn exprReferencesAttributes(expr: []const u8) bool { - // Check for 'attributes' as standalone or prefix (attributes.class, attributes.id, etc.) - if (std.mem.startsWith(u8, expr, "attributes")) { - // Must be exactly "attributes" or "attributes." followed by more - if (expr.len == 10) return true; // exactly "attributes" - if (expr.len > 10 and expr[10] == '.') return true; // "attributes.something" - } - return false; -} - -/// Checks if text segments contain interpolations referencing 'attributes'. -fn textSegmentsReferenceAttributes(segs: []const ast.TextSegment) bool { - for (segs) |seg| { - switch (seg) { - .interp_escaped, .interp_unescaped => |expr| { - if (exprReferencesAttributes(expr)) return true; - }, - else => {}, - } - } - return false; -} - -/// Zig reserved keywords - field names matching these must be escaped with @"..." -/// when used in generated code (e.g., @"type" instead of type) -const zig_keywords = std.StaticStringMap(void).initComptime(.{ - .{ "addrspace", {} }, - .{ "align", {} }, - .{ "allowzero", {} }, - .{ "and", {} }, - .{ "anyframe", {} }, - .{ "anytype", {} }, - .{ "asm", {} }, - .{ "async", {} }, - .{ "await", {} }, - .{ "break", {} }, - .{ "callconv", {} }, - .{ "catch", {} }, - .{ "comptime", {} }, - .{ "const", {} }, - .{ "continue", {} }, - .{ "defer", {} }, - .{ "else", {} }, - .{ "enum", {} }, - .{ "errdefer", {} }, - .{ "error", {} }, - .{ "export", {} }, - .{ "extern", {} }, - .{ "false", {} }, - .{ "fn", {} }, - .{ "for", {} }, - .{ "if", {} }, - .{ "inline", {} }, - .{ "linksection", {} }, - .{ "noalias", {} }, - .{ "noinline", {} }, - .{ "nosuspend", {} }, - .{ "null", {} }, - .{ "opaque", {} }, - .{ "or", {} }, - .{ "orelse", {} }, - .{ "packed", {} }, - .{ "pub", {} }, - .{ "resume", {} }, - .{ "return", {} }, - .{ "struct", {} }, - .{ "suspend", {} }, - .{ "switch", {} }, - .{ "test", {} }, - .{ "threadlocal", {} }, - .{ "true", {} }, - .{ "try", {} }, - .{ "type", {} }, - .{ "undefined", {} }, - .{ "union", {} }, - .{ "unreachable", {} }, - .{ "usingnamespace", {} }, - .{ "var", {} }, - .{ "volatile", {} }, - .{ "while", {} }, -}); - -/// Escapes identifier if it's a Zig keyword by wrapping in @"..." -fn escapeIdent(ident: []const u8, buf: []u8) []const u8 { - if (zig_keywords.has(ident)) { - return std.fmt.bufPrint(buf, "@\"{s}\"", .{ident}) catch ident; - } - return ident; -} - -/// Core compiler that transforms AST nodes into Zig source code. -/// Maintains state for: -/// - Static string buffering (merges consecutive literals into single appendSlice) -/// - Loop variable tracking (to distinguish loop vars from data fields) -/// - Mixin parameter tracking (for proper scoping) -/// - Template inheritance (blocks from child templates) -/// - Mixin definitions (collected during parsing for later calls) -const Compiler = struct { - allocator: std.mem.Allocator, - writer: std.ArrayList(u8).Writer, - source_dir: []const u8, - extension: []const u8, - buf: std.ArrayList(u8), // Accumulates static strings for batch output - depth: usize, // Current indentation level in generated code - loop_vars: std.ArrayList([]const u8), // Active loop variable names (for each loops) - mixin_params: std.ArrayList([]const u8), // Current mixin's parameter names - mixins: std.StringHashMap(ast.MixinDef), // All discovered mixin definitions - blocks: std.StringHashMap(BlockDef), // Child template block overrides - uses_data: bool, // True if template accesses the data parameter 'd' - mixin_depth: usize, // Nesting level for generating unique mixin variable names - current_attrs_var: ?[]const u8, // Variable name for current mixin's &attributes - - fn init( - allocator: std.mem.Allocator, - writer: std.ArrayList(u8).Writer, - source_dir: []const u8, - extension: []const u8, - ) Compiler { - return .{ - .allocator = allocator, - .writer = writer, - .source_dir = source_dir, - .extension = extension, - .buf = .{}, - .depth = 1, - .loop_vars = .{}, - .mixin_params = .{}, - .mixins = std.StringHashMap(ast.MixinDef).init(allocator), - .blocks = std.StringHashMap(BlockDef).init(allocator), - .uses_data = false, - .mixin_depth = 0, - .current_attrs_var = null, - }; - } - - /// Resolves template inheritance chain (extends keyword). - /// Walks up the inheritance chain collecting blocks, then returns the root template's nodes. - /// Block overrides are stored in self.blocks and applied during emitBlock(). - fn resolveInheritance(self: *Compiler, doc: ast.Document) ![]const ast.Node { - try self.collectMixins(doc.nodes); - - if (doc.extends_path) |extends_path| { - // Child template: collect its block overrides - try self.collectBlocks(doc.nodes); - - // Load parent and recursively resolve (parent may also extend) - const parent_doc = try self.loadTemplate(extends_path); - try self.collectMixins(parent_doc.nodes); - return try self.resolveInheritance(parent_doc); - } - - // Root template: return its nodes (blocks resolved during emission) - return doc.nodes; - } - - /// Recursively collects all mixin definitions from the AST. - /// Mixins can be defined anywhere in a template (top-level or nested). - fn collectMixins(self: *Compiler, nodes: []const ast.Node) !void { - for (nodes) |node| { - switch (node) { - .mixin_def => |def| try self.mixins.put(def.name, def), - .element => |e| try self.collectMixins(e.children), - .conditional => |c| { - for (c.branches) |br| try self.collectMixins(br.children); - }, - .each => |e| { - try self.collectMixins(e.children); - try self.collectMixins(e.else_children); - }, - .block => |b| try self.collectMixins(b.children), - else => {}, - } - } - } - - /// Collects block definitions from a child template for inheritance. - /// These override or extend the parent template's blocks. - fn collectBlocks(self: *Compiler, nodes: []const ast.Node) !void { - for (nodes) |node| { - switch (node) { - .block => |blk| { - try self.blocks.put(blk.name, .{ - .mode = blk.mode, - .children = blk.children, - }); - }, - .element => |e| { - try self.collectBlocks(e.children); - }, - .conditional => |c| { - for (c.branches) |br| { - try self.collectBlocks(br.children); - } - }, - .each => |e| { - try self.collectBlocks(e.children); - }, - else => {}, - } - } - } - - /// Loads and parses a template file by path (for extends/include). - /// Path can be with or without extension. - fn loadTemplate(self: *Compiler, path: []const u8) !ast.Document { - const full_path = blk: { - if (std.mem.endsWith(u8, path, self.extension)) { - break :blk try std.fs.path.join(self.allocator, &.{ self.source_dir, path }); - } else { - const with_ext = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ path, self.extension }); - defer self.allocator.free(with_ext); - break :blk try std.fs.path.join(self.allocator, &.{ self.source_dir, with_ext }); - } - }; - defer self.allocator.free(full_path); - - const source = std.fs.cwd().readFileAlloc(self.allocator, full_path, 5 * 1024 * 1024) catch |err| { - std.log.err("Failed to load template '{s}': {}", .{ full_path, err }); - return err; - }; - - var lexer = Lexer.init(self.allocator, source); - const tokens = lexer.tokenize() catch |err| { - if (lexer.getDiagnostic()) |diag| { - logDiagnostic(path, diag); - } else { - std.log.err("Tokenize error in included template '{s}': {}", .{ path, err }); - } - return err; - }; - - var parser = Parser.initWithSource(self.allocator, tokens, source); - return parser.parse() catch |err| { - if (parser.getDiagnostic()) |diag| { - logDiagnostic(path, diag); - } else { - std.log.err("Parse error in included template '{s}': {}", .{ path, err }); - } - return err; - }; - } - - /// Writes buffered static content as a single appendSlice call and clears the buffer. - fn flush(self: *Compiler) !void { - if (self.buf.items.len > 0) { - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \""); - try self.writer.writeAll(self.buf.items); - try self.writer.writeAll("\");\n"); - self.buf.items.len = 0; - } - } - - /// Writes buffered static content as a return statement (for static-only templates). - fn flushAsReturn(self: *Compiler) !void { - try self.writer.writeAll("\""); - try self.writer.writeAll(self.buf.items); - try self.writer.writeAll("\";\n"); - self.buf.items.len = 0; - } - - /// Appends static string content to the buffer, escaping for Zig string literals. - fn appendStatic(self: *Compiler, s: []const u8) !void { - for (s) |c| { - const escaped: []const u8 = switch (c) { - '\\' => "\\\\", - '"' => "\\\"", - '\n' => "\\n", - '\r' => "\\r", - '\t' => "\\t", - else => &[_]u8{c}, - }; - try self.buf.appendSlice(self.allocator, escaped); - } - } - - /// Appends string with whitespace normalization (for backtick template literals). - /// Collapses newlines/spaces into single spaces, escapes quotes as " for HTML. - fn appendNormalizedWhitespace(self: *Compiler, s: []const u8) !void { - var in_whitespace = true; // Start true to skip leading whitespace - for (s) |c| { - if (c == ' ' or c == '\t' or c == '\n' or c == '\r') { - if (!in_whitespace) { - try self.buf.appendSlice(self.allocator, " "); - in_whitespace = true; - } - } else { - const escaped: []const u8 = switch (c) { - '\\' => "\\\\", - // Escape double quotes as HTML entity for valid attribute values - '"' => """, - else => &[_]u8{c}, - }; - try self.buf.appendSlice(self.allocator, escaped); - in_whitespace = false; - } - } - // Remove trailing space if present - if (self.buf.items.len > 0 and self.buf.items[self.buf.items.len - 1] == ' ') { - self.buf.items.len -= 1; - } - } - - fn writeIndent(self: *Compiler) !void { - for (0..self.depth) |_| try self.writer.writeAll(" "); - } - - /// Main dispatch function - emits Zig code for any AST node type. - fn emitNode(self: *Compiler, node: ast.Node) anyerror!void { - switch (node) { - .doctype => |dt| { - if (std.mem.eql(u8, dt.value, "html")) { - try self.appendStatic("<!DOCTYPE html>"); - } else { - try self.appendStatic("<!DOCTYPE "); - try self.appendStatic(dt.value); - try self.appendStatic(">"); - } - }, - .element => |e| try self.emitElement(e), - .text => |t| try self.emitText(t.segments), - .raw_text => |r| try self.appendStatic(r.content), - .conditional => |c| try self.emitConditional(c), - .each => |e| try self.emitEach(e), - .case => |c| try self.emitCase(c), - .comment => |c| if (c.rendered) { - try self.appendStatic("<!-- "); - try self.appendStatic(c.content); - try self.appendStatic(" -->"); - }, - .block => |b| try self.emitBlock(b), - .include => |inc| try self.emitInclude(inc), - .mixin_call => |call| try self.emitMixinCall(call), - .mixin_def => {}, // Mixin definitions are collected, not emitted directly - .mixin_block => {}, // Handled within mixin call context - .extends => {}, // Handled at document level - .document => |dc| for (dc.nodes) |child| try self.emitNode(child), - else => {}, - } - } - - /// Emits an HTML element: opening tag, attributes, children, closing tag. - /// Handles void elements (self-closing), class merging, and buffered code. - fn emitElement(self: *Compiler, e: ast.Element) anyerror!void { - const is_void = isVoidElement(e.tag) or e.self_closing; - - try self.appendStatic("<"); - try self.appendStatic(e.tag); - - if (e.id) |id| { - try self.appendStatic(" id=\""); - try self.appendStatic(id); - try self.appendStatic("\""); - } - - // Check if there's a class attribute that needs to be merged with shorthand classes - var class_attr_value: ?[]const u8 = null; - var class_attr_escaped: bool = true; - for (e.attributes) |attr| { - if (std.mem.eql(u8, attr.name, "class")) { - class_attr_value = attr.value; - class_attr_escaped = attr.escaped; - break; - } - } - - // Emit merged class attribute (shorthand classes + class attribute value) - if (e.classes.len > 0 or class_attr_value != null) { - try self.emitMergedClassAttribute(e.classes, class_attr_value, class_attr_escaped); - } - - // Emit other attributes (skip class since we handled it above) - for (e.attributes) |attr| { - if (std.mem.eql(u8, attr.name, "class")) continue; // Already handled - if (attr.value) |v| { - try self.emitAttribute(attr.name, v, attr.escaped); - } else { - // Boolean attribute - try self.appendStatic(" "); - try self.appendStatic(attr.name); - try self.appendStatic("=\""); - try self.appendStatic(attr.name); - try self.appendStatic("\""); - } - } - - if (is_void) { - try self.appendStatic(" />"); - return; - } - - try self.appendStatic(">"); - - if (e.inline_text) |segs| { - try self.emitText(segs); - } - - if (e.buffered_code) |bc| { - try self.emitExpr(bc.expression, bc.escaped); - } - - for (e.children) |child| { - try self.emitNode(child); - } - - try self.appendStatic("</"); - try self.appendStatic(e.tag); - try self.appendStatic(">"); - } - - /// Emits a merged class attribute combining shorthand classes (.foo.bar) with - /// dynamic class attribute values. Handles static strings, arrays, and concatenation. - fn emitMergedClassAttribute(self: *Compiler, shorthand_classes: []const []const u8, attr_value: ?[]const u8, escaped: bool) !void { - _ = escaped; - - if (attr_value) |value| { - // Check for string concatenation first: "literal" + variable - if (findConcatOperator(value)) |concat_pos| { - // Has concatenation - need runtime handling - try self.flush(); - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \" class=\\\"\");\n"); - - // Add shorthand classes first - if (shorthand_classes.len > 0) { - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \""); - for (shorthand_classes, 0..) |cls, i| { - if (i > 0) try self.writer.writeAll(" "); - try self.writer.writeAll(cls); - } - try self.writer.writeAll(" \");\n"); // trailing space before concat value - } - - // Emit the concatenation expression - try self.emitConcatExpr(value, concat_pos); - - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n"); - return; - } - - // Check if attribute value is static (string literal) or dynamic - const is_static = value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`'); - const is_array = value.len >= 2 and value[0] == '[' and value[value.len - 1] == ']'; - - if (is_static or is_array) { - // Static value - can merge at compile time - try self.appendStatic(" class=\""); - // First add shorthand classes - for (shorthand_classes, 0..) |cls, i| { - if (i > 0) try self.appendStatic(" "); - try self.appendStatic(cls); - } - // Then add attribute value - if (shorthand_classes.len > 0) try self.appendStatic(" "); - if (is_array) { - try self.appendStatic(parseArrayToSpaceSeparated(value)); - } else if (value[0] == '`') { - try self.appendNormalizedWhitespace(value[1 .. value.len - 1]); - } else { - try self.appendStatic(value[1 .. value.len - 1]); - } - try self.appendStatic("\""); - } else { - // Dynamic value - need runtime concatenation - try self.flush(); - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \" class=\\\"\");\n"); - - // Add shorthand classes first - if (shorthand_classes.len > 0) { - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \""); - for (shorthand_classes, 0..) |cls, i| { - if (i > 0) try self.writer.writeAll(" "); - try self.writer.writeAll(cls); - } - try self.writer.writeAll(" \");\n"); // trailing space before dynamic value - } - - // Add dynamic value - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(value, &accessor_buf); - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor}); - - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n"); - } - } else { - // No attribute value, just shorthand classes - try self.appendStatic(" class=\""); - for (shorthand_classes, 0..) |cls, i| { - if (i > 0) try self.appendStatic(" "); - try self.appendStatic(cls); - } - try self.appendStatic("\""); - } - } - - fn emitText(self: *Compiler, segs: []const ast.TextSegment) anyerror!void { - for (segs) |seg| { - switch (seg) { - .literal => |lit| try self.appendStatic(lit), - .interp_escaped => |expr| try self.emitExpr(expr, true), - .interp_unescaped => |expr| try self.emitExpr(expr, false), - .interp_tag => |t| try self.emitInlineTag(t), - } - } - } - - fn emitInlineTag(self: *Compiler, t: ast.InlineTag) anyerror!void { - try self.appendStatic("<"); - try self.appendStatic(t.tag); - if (t.id) |id| { - try self.appendStatic(" id=\""); - try self.appendStatic(id); - try self.appendStatic("\""); - } - if (t.classes.len > 0) { - try self.appendStatic(" class=\""); - for (t.classes, 0..) |cls, i| { - if (i > 0) try self.appendStatic(" "); - try self.appendStatic(cls); - } - try self.appendStatic("\""); - } - for (t.attributes) |attr| { - if (attr.value) |v| { - if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) { - try self.appendStatic(" "); - try self.appendStatic(attr.name); - try self.appendStatic("=\""); - try self.appendStatic(v[1 .. v.len - 1]); - try self.appendStatic("\""); - } - } - } - try self.appendStatic(">"); - try self.emitText(t.text_segments); - try self.appendStatic("</"); - try self.appendStatic(t.tag); - try self.appendStatic(">"); - } - - /// Emits code for an interpolated expression (#{expr} or !{expr}). - /// Flushes static buffer first since this generates runtime code. - fn emitExpr(self: *Compiler, expr: []const u8, escaped: bool) !void { - try self.flush(); - try self.writeIndent(); - - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(expr, &accessor_buf); - - if (escaped) { - try self.writer.print("try esc(&o, a, strVal({s}));\n", .{accessor}); - } else { - try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor}); - } - } - - /// Emits an HTML attribute. Handles various value types: - /// - String literals (single, double, backtick quoted) - /// - Object literals ({color: 'red'} -> style="color:red;") - /// - Array literals (['a', 'b'] -> class="a b") - /// - String concatenation ("btn-" + type) - /// - Dynamic variable references - fn emitAttribute(self: *Compiler, name: []const u8, value: []const u8, escaped: bool) !void { - _ = escaped; - - // Check for string concatenation: "literal" + variable or variable + "literal" - if (findConcatOperator(value)) |concat_pos| { - // Parse concatenation expression - try self.flush(); - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, \" {s}=\\\"\");\n", .{name}); - - try self.emitConcatExpr(value, concat_pos); - - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n"); - } else if (value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`')) { - // Simple string literal (single, double, or backtick quoted) - try self.appendStatic(" "); - try self.appendStatic(name); - try self.appendStatic("=\""); - // For backtick strings, normalize whitespace (collapse newlines and multiple spaces) - if (value[0] == '`') { - try self.appendNormalizedWhitespace(value[1 .. value.len - 1]); - } else { - try self.appendStatic(value[1 .. value.len - 1]); - } - try self.appendStatic("\""); - } else if (value.len >= 2 and value[0] == '{' and value[value.len - 1] == '}') { - // Object literal - convert to appropriate format - try self.appendStatic(" "); - try self.appendStatic(name); - try self.appendStatic("=\""); - if (std.mem.eql(u8, name, "style")) { - // For style attribute, convert object to CSS: {color: 'red'} -> color:red; - try self.appendStatic(parseObjectToCSS(value)); - } else { - // For other attributes (like class), join values with spaces - try self.appendStatic(parseObjectToSpaceSeparated(value)); - } - try self.appendStatic("\""); - } else if (value.len >= 2 and value[0] == '[' and value[value.len - 1] == ']') { - // Array literal - join with spaces for class attribute, otherwise as-is - try self.appendStatic(" "); - try self.appendStatic(name); - try self.appendStatic("=\""); - try self.appendStatic(parseArrayToSpaceSeparated(value)); - try self.appendStatic("\""); - } else { - // Dynamic value (variable reference) - try self.flush(); - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, \" {s}=\\\"\");\n", .{name}); - - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(value, &accessor_buf); - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor}); - - try self.writeIndent(); - try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n"); - } - } - - /// Finds the + operator for string concatenation, skipping + chars inside quotes. - /// Returns the position of the operator, or null if not found. - fn findConcatOperator(value: []const u8) ?usize { - var in_string = false; - var string_char: u8 = 0; - var i: usize = 0; - - while (i < value.len) : (i += 1) { - const c = value[i]; - - if (in_string) { - if (c == string_char) { - in_string = false; - } - } else { - if (c == '"' or c == '\'' or c == '`') { - in_string = true; - string_char = c; - } else if (c == '+') { - // Check it's surrounded by spaces (typical concat) - if (i > 0 and i + 1 < value.len) { - return i; - } - } - } - } - return null; - } - - /// Emits code for a string concatenation expression (e.g., "btn btn-" + type). - /// Recursively handles chained concatenations. - fn emitConcatExpr(self: *Compiler, value: []const u8, concat_pos: usize) !void { - const left = std.mem.trim(u8, value[0..concat_pos], " "); - const right = std.mem.trim(u8, value[concat_pos + 1 ..], " "); - - // Emit left part - if (left.len >= 2 and (left[0] == '"' or left[0] == '\'' or left[0] == '`')) { - // String literal (single, double, or backtick quoted) - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, {s});\n", .{left}); - } else { - // Variable - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(left, &accessor_buf); - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor}); - } - - // Check if right part also has concatenation - if (findConcatOperator(right)) |next_concat| { - try self.emitConcatExpr(right, next_concat); - } else { - // Emit right part - if (right.len >= 2 and (right[0] == '"' or right[0] == '\'' or right[0] == '`')) { - // String literal (single, double, or backtick quoted) - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, {s});\n", .{right}); - } else { - // Variable - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(right, &accessor_buf); - try self.writeIndent(); - try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor}); - } - } - } - - /// Emits an expression inline (used for dynamic attribute values). - fn emitExprInline(self: *Compiler, expr: []const u8, escaped: bool) !void { - try self.flush(); - try self.writeIndent(); - - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(expr, &accessor_buf); - - if (escaped) { - try self.writer.print("try esc(&o, a, strVal({s}));\n", .{accessor}); - } else { - try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor}); - } - } - - fn isLoopVar(self: *Compiler, name: []const u8) bool { - for (self.loop_vars.items) |v| { - if (std.mem.eql(u8, v, name)) return true; - } - return false; - } - - fn isMixinParam(self: *Compiler, name: []const u8) bool { - for (self.mixin_params.items) |p| { - if (std.mem.eql(u8, p, name)) return true; - } - return false; - } - - /// Builds a Zig accessor expression for a template variable. - /// Handles: loop vars (item), mixin params (text), data fields (@field(d, "name")), - /// nested access (user.name), and mixin attributes (attributes.class). - fn buildAccessor(self: *Compiler, expr: []const u8, buf: []u8) []const u8 { - if (std.mem.indexOfScalar(u8, expr, '.')) |dot| { - const base = expr[0..dot]; - const rest = expr[dot + 1 ..]; - - // Special case: attributes.X should use current mixin's attributes variable - if (std.mem.eql(u8, base, "attributes")) { - if (self.current_attrs_var) |attrs_var| { - return std.fmt.bufPrint(buf, "{s}.{s}", .{ attrs_var, rest }) catch expr; - } - } - - // For loop variables or mixin params like friend.name, access directly - if (self.isLoopVar(base) or self.isMixinParam(base)) { - // Escape base if it's a keyword - use the output buffer - if (zig_keywords.has(base)) { - return std.fmt.bufPrint(buf, "@\"{s}\".{s}", .{ base, rest }) catch expr; - } - return std.fmt.bufPrint(buf, "{s}.{s}", .{ base, rest }) catch expr; - } - // For top-level data field access - mark that we use 'd' - self.uses_data = true; - return std.fmt.bufPrint(buf, "@field(d, \"{s}\").{s}", .{ base, rest }) catch expr; - } else { - // Special case: 'attributes' alone should use current mixin's attributes variable - if (std.mem.eql(u8, expr, "attributes")) { - if (self.current_attrs_var) |attrs_var| { - return attrs_var; - } - } - - // Check if it's a loop variable or mixin param - if (self.isLoopVar(expr) or self.isMixinParam(expr)) { - // Escape if it's a keyword - use the output buffer - if (zig_keywords.has(expr)) { - return std.fmt.bufPrint(buf, "@\"{s}\"", .{expr}) catch expr; - } - return expr; - } - // For top-level like "name", access from d - mark that we use 'd' - self.uses_data = true; - return std.fmt.bufPrint(buf, "@field(d, \"{s}\")", .{expr}) catch expr; - } - } - - fn emitConditional(self: *Compiler, c: ast.Conditional) anyerror!void { - try self.flush(); - for (c.branches, 0..) |br, i| { - try self.writeIndent(); - if (i == 0) { - if (br.is_unless) { - try self.writer.writeAll("if (!"); - } else { - try self.writer.writeAll("if ("); - } - try self.emitCondition(br.condition orelse "true"); - try self.writer.writeAll(") {\n"); - } else if (br.condition) |cond| { - try self.writer.writeAll("} else if ("); - try self.emitCondition(cond); - try self.writer.writeAll(") {\n"); - } else { - try self.writer.writeAll("} else {\n"); - } - self.depth += 1; - for (br.children) |child| try self.emitNode(child); - try self.flush(); - self.depth -= 1; - } - try self.writeIndent(); - try self.writer.writeAll("}\n"); - } - - /// Emits a condition expression for if/else if. - /// Handles string comparisons (== "value") and optional field access (@hasField). - fn emitCondition(self: *Compiler, cond: []const u8) !void { - // String equality: status == "closed" -> std.mem.eql(u8, strVal(status), "closed") - if (std.mem.indexOf(u8, cond, " == \"")) |eq_pos| { - const lhs = std.mem.trim(u8, cond[0..eq_pos], " "); - const rhs_start = eq_pos + 5; // skip ' == "' - if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| { - const rhs = cond[rhs_start .. rhs_start + rhs_end]; - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(lhs, &accessor_buf); - try self.writer.print("std.mem.eql(u8, strVal({s}), \"{s}\")", .{ accessor, rhs }); - return; - } - } - // Handle string inequality: status != "closed" - if (std.mem.indexOf(u8, cond, " != \"")) |eq_pos| { - const lhs = std.mem.trim(u8, cond[0..eq_pos], " "); - const rhs_start = eq_pos + 5; - if (std.mem.indexOfScalar(u8, cond[rhs_start..], '"')) |rhs_end| { - const rhs = cond[rhs_start .. rhs_start + rhs_end]; - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(lhs, &accessor_buf); - try self.writer.print("!std.mem.eql(u8, strVal({s}), \"{s}\")", .{ accessor, rhs }); - return; - } - } - // Regular field access - use buildAccessor for consistency - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(cond, &accessor_buf); - - // Check if this is a simple top-level field access (no dots, not a loop var or mixin param) - const is_simple_field = std.mem.indexOfScalar(u8, cond, '.') == null and - !self.isLoopVar(cond) and !self.isMixinParam(cond); - - if (is_simple_field) { - // Use @hasField to make the field optional at compile time - self.uses_data = true; - try self.writer.print("@hasField(@TypeOf(d), \"{s}\") and truthy({s})", .{ cond, accessor }); - } else { - try self.writer.print("truthy({s})", .{accessor}); - } - } - - /// Emits code for an each loop (iteration over arrays/slices). - /// Handles optional index variable and else branch for empty collections. - fn emitEach(self: *Compiler, e: ast.Each) anyerror!void { - try self.flush(); - try self.writeIndent(); - - try self.loop_vars.append(self.allocator, e.value_name); - - var accessor_buf: [512]u8 = undefined; - const collection_accessor = self.buildAccessor(e.collection, &accessor_buf); - - // Wrap in length check if there's an else branch - if (e.else_children.len > 0) { - try self.writer.print("if ({s}.len > 0) {{\n", .{collection_accessor}); - self.depth += 1; - try self.writeIndent(); - } - - // Handle optional collections (nested fields may be nullable) - if (std.mem.indexOfScalar(u8, e.collection, '.')) |_| { - try self.writer.print("for (if (@typeInfo(@TypeOf({s})) == .optional) ({s} orelse &.{{}}) else {s}) |{s}", .{ collection_accessor, collection_accessor, collection_accessor, e.value_name }); - } else { - try self.writer.print("for ({s}) |{s}", .{ collection_accessor, e.value_name }); - } - if (e.index_name) |idx| { - try self.writer.print(", {s}", .{idx}); - } - try self.writer.writeAll("| {\n"); - - self.depth += 1; - for (e.children) |child| { - try self.emitNode(child); - } - try self.flush(); - self.depth -= 1; - - try self.writeIndent(); - try self.writer.writeAll("}\n"); - - // Handle else branch - if (e.else_children.len > 0) { - self.depth -= 1; - try self.writeIndent(); - try self.writer.writeAll("} else {\n"); - self.depth += 1; - for (e.else_children) |child| { - try self.emitNode(child); - } - try self.flush(); - self.depth -= 1; - try self.writeIndent(); - try self.writer.writeAll("}\n"); - } - - // Pop loop variable - _ = self.loop_vars.pop(); - } - - /// Emits code for a case/when statement (switch-like construct). - /// Generates if/else if chain since Zig switch requires comptime values. - fn emitCase(self: *Compiler, c: ast.Case) anyerror!void { - try self.flush(); - - var accessor_buf: [512]u8 = undefined; - const expr_accessor = self.buildAccessor(c.expression, &accessor_buf); - - var first = true; - for (c.whens) |when| { - try self.writeIndent(); - - if (first) { - first = false; - } else { - try self.writer.writeAll("} else "); - } - - // Check if value is a string literal - if (when.value.len >= 2 and when.value[0] == '"') { - const str_val = when.value[1 .. when.value.len - 1]; - try self.writer.print("if (std.mem.eql(u8, strVal({s}), \"{s}\")) {{\n", .{ expr_accessor, str_val }); - } else { - // Numeric or other comparison - try self.writer.print("if ({s} == {s}) {{\n", .{ expr_accessor, when.value }); - } - - self.depth += 1; - - if (when.has_break) { - // Explicit break - do nothing - } else if (when.children.len == 0) { - // Fall-through - we'll handle this by continuing to next case - // For now, just skip (Zig doesn't have fall-through) - } else { - for (when.children) |child| { - try self.emitNode(child); - } - } - try self.flush(); - self.depth -= 1; - } - - // Default case - if (c.default_children.len > 0) { - try self.writeIndent(); - if (!first) { - try self.writer.writeAll("} else {\n"); - } else { - try self.writer.writeAll("{\n"); - } - self.depth += 1; - for (c.default_children) |child| { - try self.emitNode(child); - } - try self.flush(); - self.depth -= 1; - } - - if (!first or c.default_children.len > 0) { - try self.writeIndent(); - try self.writer.writeAll("}\n"); - } - } - - /// Emits a named block, applying any child template overrides. - /// Supports replace, append, and prepend modes for inheritance. - fn emitBlock(self: *Compiler, blk: ast.Block) anyerror!void { - if (self.blocks.get(blk.name)) |child_block| { - switch (child_block.mode) { - .replace => { - // Child completely replaces parent block - for (child_block.children) |child| { - try self.emitNode(child); - } - }, - .append => { - // Parent content first, then child - for (blk.children) |child| { - try self.emitNode(child); - } - for (child_block.children) |child| { - try self.emitNode(child); - } - }, - .prepend => { - // Child content first, then parent - for (child_block.children) |child| { - try self.emitNode(child); - } - for (blk.children) |child| { - try self.emitNode(child); - } - }, - } - } else { - // No override - render default block content - for (blk.children) |child| { - try self.emitNode(child); - } - } - } - - /// Emits an include directive by inlining the included template's content. - fn emitInclude(self: *Compiler, inc: ast.Include) anyerror!void { - const included_doc = self.loadTemplate(inc.path) catch |err| { - std.log.warn("Failed to load include '{s}': {}", .{ inc.path, err }); - return; - }; - - try self.collectMixins(included_doc.nodes); - - for (included_doc.nodes) |node| { - try self.emitNode(node); - } - } - - /// Emits a mixin call (+mixinName(args)). - /// Looks up the mixin definition, falling back to lazy-loading from mixins/ directory. - fn emitMixinCall(self: *Compiler, call: ast.MixinCall) anyerror!void { - const mixin_def = self.mixins.get(call.name) orelse { - // Lazy-load from mixins/ directory - if (self.loadMixinFromDir(call.name)) |def| { - try self.mixins.put(def.name, def); - try self.emitMixinCallWithDef(call, def); - return; - } - std.log.warn("Mixin '{s}' not found", .{call.name}); - return; - }; - - try self.emitMixinCallWithDef(call, mixin_def); - } - - /// Emits the actual mixin body with parameter bindings. - /// Creates a scope block with local variables for each mixin parameter. - /// Handles default values, rest parameters, block content, and &attributes. - fn emitMixinCallWithDef(self: *Compiler, call: ast.MixinCall, mixin_def: ast.MixinDef) anyerror!void { - // Save/restore mixin params to handle nested mixin calls - const prev_params_len = self.mixin_params.items.len; - defer self.mixin_params.items.len = prev_params_len; - - const regular_params = if (mixin_def.has_rest and mixin_def.params.len > 0) - mixin_def.params.len - 1 - else - mixin_def.params.len; - - try self.flush(); - - // Scope block prevents variable name collisions on repeated mixin calls - try self.writeIndent(); - try self.writer.writeAll("{\n"); - self.depth += 1; - - for (mixin_def.params[0..regular_params], 0..) |param, i| { - try self.mixin_params.append(self.allocator, param); - - // Escape param name if it's a Zig keyword - var ident_buf: [64]u8 = undefined; - const safe_param = escapeIdent(param, &ident_buf); - - if (i < call.args.len) { - // Argument provided - const arg = call.args[i]; - // Check if it's a string literal - if (arg.len >= 2 and (arg[0] == '"' or arg[0] == '\'')) { - try self.writeIndent(); - try self.writer.print("const {s} = {s};\n", .{ safe_param, arg }); - } else { - // It's a variable reference - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(arg, &accessor_buf); - // Skip declaration if accessor equals param name (already in scope) - if (!std.mem.eql(u8, accessor, safe_param)) { - try self.writeIndent(); - try self.writer.print("const {s} = {s};\n", .{ safe_param, accessor }); - } - } - } else if (i < mixin_def.defaults.len) { - // Use default value - try self.writeIndent(); - if (mixin_def.defaults[i]) |default| { - try self.writer.print("const {s} = {s};\n", .{ safe_param, default }); - } else { - try self.writer.print("const {s} = \"\";\n", .{safe_param}); - } - } else { - // No value - use empty string - try self.writeIndent(); - try self.writer.print("const {s} = \"\";\n", .{safe_param}); - } - } - - // Handle rest parameters - if (mixin_def.has_rest and mixin_def.params.len > 0) { - const rest_param = mixin_def.params[mixin_def.params.len - 1]; - try self.mixin_params.append(self.allocator, rest_param); - - // Rest args are remaining arguments as an array - try self.writeIndent(); - try self.writer.print("const {s} = &[_][]const u8{{", .{rest_param}); - - for (call.args[regular_params..], 0..) |arg, i| { - if (i > 0) try self.writer.writeAll(", "); - try self.writer.print("{s}", .{arg}); - } - try self.writer.writeAll("};\n"); - } - - // Check if mixin body actually uses &attributes before emitting the struct - const uses_attributes = mixinUsesAttributes(mixin_def.children); - - // Save previous attrs var and restore after mixin body - const prev_attrs_var = self.current_attrs_var; - defer self.current_attrs_var = prev_attrs_var; - - // Only emit attributes struct if the mixin actually uses it - if (uses_attributes) { - // Use unique name based on mixin depth to avoid shadowing in nested mixin calls - self.mixin_depth += 1; - const current_depth = self.mixin_depth; - - var attr_var_buf: [32]u8 = undefined; - const attr_var_name = std.fmt.bufPrint(&attr_var_buf, "mixin_attrs_{d}", .{current_depth}) catch "mixin_attrs"; - - self.current_attrs_var = attr_var_name; - try self.mixin_params.append(self.allocator, attr_var_name); - - try self.writeIndent(); - try self.writer.print("const {s}: struct {{\n", .{attr_var_name}); - self.depth += 1; - try self.writeIndent(); - try self.writer.writeAll("class: []const u8 = \"\",\n"); - try self.writeIndent(); - try self.writer.writeAll("id: []const u8 = \"\",\n"); - try self.writeIndent(); - try self.writer.writeAll("style: []const u8 = \"\",\n"); - self.depth -= 1; - try self.writeIndent(); - try self.writer.writeAll("} = .{\n"); - self.depth += 1; - - for (call.attributes) |attr| { - if (std.mem.eql(u8, attr.name, "class") or - std.mem.eql(u8, attr.name, "id") or - std.mem.eql(u8, attr.name, "style")) - { - try self.writeIndent(); - try self.writer.print(".{s} = ", .{attr.name}); - if (attr.value) |val| { - if (val.len >= 2 and (val[0] == '"' or val[0] == '\'')) { - try self.writer.print("{s},\n", .{val}); - } else { - var accessor_buf: [512]u8 = undefined; - const accessor = self.buildAccessor(val, &accessor_buf); - try self.writer.print("{s},\n", .{accessor}); - } - } else { - try self.writer.writeAll("\"\",\n"); - } - } - } - - self.depth -= 1; - try self.writeIndent(); - try self.writer.writeAll("};\n"); - } - - // Emit mixin body - for (mixin_def.children) |child| { - if (child == .mixin_block) { - for (call.block_children) |block_child| { - try self.emitNode(block_child); - } - } else { - try self.emitNode(child); - } - } - - // Close scope block - try self.flush(); - self.depth -= 1; - try self.writeIndent(); - try self.writer.writeAll("}\n"); - - if (uses_attributes) { - self.mixin_depth -= 1; - } - } - - /// Attempts to load a mixin from the mixins/ subdirectory. - /// First tries mixins/{name}.pug, then scans all files in mixins/ for the definition. - fn loadMixinFromDir(self: *Compiler, name: []const u8) ?ast.MixinDef { - const specific_path = std.fs.path.join(self.allocator, &.{ self.source_dir, "mixins", name }) catch return null; - defer self.allocator.free(specific_path); - - const with_ext = std.fmt.allocPrint(self.allocator, "{s}{s}", .{ specific_path, self.extension }) catch return null; - defer self.allocator.free(with_ext); - - if (std.fs.cwd().readFileAlloc(self.allocator, with_ext, 1024 * 1024)) |source| { - if (self.parseMixinFromSource(source, name)) |def| { - return def; - } - } else |_| {} - - // Try scanning all files in mixins directory - const mixins_dir_path = std.fs.path.join(self.allocator, &.{ self.source_dir, "mixins" }) catch return null; - defer self.allocator.free(mixins_dir_path); - - var dir = std.fs.cwd().openDir(mixins_dir_path, .{ .iterate = true }) catch return null; - defer dir.close(); - - var iter = dir.iterate(); - while (iter.next() catch return null) |entry| { - if (entry.kind == .file and std.mem.endsWith(u8, entry.name, self.extension)) { - const file_path = std.fs.path.join(self.allocator, &.{ mixins_dir_path, entry.name }) catch continue; - defer self.allocator.free(file_path); - - if (std.fs.cwd().readFileAlloc(self.allocator, file_path, 1024 * 1024)) |source| { - if (self.parseMixinFromSource(source, name)) |def| { - return def; - } - } else |_| {} - } - } - - return null; - } - - /// Parses template source to find and return a specific mixin definition by name. - fn parseMixinFromSource(self: *Compiler, source: []const u8, name: []const u8) ?ast.MixinDef { - var lexer = Lexer.init(self.allocator, source); - const tokens = lexer.tokenize() catch return null; - - var parser = Parser.init(self.allocator, tokens); - const doc = parser.parse() catch return null; - - // Find the mixin with matching name - for (doc.nodes) |node| { - if (node == .mixin_def) { - if (std.mem.eql(u8, node.mixin_def.name, name)) { - return node.mixin_def; - } - } - } - - return null; - } -}; - -/// Parses a JS-style object literal into CSS property string. -/// Example: {color: 'red', background: 'green'} -> "color:red;background:green;" -/// Note: Returns slice from static buffer - safe because result is immediately consumed. -fn parseObjectToCSS(input: []const u8) []const u8 { - const trimmed = std.mem.trim(u8, input, " \t\n\r"); - - // Must start with { and end with } - if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') { - return input; - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return ""; - - // Use comptime buffer for simple cases - var result: [1024]u8 = undefined; - var result_len: usize = 0; - - var pos: usize = 0; - while (pos < content.len) { - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; - - // Parse property name - const name_start = pos; - while (pos < content.len and content[pos] != ':' and content[pos] != ' ') { - pos += 1; - } - const name = content[name_start..pos]; - - // Skip to colon - while (pos < content.len and content[pos] != ':') { - pos += 1; - } - if (pos >= content.len) break; - pos += 1; // skip : - - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) { - pos += 1; - } - - // Parse value (handle quoted strings) - var value_start = pos; - var value_end = pos; - if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; // skip closing quote - } else { - // Unquoted value - read until comma or end - while (pos < content.len and content[pos] != ',' and content[pos] != '}') { - pos += 1; - } - value_end = pos; - // Trim trailing whitespace from value - while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) { - value_end -= 1; - } - } - const value = content[value_start..value_end]; - - // Append name:value; - if (result_len + name.len + 1 + value.len + 1 < result.len) { - @memcpy(result[result_len..][0..name.len], name); - result_len += name.len; - result[result_len] = ':'; - result_len += 1; - @memcpy(result[result_len..][0..value.len], value); - result_len += value.len; - result[result_len] = ';'; - result_len += 1; - } - - // Skip comma and whitespace - while (pos < content.len and (content[pos] == ',' or content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - } - - // Return slice from static buffer - this works because we're building static strings - return result[0..result_len]; -} - -/// Parses a JS-style object literal and extracts values as space-separated string. -/// Example: {foo: 'bar', baz: 'qux'} -> "bar qux" -fn parseObjectToSpaceSeparated(input: []const u8) []const u8 { - const trimmed = std.mem.trim(u8, input, " \t\n\r"); - if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') { - return input; - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return ""; - - var result: [1024]u8 = undefined; - var result_len: usize = 0; - var first = true; - - var pos: usize = 0; - while (pos < content.len) { - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; - - // Skip property name until colon - while (pos < content.len and content[pos] != ':') { - pos += 1; - } - if (pos >= content.len) break; - pos += 1; // skip : - - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) { - pos += 1; - } - - // Parse value - var value_start = pos; - var value_end = pos; - if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; - } else { - while (pos < content.len and content[pos] != ',' and content[pos] != '}') { - pos += 1; - } - value_end = pos; - while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) { - value_end -= 1; - } - } - const value = content[value_start..value_end]; - - // Append value with space separator - if (result_len + (if (first) @as(usize, 0) else @as(usize, 1)) + value.len < result.len) { - if (!first) { - result[result_len] = ' '; - result_len += 1; - } - @memcpy(result[result_len..][0..value.len], value); - result_len += value.len; - first = false; - } - - // Skip comma and whitespace - while (pos < content.len and (content[pos] == ',' or content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - } - - return result[0..result_len]; -} - -/// Parses a JS-style array literal and joins values with spaces. -/// Example: ['foo', 'bar', 'baz'] -> "foo bar baz" -fn parseArrayToSpaceSeparated(input: []const u8) []const u8 { - const trimmed = std.mem.trim(u8, input, " \t\n\r"); - if (trimmed.len < 2 or trimmed[0] != '[' or trimmed[trimmed.len - 1] != ']') { - return input; - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return ""; - - var result: [1024]u8 = undefined; - var result_len: usize = 0; - var first = true; - - var pos: usize = 0; - while (pos < content.len) { - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; - - // Parse value - var value_start = pos; - var value_end = pos; - if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; - } else { - while (pos < content.len and content[pos] != ',' and content[pos] != ']') { - pos += 1; - } - value_end = pos; - while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) { - value_end -= 1; - } - } - const value = content[value_start..value_end]; - - // Append value with space separator - if (value.len > 0 and result_len + (if (first) @as(usize, 0) else @as(usize, 1)) + value.len < result.len) { - if (!first) { - result[result_len] = ' '; - result_len += 1; - } - @memcpy(result[result_len..][0..value.len], value); - result_len += value.len; - first = false; - } - - // Skip comma and whitespace - while (pos < content.len and (content[pos] == ',' or content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - } - - return result[0..result_len]; -} - -/// Returns true if the tag is a void element (self-closing, no closing tag). -fn isVoidElement(tag: []const u8) bool { - const voids = std.StaticStringMap(void).initComptime(.{ - .{ "area", {} }, .{ "base", {} }, .{ "br", {} }, .{ "col", {} }, - .{ "embed", {} }, .{ "hr", {} }, .{ "img", {} }, .{ "input", {} }, - .{ "link", {} }, .{ "meta", {} }, .{ "param", {} }, .{ "source", {} }, - .{ "track", {} }, .{ "wbr", {} }, - }); - return voids.has(tag); -} diff --git a/src/codegen.zig b/src/codegen.zig index 45670ed..0f7c379 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -1,34 +1,44 @@ -//! Pugz Code Generator - Converts AST to HTML output. -//! -//! This module traverses the AST and generates HTML strings. It handles: -//! - Element rendering with tags, classes, IDs, and attributes -//! - Text content with interpolation placeholders -//! - Proper indentation for pretty-printed output -//! - Self-closing tags (void elements) -//! - Comment rendering +// 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 ast = @import("ast.zig"); +const Allocator = std.mem.Allocator; +const mem = std.mem; -/// Configuration options for code generation. -pub const Options = struct { - /// Enable pretty-printing with indentation and newlines. - pretty: bool = true, - /// Indentation string (spaces or tabs). - indent_str: []const u8 = " ", - /// Enable self-closing tag syntax for void elements. - self_closing: bool = true, -}; +// 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; -/// Errors that can occur during code generation. -pub const CodeGenError = error{ - OutOfMemory, -}; +// Import runtime for attribute handling and HTML escaping +const runtime = @import("runtime.zig"); +pub const escapeChar = runtime.escapeChar; -/// HTML void elements that should not have closing tags. -/// -/// ref: https://developer.mozilla.org/en-US/docs/Glossary/Void_element -const void_elements = std.StaticStringMap(void).initComptime(.{ +// 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", "<!DOCTYPE html>" }, + .{ "xml", "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" }, + .{ "transitional", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" }, + .{ "strict", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" }, + .{ "frameset", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">" }, + .{ "1.1", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" }, + .{ "basic", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML Basic 1.1//EN\" \"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd\">" }, + .{ "mobile", "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.2//EN\" \"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd\">" }, + .{ "plist", "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" }, +}); + +// Self-closing (void) elements in HTML5 +pub const void_elements = std.StaticStringMap(void).initComptime(.{ .{ "area", {} }, .{ "base", {} }, .{ "br", {} }, @@ -45,738 +55,861 @@ const void_elements = std.StaticStringMap(void).initComptime(.{ .{ "wbr", {} }, }); -/// Whitespace-sensitive elements where pretty-printing should be disabled. -const whitespace_sensitive = std.StaticStringMap(void).initComptime(.{ +// Whitespace-sensitive tags +pub const whitespace_sensitive_tags = std.StaticStringMap(void).initComptime(.{ .{ "pre", {} }, .{ "textarea", {} }, .{ "script", {} }, .{ "style", {} }, }); -/// Code generator that converts AST to HTML. -pub const CodeGen = struct { - allocator: std.mem.Allocator, - options: Options, - output: std.ArrayList(u8), - depth: usize, - /// Track if we're inside a whitespace-sensitive element. - preserve_whitespace: bool, +// ============================================================================ +// Compiler Options +// ============================================================================ - /// Creates a new code generator with the given options. - pub fn init(allocator: std.mem.Allocator, options: Options) CodeGen { - return .{ +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 = .empty, - .depth = 0, - .preserve_whitespace = false, + .output = .{}, + .terse = options.terse, }; + + // Set up doctype + if (options.doctype) |dt| { + compiler.setDoctype(dt); + } + + return compiler; } - /// Releases allocated memory. - pub fn deinit(self: *CodeGen) void { + pub fn deinit(self: *Compiler) void { self.output.deinit(self.allocator); } - /// Generates HTML from the given document AST. - /// Returns a slice of the generated HTML owned by the CodeGen. - pub fn generate(self: *CodeGen, doc: ast.Document) CodeGenError![]const u8 { - // Pre-allocate reasonable capacity - try self.output.ensureTotalCapacity(self.allocator, 1024); + /// 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); + } - for (doc.nodes) |node| { - try self.visitNode(node); + /// 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; } - return self.output.items; + // HTML5 uses terse mode + self.terse = mem.eql(u8, lower, "html"); } - /// Generates HTML and returns an owned copy. - /// Caller must free the returned slice. - pub fn generateOwned(self: *CodeGen, doc: ast.Document) CodeGenError![]u8 { - const result = try self.generate(doc); - return try self.allocator.dupe(u8, result); + // ======================================================================== + // Output Helpers + // ======================================================================== + + fn write(self: *Compiler, str: []const u8) CompilerError!void { + try self.output.appendSlice(self.allocator, str); } - /// Visits a single AST node and generates corresponding HTML. - fn visitNode(self: *CodeGen, node: ast.Node) CodeGenError!void { - switch (node) { - .doctype => |dt| try self.visitDoctype(dt), - .element => |elem| try self.visitElement(elem), - .text => |text| try self.visitText(text), - .comment => |comment| try self.visitComment(comment), - .conditional => |cond| try self.visitConditional(cond), - .each => |each| try self.visitEach(each), - .@"while" => |whl| try self.visitWhile(whl), - .case => |c| try self.visitCase(c), - .mixin_def => {}, // Mixin definitions don't produce direct output - .mixin_call => |call| try self.visitMixinCall(call), - .mixin_block => {}, // Mixin block placeholder - handled at mixin call site - .include => |inc| try self.visitInclude(inc), - .extends => {}, // Handled at document level - .block => |blk| try self.visitBlock(blk), - .raw_text => |raw| try self.visitRawText(raw), - .code => |code| try self.visitCode(code), - .document => |doc| { - for (doc.nodes) |child| { - try self.visitNode(child); + fn writeChar(self: *Compiler, c: u8) CompilerError!void { + try self.output.append(self.allocator, c); + } + + fn writeEscaped(self: *Compiler, str: []const u8) CompilerError!void { + // For attribute values - escapes < > & " + for (str) |c| { + if (escapeChar(c)) |escaped| { + try self.write(escaped); + } else { + try self.writeChar(c); + } + } + } + + fn writeTextEscaped(self: *Compiler, str: []const u8) CompilerError!void { + // For text content - escapes < > & (NOT quotes) + // Preserves existing HTML entities like ’ or & + var i: usize = 0; + while (i < str.len) { + const c = str[i]; + switch (c) { + '<' => try self.write("<"), + '>' => try self.write(">"), + '&' => { + // Check if this is already an HTML entity + if (isHtmlEntity(str[i..])) { + // Pass through the entity as-is + try self.writeChar(c); + } else { + try self.write("&"); + } + }, + else => try self.writeChar(c), + } + i += 1; + } + } + + fn isHtmlEntity(str: []const u8) bool { + // Check if str starts with a valid HTML entity: &name; or &#digits; or &#xhex; + if (str.len < 3 or str[0] != '&') return false; + + var i: usize = 1; + + // Numeric entity: &#digits; or &#xhex; + if (str[i] == '#') { + i += 1; + if (i >= str.len) return false; + + // Hex entity: &#x...; + if (str[i] == 'x' or str[i] == 'X') { + i += 1; + if (i >= str.len) return false; + // Need at least one hex digit + var has_hex = false; + while (i < str.len and i < 10) : (i += 1) { + const ch = str[i]; + if (ch == ';') return has_hex; + if ((ch >= '0' and ch <= '9') or + (ch >= 'a' and ch <= 'f') or + (ch >= 'A' and ch <= 'F')) + { + has_hex = true; + } else { + return false; + } } + return false; + } + + // Decimal entity: &#digits; + var has_digit = false; + while (i < str.len and i < 10) : (i += 1) { + const ch = str[i]; + if (ch == ';') return has_digit; + if (ch >= '0' and ch <= '9') { + has_digit = true; + } else { + return false; + } + } + return false; + } + + // Named entity: &name; + var has_alpha = false; + while (i < str.len and i < 32) : (i += 1) { + const ch = str[i]; + if (ch == ';') return has_alpha; + if ((ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or (ch >= '0' and ch <= '9')) { + has_alpha = true; + } else { + return false; + } + } + return false; + } + + 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; }, } } - /// Doctype shortcuts mapping - const doctype_shortcuts = std.StaticStringMap([]const u8).initComptime(.{ - .{ "html", "<!DOCTYPE html>" }, - .{ "xml", "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" }, - .{ "transitional", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" }, - .{ "strict", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" }, - .{ "frameset", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">" }, - .{ "1.1", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" }, - .{ "basic", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML Basic 1.1//EN\" \"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd\">" }, - .{ "mobile", "<!DOCTYPE html PUBLIC \"-//WAPFORUM//DTD XHTML Mobile 1.2//EN\" \"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd\">" }, - .{ "plist", "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" }, - }); - - /// Generates doctype declaration. - fn visitDoctype(self: *CodeGen, dt: ast.Doctype) CodeGenError!void { - if (doctype_shortcuts.get(dt.value)) |output| { - try self.write(output); - } else { - try self.write("<!DOCTYPE "); - try self.write(dt.value); - try self.write(">"); + fn visitBlock(self: *Compiler, block: *Node) CompilerError!void { + for (block.nodes.items) |child| { + try self.visit(child); } - try self.writeNewline(); } - /// Generates HTML for an element node. - fn visitElement(self: *CodeGen, elem: ast.Element) CodeGenError!void { - const is_void_element = void_elements.has(elem.tag) or elem.self_closing; - const was_preserving = self.preserve_whitespace; + fn visitTag(self: *Compiler, tag: *Node) CompilerError!void { + const name = tag.name orelse return error.InvalidNode; - // Check if entering whitespace-sensitive element - if (whitespace_sensitive.has(elem.tag)) { - self.preserve_whitespace = true; + // 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.writeIndent(); - try self.write("<"); - try self.write(elem.tag); + try self.writeChar('<'); + try self.write(name); - // ID attribute - if (elem.id) |id| { - try self.write(" id=\""); - try self.writeEscaped(id); - try self.write("\""); - } + // Attributes + try self.visitAttributes(tag); - // Class attribute - if (elem.classes.len > 0) { - try self.write(" class=\""); - for (elem.classes, 0..) |class, i| { - if (i > 0) try self.write(" "); - try self.writeEscaped(class); - } - try self.write("\""); - } - - // Other attributes - for (elem.attributes) |attr| { - try self.write(" "); - try self.write(attr.name); - if (attr.value) |value| { - try self.write("=\""); - if (attr.escaped) { - try self.writeEscaped(value); - } else { - try self.write(value); - } - try self.write("\""); + if (is_self_closing) { + if (self.terse and !tag.self_closing) { + try self.writeChar('>'); } else { - // Boolean attribute: checked -> checked="checked" - try self.write("=\""); + 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("</"); + try self.write(name); + try self.writeChar('>'); + } + // escape_pretty restoration handled by defer above + } + + fn visitAttributes(self: *Compiler, tag: *Node) CompilerError!void { + for (tag.attrs.items) |attr| { + if (attr.val) |val| { + // Skip empty class/style attributes + if (mem.eql(u8, attr.name, "class") or mem.eql(u8, attr.name, "style")) { + // Skip if value is empty, null, or undefined + if (val.len == 0 or + mem.eql(u8, val, "''") or + mem.eql(u8, val, "\"\"") or + mem.eql(u8, val, "null") or + mem.eql(u8, val, "undefined")) + { + continue; + } + } + + // 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 + try self.writeChar(' '); + try self.write(attr.name); + continue; + } else { + // false: don't output the attribute at all + continue; + } + } + + try self.writeChar(' '); try self.write(attr.name); - try self.write("\""); + try self.write("=\""); + if (attr.must_escape) { + try self.writeEscaped(val); + } else { + try self.write(val); + } + try self.writeChar('"'); + } else { + // No value - output attribute name only (boolean attribute) + try self.writeChar(' '); + try self.write(attr.name); + } + } + } + + fn visitText(self: *Compiler, text: *Node) CompilerError!void { + if (text.val) |val| { + if (text.is_html) { + try self.write(val); + } else { + // Text content: only escape < > & (not quotes) + try self.writeTextEscaped(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); + } } } - // Close opening tag - if (is_void_element and self.options.self_closing) { - try self.write(" />"); - try self.writeNewline(); - self.preserve_whitespace = was_preserving; + // 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("<!--"); + if (comment.val) |val| { + try self.write(val); + } + try self.write("-->"); + } + + fn visitBlockComment(self: *Compiler, comment: *Node) CompilerError!void { + if (!comment.buffer) return; + + try self.prettyIndent(); + try self.write("<!--"); + if (comment.val) |val| { + try self.write(val); + } + + // Visit block content + for (comment.nodes.items) |child| { + try self.visit(child); + } + + 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("<!DOCTYPE html>"); + } + 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; } - try self.write(">"); - - // Inline text - const has_inline_text = elem.inline_text != null and elem.inline_text.?.len > 0; - const has_children = elem.children.len > 0; - - if (has_inline_text) { - try self.writeTextSegments(elem.inline_text.?); - } - - // Children - if (has_children) { - if (!self.preserve_whitespace) { - try self.writeNewline(); - } - self.depth += 1; - for (elem.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - if (!self.preserve_whitespace) { - try self.writeIndent(); - } - } - - // Closing tag (not for void elements) - if (!is_void_element) { - try self.write("</"); - try self.write(elem.tag); - try self.write(">"); - try self.writeNewline(); - } - - self.preserve_whitespace = was_preserving; - } - - /// Generates output for a text node. - fn visitText(self: *CodeGen, text: ast.Text) CodeGenError!void { - try self.writeIndent(); - try self.writeTextSegments(text.segments); - try self.writeNewline(); - } - - /// Generates HTML comment. - fn visitComment(self: *CodeGen, comment: ast.Comment) CodeGenError!void { - if (!comment.rendered) return; - - try self.writeIndent(); - try self.write("<!--"); - if (comment.content.len > 0) { - try self.write(" "); - try self.write(comment.content); - try self.write(" "); - } - try self.write("-->"); - try self.writeNewline(); - } - - /// Generates placeholder for conditional (runtime evaluation needed). - fn visitConditional(self: *CodeGen, cond: ast.Conditional) CodeGenError!void { - // Output each branch with placeholder comments - for (cond.branches, 0..) |branch, i| { - try self.writeIndent(); - if (i == 0) { - if (branch.is_unless) { - try self.write("<!-- unless "); - } else { - try self.write("<!-- if "); - } - if (branch.condition) |condition| { - try self.write(condition); - } - try self.write(" -->"); - } else if (branch.condition) |condition| { - try self.write("<!-- else if "); - try self.write(condition); - try self.write(" -->"); - } else { - try self.write("<!-- else -->"); - } - try self.writeNewline(); - - self.depth += 1; - for (branch.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - } - - try self.writeIndent(); - try self.write("<!-- endif -->"); - try self.writeNewline(); - } - - /// Generates placeholder for each loop (runtime evaluation needed). - fn visitEach(self: *CodeGen, each: ast.Each) CodeGenError!void { - try self.writeIndent(); - try self.write("<!-- each "); - try self.write(each.value_name); - if (each.index_name) |idx| { - try self.write(", "); - try self.write(idx); - } - try self.write(" in "); - try self.write(each.collection); - try self.write(" -->"); - try self.writeNewline(); - - self.depth += 1; - for (each.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - - if (each.else_children.len > 0) { - try self.writeIndent(); - try self.write("<!-- else -->"); - try self.writeNewline(); - self.depth += 1; - for (each.else_children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - } - - try self.writeIndent(); - try self.write("<!-- endeach -->"); - try self.writeNewline(); - } - - /// Generates placeholder for while loop (runtime evaluation needed). - fn visitWhile(self: *CodeGen, whl: ast.While) CodeGenError!void { - try self.writeIndent(); - try self.write("<!-- while "); - try self.write(whl.condition); - try self.write(" -->"); - try self.writeNewline(); - - self.depth += 1; - for (whl.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - - try self.writeIndent(); - try self.write("<!-- endwhile -->"); - try self.writeNewline(); - } - - /// Generates placeholder for case statement (runtime evaluation needed). - fn visitCase(self: *CodeGen, c: ast.Case) CodeGenError!void { - try self.writeIndent(); - try self.write("<!-- case "); - try self.write(c.expression); - try self.write(" -->"); - try self.writeNewline(); - - for (c.whens) |when| { - try self.writeIndent(); - try self.write("<!-- when "); - try self.write(when.value); - try self.write(" -->"); - try self.writeNewline(); - - self.depth += 1; - for (when.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - } - - if (c.default_children.len > 0) { - try self.writeIndent(); - try self.write("<!-- default -->"); - try self.writeNewline(); - self.depth += 1; - for (c.default_children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - } - - try self.writeIndent(); - try self.write("<!-- endcase -->"); - try self.writeNewline(); - } - - /// Generates placeholder for mixin call (runtime evaluation needed). - fn visitMixinCall(self: *CodeGen, call: ast.MixinCall) CodeGenError!void { - try self.writeIndent(); - try self.write("<!-- +"); - try self.write(call.name); - try self.write(" -->"); - try self.writeNewline(); - } - - /// Generates placeholder for include (file loading needed). - fn visitInclude(self: *CodeGen, inc: ast.Include) CodeGenError!void { - try self.writeIndent(); - try self.write("<!-- include "); - try self.write(inc.path); - try self.write(" -->"); - try self.writeNewline(); - } - - /// Generates content for a named block. - fn visitBlock(self: *CodeGen, blk: ast.Block) CodeGenError!void { - try self.writeIndent(); - try self.write("<!-- block "); - try self.write(blk.name); - try self.write(" -->"); - try self.writeNewline(); - - self.depth += 1; - for (blk.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - - try self.writeIndent(); - try self.write("<!-- endblock -->"); - try self.writeNewline(); - } - - /// Generates raw text content (for script/style blocks). - fn visitRawText(self: *CodeGen, raw: ast.RawText) CodeGenError!void { - try self.writeIndent(); - try self.write(raw.content); - try self.writeNewline(); - } - - /// Generates code output (escaped or unescaped). - fn visitCode(self: *CodeGen, code: ast.Code) CodeGenError!void { - try self.writeIndent(); - if (code.escaped) { - try self.write("{{ "); - } else { - try self.write("{{{ "); - } - try self.write(code.expression); - if (code.escaped) { - try self.write(" }}"); - } else { - try self.write(" }}}"); - } - try self.writeNewline(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Output helpers - // ───────────────────────────────────────────────────────────────────────── - - /// Writes text segments, handling interpolation. - fn writeTextSegments(self: *CodeGen, segments: []const ast.TextSegment) CodeGenError!void { - for (segments) |seg| { - switch (seg) { - .literal => |lit| try self.writeEscaped(lit), - .interp_escaped => |expr| { - try self.write("{{ "); - try self.write(expr); - try self.write(" }}"); - }, - .interp_unescaped => |expr| { - try self.write("{{{ "); - try self.write(expr); - try self.write(" }}}"); - }, - .interp_tag => |inline_tag| { - try self.writeInlineTag(inline_tag); - }, - } + // Mixin call - visit block if present + for (mixin.nodes.items) |child| { + try self.visit(child); } } - /// Writes an inline tag from tag interpolation. - fn writeInlineTag(self: *CodeGen, tag: ast.InlineTag) CodeGenError!void { - try self.write("<"); - try self.write(tag.tag); - - // Write ID if present - if (tag.id) |id| { - try self.write(" id=\""); - try self.writeEscaped(id); - try self.write("\""); - } - - // Write classes if present - if (tag.classes.len > 0) { - try self.write(" class=\""); - for (tag.classes, 0..) |class, i| { - if (i > 0) try self.write(" "); - try self.writeEscaped(class); - } - try self.write("\""); - } - - // Write attributes - for (tag.attributes) |attr| { - if (attr.value) |value| { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - if (attr.escaped) { - try self.writeEscaped(value); - } else { - try self.write(value); - } - try self.write("\""); - } else { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - try self.write(attr.name); - try self.write("\""); - } - } - - try self.write(">"); - - // Write text content (may contain nested interpolations) - try self.writeTextSegments(tag.text_segments); - - try self.write("</"); - try self.write(tag.tag); - try self.write(">"); + fn visitMixinBlock(_: *Compiler, _: *Node) CompilerError!void { + // MixinBlock is a placeholder for mixin content + // Handled at mixin call site } - /// Writes indentation based on current depth. - fn writeIndent(self: *CodeGen) CodeGenError!void { - if (!self.options.pretty or self.preserve_whitespace) return; - - for (0..self.depth) |_| { - try self.write(self.options.indent_str); + fn visitCase(self: *Compiler, case_node: *Node) CompilerError!void { + // Case/switch - visit block + for (case_node.nodes.items) |child| { + try self.visit(child); } } - /// Writes a newline if pretty-printing is enabled. - fn writeNewline(self: *CodeGen) CodeGenError!void { - if (!self.options.pretty or self.preserve_whitespace) return; - try self.write("\n"); - } - - /// Writes a string directly to output. - fn write(self: *CodeGen, str: []const u8) CodeGenError!void { - try self.output.appendSlice(self.allocator, str); - } - - /// Writes a string with HTML entity escaping. - fn writeEscaped(self: *CodeGen, str: []const u8) CodeGenError!void { - for (str) |c| { - switch (c) { - '&' => try self.write("&"), - '<' => try self.write("<"), - '>' => try self.write(">"), - '"' => try self.write("""), - '\'' => try self.write("'"), - else => try self.output.append(self.allocator, c), - } + 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 function -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ +// Convenience Functions +// ============================================================================ -/// Generates HTML from an AST document with default options. -/// Returns an owned slice that the caller must free. -pub fn generate(allocator: std.mem.Allocator, doc: ast.Document) CodeGenError![]u8 { - var gen = CodeGen.init(allocator, .{}); - defer gen.deinit(); - return gen.generateOwned(doc); +/// 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); } -/// Generates HTML with custom options. -/// Returns an owned slice that the caller must free. -pub fn generateWithOptions(allocator: std.mem.Allocator, doc: ast.Document, options: Options) CodeGenError![]u8 { - var gen = CodeGen.init(allocator, options); - defer gen.deinit(); - return gen.generateOwned(doc); +/// 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 "generate simple element" { +test "compile - simple text" { const allocator = std.testing.allocator; - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .element = .{ - .tag = "div", - .id = null, - .classes = &.{}, - .attributes = &.{}, - .inline_text = null, - .children = &.{}, - .self_closing = false, - } }, - }), + const text = try allocator.create(Node); + text.* = Node{ + .type = .Text, + .val = "Hello, World!", + .line = 1, + .column = 1, }; - const html = try generate(allocator, doc); - defer allocator.free(html); + var root = try allocator.create(Node); + root.* = Node{ + .type = .Block, + .line = 1, + .column = 1, + }; + try root.nodes.append(allocator, text); - try std.testing.expectEqualStrings("<div></div>\n", html); + 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 "generate element with id and class" { +test "compile - simple tag" { const allocator = std.testing.allocator; - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .element = .{ - .tag = "div", - .id = "main", - .classes = &.{ "container", "active" }, - .attributes = &.{}, - .inline_text = null, - .children = &.{}, - .self_closing = false, - } }, - }), + const tag = try allocator.create(Node); + tag.* = Node{ + .type = .Tag, + .name = "div", + .line = 1, + .column = 1, }; - const html = try generate(allocator, doc); - defer allocator.free(html); + var root = try allocator.create(Node); + root.* = Node{ + .type = .Block, + .line = 1, + .column = 1, + }; + try root.nodes.append(allocator, tag); - try std.testing.expectEqualStrings("<div id=\"main\" class=\"container active\"></div>\n", html); + defer { + root.deinit(allocator); + allocator.destroy(root); + } + + const output = try compile(allocator, root); + defer allocator.free(output); + + try std.testing.expectEqualStrings("<div></div>", output); } -test "generate void element" { +test "compile - tag with text" { const allocator = std.testing.allocator; - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .element = .{ - .tag = "br", - .id = null, - .classes = &.{}, - .attributes = &.{}, - .inline_text = null, - .children = &.{}, - .self_closing = false, - } }, - }), + const text = try allocator.create(Node); + text.* = Node{ + .type = .Text, + .val = "Hello", + .line = 1, + .column = 5, }; - const html = try generate(allocator, doc); - defer allocator.free(html); + const tag = try allocator.create(Node); + tag.* = Node{ + .type = .Tag, + .name = "p", + .line = 1, + .column = 1, + }; + try tag.nodes.append(allocator, text); - try std.testing.expectEqualStrings("<br />\n", html); + 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("<p>Hello</p>", output); } -test "generate nested elements" { +test "compile - tag with attributes" { const allocator = std.testing.allocator; - var inner_text = [_]ast.TextSegment{.{ .literal = "Hello" }}; - var inner_node = [_]ast.Node{ - .{ .element = .{ - .tag = "p", - .id = null, - .classes = &.{}, - .attributes = &.{}, - .inline_text = &inner_text, - .children = &.{}, - .self_closing = false, - } }, + 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, + }); - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .element = .{ - .tag = "div", - .id = null, - .classes = &.{}, - .attributes = &.{}, - .inline_text = null, - .children = &inner_node, - .self_closing = false, - } }, - }), + var root = try allocator.create(Node); + root.* = Node{ + .type = .Block, + .line = 1, + .column = 1, }; + try root.nodes.append(allocator, tag); - const html = try generate(allocator, doc); - defer allocator.free(html); + defer { + root.deinit(allocator); + allocator.destroy(root); + } - const expected = - \\<div> - \\ <p>Hello</p> - \\</div> - \\ - ; + const output = try compile(allocator, root); + defer allocator.free(output); - try std.testing.expectEqualStrings(expected, html); + try std.testing.expectEqualStrings("<a href=\"/home\"></a>", output); } -test "generate with interpolation" { +test "compile - self-closing tag" { const allocator = std.testing.allocator; - var inline_text = [_]ast.TextSegment{ - .{ .literal = "Hello, " }, - .{ .interp_escaped = "name" }, - .{ .literal = "!" }, + const tag = try allocator.create(Node); + tag.* = Node{ + .type = .Tag, + .name = "br", + .line = 1, + .column = 1, }; - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .element = .{ - .tag = "p", - .id = null, - .classes = &.{}, - .attributes = &.{}, - .inline_text = &inline_text, - .children = &.{}, - .self_closing = false, - } }, - }), + var root = try allocator.create(Node); + root.* = Node{ + .type = .Block, + .line = 1, + .column = 1, }; + try root.nodes.append(allocator, tag); - const html = try generate(allocator, doc); - defer allocator.free(html); + defer { + root.deinit(allocator); + allocator.destroy(root); + } - try std.testing.expectEqualStrings("<p>Hello, {{ name }}!</p>\n", html); + const output = try compile(allocator, root); + defer allocator.free(output); + + try std.testing.expectEqualStrings("<br>", output); } -test "generate html comment" { +test "compile - nested tags" { const allocator = std.testing.allocator; - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .comment = .{ - .content = "This is a comment", - .rendered = true, - .children = &.{}, - } }, - }), + const span = try allocator.create(Node); + span.* = Node{ + .type = .Tag, + .name = "span", + .line = 2, + .column = 3, }; - const html = try generate(allocator, doc); - defer allocator.free(html); + const div = try allocator.create(Node); + div.* = Node{ + .type = .Tag, + .name = "div", + .line = 1, + .column = 1, + }; + try div.nodes.append(allocator, span); - try std.testing.expectEqualStrings("<!-- This is a comment -->\n", html); + 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("<div><span></span></div>", output); } -test "escape html entities" { +test "compile - doctype" { const allocator = std.testing.allocator; - var inline_text = [_]ast.TextSegment{.{ .literal = "<script>alert('xss')</script>" }}; - - const doc = ast.Document{ - .nodes = @constCast(&[_]ast.Node{ - .{ .element = .{ - .tag = "p", - .id = null, - .classes = &.{}, - .attributes = &.{}, - .inline_text = &inline_text, - .children = &.{}, - .self_closing = false, - } }, - }), + const doctype = try allocator.create(Node); + doctype.* = Node{ + .type = .Doctype, + .val = "html", + .line = 1, + .column = 1, }; - const html = try generate(allocator, doc); - defer allocator.free(html); + var root = try allocator.create(Node); + root.* = Node{ + .type = .Block, + .line = 1, + .column = 1, + }; + try root.nodes.append(allocator, doctype); - try std.testing.expectEqualStrings("<p><script>alert('xss')</script></p>\n", html); + defer { + root.deinit(allocator); + allocator.destroy(root); + } + + const output = try compile(allocator, root); + defer allocator.free(output); + + try std.testing.expectEqualStrings("<!DOCTYPE html>", 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("<!-- this is a comment -->", output); +} + +test "compile - text escaping" { + const allocator = std.testing.allocator; + + const text = try allocator.create(Node); + text.* = Node{ + .type = .Text, + .val = "<script>alert('xss')</script>", + .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/error.zig b/src/error.zig new file mode 100644 index 0000000..3138c6f --- /dev/null +++ b/src/error.zig @@ -0,0 +1,403 @@ +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/lexer.zig b/src/lexer.zig index e1d3f37..71bfedf 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -1,1804 +1,2686 @@ -//! Pug Lexer - Tokenizes Pug template source into a stream of tokens. -//! -//! The lexer handles indentation-based nesting (emitting indent/dedent tokens), -//! Pug-specific syntax (tags, classes, IDs, attributes), and text content -//! including interpolation markers. -//! -//! ## Error Diagnostics -//! When tokenization fails, call `getDiagnostic()` to get rich error info: -//! ```zig -//! var lexer = Lexer.init(allocator, source); -//! const tokens = lexer.tokenize() catch |err| { -//! if (lexer.getDiagnostic()) |diag| { -//! std.debug.print("{}\n", .{diag}); -//! } -//! return err; -//! }; -//! ``` - const std = @import("std"); -const diagnostic = @import("diagnostic.zig"); +const mem = std.mem; +const Allocator = std.mem.Allocator; -pub const Diagnostic = diagnostic.Diagnostic; +// ============================================================================ +// Token Types +// ============================================================================ -/// All possible token types produced by the lexer. pub const TokenType = enum { - // Structure tokens for indentation-based nesting - indent, // Increased indentation level - dedent, // Decreased indentation level - newline, // Line terminator - eof, // End of source - - // Element tokens - tag, // HTML tag name: div, p, a, span, etc. - class, // Class selector: .classname - id, // ID selector: #idname - - // Attribute tokens for (attr=value) syntax - lparen, // Opening paren: ( - rparen, // Closing paren: ) - attr_name, // Attribute name: href, class, data-id - attr_eq, // Assignment: = or != - attr_value, // Attribute value (quoted or unquoted) - comma, // Attribute separator: , - - // Text content tokens - text, // Plain text content - buffered_text, // Escaped output: = expr - unescaped_text, // Raw output: != expr - pipe_text, // Piped text: | text - dot_block, // Text block marker: . - literal_html, // Literal HTML: <tag>... - self_close, // Self-closing marker: / - - // Interpolation tokens for #{} and !{} syntax - interp_start, // Escaped interpolation: #{ - interp_start_unesc, // Unescaped interpolation: !{ - interp_end, // Interpolation end: } - - // Tag interpolation tokens for #[tag text] syntax - tag_interp_start, // Tag interpolation start: #[ - tag_interp_end, // Tag interpolation end: ] - - // Control flow keywords - kw_if, - kw_else, - kw_unless, - kw_each, - kw_for, // alias for each - kw_while, - kw_in, - kw_case, - kw_when, - kw_default, - - // Template structure keywords - kw_doctype, - kw_mixin, - kw_block, - kw_extends, - kw_include, - kw_append, - kw_prepend, - - // Mixin invocation: +mixinName - mixin_call, - - // Comment tokens - comment, // Rendered comment: // - comment_unbuffered, // Silent comment: //- - - // Unbuffered code (JS code that doesn't produce output) - unbuffered_code, // Code line: - var x = 1 - - // Miscellaneous - colon, // Block expansion: : - ampersand_attrs, // Attribute spread: &attributes + 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, }; -/// A single token with its type, value, and source location. -pub const Token = struct { - type: TokenType, - value: []const u8, // Slice into source (no allocation) - line: usize, - column: usize, -}; +// ============================================================================ +// Token Value - Tagged Union for type-safe token values +// ============================================================================ -/// Errors that can occur during lexing. -pub const LexerError = error{ - UnterminatedString, - UnmatchedBrace, - OutOfMemory, -}; +pub const TokenValue = union(enum) { + none, + string: []const u8, + boolean: bool, -/// Static map for keyword lookup. Using comptime perfect hashing would be ideal, -/// but a simple StaticStringMap is efficient for small keyword sets. -const keywords = std.StaticStringMap(TokenType).initComptime(.{ - .{ "if", .kw_if }, - .{ "else", .kw_else }, - .{ "unless", .kw_unless }, - .{ "each", .kw_each }, - .{ "for", .kw_for }, - .{ "while", .kw_while }, - .{ "case", .kw_case }, - .{ "when", .kw_when }, - .{ "default", .kw_default }, - .{ "doctype", .kw_doctype }, - .{ "mixin", .kw_mixin }, - .{ "block", .kw_block }, - .{ "extends", .kw_extends }, - .{ "include", .kw_include }, - .{ "append", .kw_append }, - .{ "prepend", .kw_prepend }, - .{ "in", .kw_in }, -}); + pub fn isNone(self: TokenValue) bool { + return self == .none; + } -/// Lexer for Pug template syntax. -/// -/// Converts source text into a sequence of tokens. Handles: -/// - Indentation tracking with indent/dedent tokens -/// - Tag, class, and ID shorthand syntax -/// - Attribute parsing within parentheses -/// - Text content and interpolation -/// - Comments and keywords -pub const Lexer = struct { - source: []const u8, - pos: usize, - line: usize, - column: usize, - indent_stack: std.ArrayList(usize), - tokens: std.ArrayList(Token), - allocator: std.mem.Allocator, - at_line_start: bool, - current_indent: usize, - in_raw_block: bool, - raw_block_indent: usize, - raw_block_started: bool, - in_comment_block: bool, - comment_block_indent: usize, - comment_block_started: bool, - comment_base_indent: usize, - /// Last error diagnostic (populated on error) - last_diagnostic: ?Diagnostic, - - /// Creates a new lexer for the given source. - /// Does not allocate; allocations happen during tokenize(). - pub fn init(allocator: std.mem.Allocator, source: []const u8) Lexer { - return .{ - .source = source, - .pos = 0, - .line = 1, - .column = 1, - .indent_stack = .empty, - .tokens = .empty, - .allocator = allocator, - .at_line_start = true, - .current_indent = 0, - .in_raw_block = false, - .raw_block_indent = 0, - .raw_block_started = false, - .in_comment_block = false, - .comment_block_indent = 0, - .comment_block_started = false, - .comment_base_indent = 0, - .last_diagnostic = null, + pub fn getString(self: TokenValue) ?[]const u8 { + return switch (self) { + .string => |s| s, + else => null, }; } - /// Returns the last error diagnostic, if any. - /// Call this after tokenize() returns an error to get detailed error info. - pub fn getDiagnostic(self: *const Lexer) ?Diagnostic { - return self.last_diagnostic; + pub fn getBool(self: TokenValue) ?bool { + return switch (self) { + .boolean => |b| b, + else => null, + }; } - /// Sets a diagnostic error with full context. - fn setDiagnostic(self: *Lexer, message: []const u8, suggestion: ?[]const u8) void { - self.last_diagnostic = Diagnostic.withContext( - @intCast(self.line), - @intCast(self.column), - message, - diagnostic.extractSourceLine(self.source, self.pos) orelse "", - suggestion, - ); + pub fn fromString(s: []const u8) TokenValue { + return .{ .string = s }; } - /// Releases all allocated memory (tokens and indent stack). - /// Call this when done with the lexer, typically via defer. - pub fn deinit(self: *Lexer) void { - self.indent_stack.deinit(self.allocator); - self.tokens.deinit(self.allocator); + pub fn fromBool(b: bool) TokenValue { + return .{ .boolean = b }; } - /// Tokenizes the source and returns the token slice. - /// - /// Returns a slice of tokens owned by the Lexer. The slice remains valid - /// until deinit() is called. On error, calls reset() via errdefer to - /// restore the lexer to a clean state for potential retry or inspection. - pub fn tokenize(self: *Lexer) ![]Token { - // Skip UTF-8 BOM if present (EF BB BF) - if (self.source.len >= 3 and - self.source[0] == 0xEF and - self.source[1] == 0xBB and - self.source[2] == 0xBF) - { - self.pos = 3; - self.column = 4; - } - - // Pre-allocate with estimated capacity: ~1 token per 10 chars is a reasonable heuristic - const estimated_tokens = @max(16, self.source.len / 10); - try self.tokens.ensureTotalCapacity(self.allocator, estimated_tokens); - try self.indent_stack.ensureTotalCapacity(self.allocator, 16); // Reasonable nesting depth - - try self.indent_stack.append(self.allocator, 0); - errdefer self.reset(); - - while (!self.isAtEnd()) { - try self.scanToken(); - } - - // Emit dedents for any remaining indentation levels - while (self.indent_stack.items.len > 1) { - _ = self.indent_stack.pop(); - try self.addToken(.dedent, ""); - } - - try self.addToken(.eof, ""); - return self.tokens.items; - } - - /// Resets lexer state while retaining allocated capacity. - /// Called on error to restore clean state for reuse. - pub fn reset(self: *Lexer) void { - self.tokens.clearRetainingCapacity(); - self.indent_stack.clearRetainingCapacity(); - self.pos = 0; - self.line = 1; - self.column = 1; - self.at_line_start = true; - self.current_indent = 0; - self.last_diagnostic = null; - } - - /// Appends a token to the output list. - fn addToken(self: *Lexer, token_type: TokenType, value: []const u8) !void { - try self.tokens.append(self.allocator, .{ - .type = token_type, - .value = value, - .line = self.line, - .column = self.column, - }); - } - - /// Main token dispatch. Processes one token based on current character. - /// Handles indentation at line start, then dispatches to specific scanners. - fn scanToken(self: *Lexer) !void { - if (self.at_line_start) { - // In comment block mode, handle indentation specially (similar to raw block) - if (self.in_comment_block) { - const indent = self.measureIndent(); - self.current_indent = indent; - - if (indent > self.comment_block_indent) { - // First line in comment block - emit indent token and record base indent - if (!self.comment_block_started) { - self.comment_block_started = true; - self.comment_base_indent = indent; // Record the base indent for stripping - try self.indent_stack.append(self.allocator, indent); - try self.addToken(.indent, ""); - } - // Scan line as raw text, stripping base indent but preserving relative indent - try self.scanCommentRawLine(indent); - self.at_line_start = false; - return; - } else { - // Exiting comment block - only emit dedent if we actually started a block - const was_started = self.comment_block_started; - self.in_comment_block = false; - self.comment_block_started = false; - if (was_started and self.indent_stack.items.len > 1) { - _ = self.indent_stack.pop(); - try self.addToken(.dedent, ""); - } - // Process indentation manually since we already consumed whitespace - // (measureIndent was already called above and self.current_indent is set) - const current_stack_indent = self.indent_stack.items[self.indent_stack.items.len - 1]; - if (indent > current_stack_indent) { - try self.indent_stack.append(self.allocator, indent); - try self.addToken(.indent, ""); - } else if (indent < current_stack_indent) { - while (self.indent_stack.items.len > 1 and - self.indent_stack.items[self.indent_stack.items.len - 1] > indent) - { - _ = self.indent_stack.pop(); - try self.addToken(.dedent, ""); - } - } - self.at_line_start = false; - return; - } - } - - // In raw block mode, handle indentation specially - if (self.in_raw_block) { - // Remember position before consuming indent - const line_start = self.pos; - const indent = self.measureIndent(); - self.current_indent = indent; - - if (indent > self.raw_block_indent) { - // First line in raw block - emit indent token - if (!self.raw_block_started) { - self.raw_block_started = true; - try self.indent_stack.append(self.allocator, indent); - try self.addToken(.indent, ""); - } - // Scan line as raw text, preserving relative indentation - try self.scanRawLineFrom(line_start); - self.at_line_start = false; - return; - } else { - // Exiting raw block - emit dedent and process normally - self.in_raw_block = false; - self.raw_block_started = false; - if (self.indent_stack.items.len > 1) { - _ = self.indent_stack.pop(); - try self.addToken(.dedent, ""); - } - try self.processIndentation(); - self.at_line_start = false; - return; - } - } - - try self.processIndentation(); - self.at_line_start = false; - } - - if (self.isAtEnd()) return; - - const c = self.peek(); - - // Whitespace (not at line start - already handled) - if (c == ' ' or c == '\t') { - self.advance(); - return; - } - - // Newline: emit token and mark next line start - if (c == '\n') { - try self.addToken(.newline, "\n"); - self.advance(); - self.line += 1; - self.column = 1; - self.at_line_start = true; - return; - } - - // Handle \r\n (Windows) and \r (old Mac) - if (c == '\r') { - self.advance(); - if (self.peek() == '\n') { - self.advance(); - } - try self.addToken(.newline, "\n"); - self.line += 1; - self.column = 1; - self.at_line_start = true; - return; - } - - // Comments: // or //- - if (c == '/' and self.peekNext() == '/') { - try self.scanComment(); - return; - } - - // Self-closing marker: / at end of tag (before newline or space) - if (c == '/') { - const next = self.peekNext(); - if (next == '\n' or next == '\r' or next == ' ' or next == 0) { - self.advance(); - try self.addToken(.self_close, "/"); - return; - } - } - - // Dot: either .class or . (text block) - if (c == '.') { - const next = self.peekNext(); - if (next == '\n' or next == '\r' or next == 0) { - self.advance(); - try self.addToken(.dot_block, "."); - // Mark that we're entering a raw text block - self.in_raw_block = true; - self.raw_block_indent = self.current_indent; - return; - } - if (isAlpha(next) or next == '-' or next == '_') { - try self.scanClass(); - return; - } - } - - // Hash: either #id, #{interpolation}, or #[tag interpolation] - if (c == '#') { - const next = self.peekNext(); - if (next == '{') { - self.advance(); - self.advance(); - try self.addToken(.interp_start, "#{"); - return; - } - if (next == '[') { - self.advance(); - self.advance(); - try self.addToken(.tag_interp_start, "#["); - return; - } - if (isAlpha(next) or next == '-' or next == '_') { - try self.scanId(); - return; - } - } - - // Unescaped interpolation: !{ - if (c == '!' and self.peekNext() == '{') { - self.advance(); - self.advance(); - try self.addToken(.interp_start_unesc, "!{"); - return; - } - - // Attributes: (...) - if (c == '(') { - try self.scanAttributes(); - return; - } - - // Pipe text: | text - if (c == '|') { - try self.scanPipeText(); - return; - } - - // Literal HTML: lines starting with < - if (c == '<') { - try self.scanLiteralHtml(); - return; - } - - // Buffered output: = expression - if (c == '=') { - self.advance(); - try self.addToken(.buffered_text, "="); - try self.scanInlineText(); - return; - } - - // Unescaped output: != expression - if (c == '!' and self.peekNext() == '=') { - self.advance(); - self.advance(); - try self.addToken(.unescaped_text, "!="); - try self.scanInlineText(); - return; - } - - // Mixin call: +name - if (c == '+') { - try self.scanMixinCall(); - return; - } - - // Unbuffered code: - var x = 1 or -var x = 1 (JS code that doesn't produce output) - // Skip the entire line since we don't execute JS - // Handle both "- var" (with space) and "-var" (no space) formats - if (c == '-') { - const next = self.peekNext(); - // Check if this is unbuffered code: - followed by space, letter, or control keywords - if (next == ' ' or isAlpha(next)) { - try self.scanUnbufferedCode(); - return; - } - } - - // Block expansion: tag: nested - if (c == ':') { - self.advance(); - try self.addToken(.colon, ":"); - return; - } - - // Attribute spread: &attributes(obj) - if (c == '&') { - try self.scanAmpersandAttrs(); - return; - } - - // Interpolation end - if (c == '}') { - self.advance(); - try self.addToken(.interp_end, "}"); - return; - } - - // Tag name or keyword - if (isAlpha(c) or c == '_') { - try self.scanTagOrKeyword(); - return; - } - - // Fallback: treat remaining content as text - try self.scanInlineText(); - } - - /// Processes leading whitespace at line start to emit indent/dedent tokens. - /// Measures indentation at current position and advances past whitespace. - /// Returns the indent level (spaces=1, tabs=2). - fn measureIndent(self: *Lexer) usize { - var indent: usize = 0; - - // Count spaces (1 each) and tabs (2 each) - while (!self.isAtEnd()) { - const c = self.peek(); - if (c == ' ') { - indent += 1; - self.advance(); - } else if (c == '\t') { - indent += 2; - self.advance(); - } else { - break; - } - } - - return indent; - } - - /// Processes leading whitespace at line start to emit indent/dedent tokens. - /// Tracks indentation levels on a stack to handle nested blocks. - fn processIndentation(self: *Lexer) !void { - const indent = self.measureIndent(); - - // Empty lines don't affect indentation - if (!self.isAtEnd() and (self.peek() == '\n' or self.peek() == '\r')) { - return; - } - - self.current_indent = indent; - const current_stack_indent = self.indent_stack.items[self.indent_stack.items.len - 1]; - - if (indent > current_stack_indent) { - // Deeper nesting: push new level and emit indent - try self.indent_stack.append(self.allocator, indent); - try self.addToken(.indent, ""); - } else if (indent < current_stack_indent) { - // Shallower nesting: pop levels and emit dedents - while (self.indent_stack.items.len > 1 and - self.indent_stack.items[self.indent_stack.items.len - 1] > indent) - { - _ = self.indent_stack.pop(); - try self.addToken(.dedent, ""); - - // Exit raw block mode when dedenting to or below original level - if (self.in_raw_block and indent <= self.raw_block_indent) { - self.in_raw_block = false; - } - } + 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}), } } +}; - /// Scans a comment (// or //-) until end of line. - /// Unbuffered comments (//-) are not rendered in output. - /// Sets up comment block mode for any indented content that follows. - fn scanComment(self: *Lexer) !void { - self.advance(); // skip first / - self.advance(); // skip second / +// ============================================================================ +// Location and Token +// ============================================================================ - const is_unbuffered = self.peek() == '-'; - if (is_unbuffered) { - self.advance(); - } +pub const Location = struct { + line: usize, + column: usize, +}; - const start = self.pos; - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - self.advance(); - } +pub const TokenLoc = struct { + start: Location, + end: ?Location = null, + filename: ?[]const u8 = null, +}; - const value = self.source[start..self.pos]; - try self.addToken(if (is_unbuffered) .comment_unbuffered else .comment, value); +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 - // Set up comment block mode - any indented content will be captured as raw text - self.in_comment_block = true; - self.comment_block_indent = self.current_indent; + /// Helper to get val as string + pub fn getVal(self: Token) ?[]const u8 { + return self.val.getString(); } - /// Scans unbuffered code: - var x = 1; or -var x = 1 or -if (condition) { ... } - /// These are JS statements that don't produce output, so we emit a token - /// but the runtime will ignore it. - fn scanUnbufferedCode(self: *Lexer) !void { - self.advance(); // skip - - // Skip optional space after - - if (self.peek() == ' ') { - self.advance(); - } - - const start = self.pos; - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - self.advance(); - } - - const value = self.source[start..self.pos]; - try self.addToken(.unbuffered_code, value); + /// Helper to check if buffer is true + pub fn isBuffered(self: Token) bool { + return self.buffer.getBool() orelse false; } - /// Scans a class selector: .classname - /// After the class, checks for inline text if no more selectors follow. - fn scanClass(self: *Lexer) !void { - self.advance(); // skip . - const start = self.pos; - - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else { - break; - } - } - - try self.addToken(.class, self.source[start..self.pos]); - - // Check for inline text after class (if no more selectors/attrs follow) - try self.tryInlineTextAfterSelector(); + /// Helper to check if must_escape is true + pub fn shouldEscape(self: Token) bool { + return self.must_escape.getBool() orelse true; } - /// Scans an ID selector: #idname - /// After the ID, checks for inline text if no more selectors follow. - fn scanId(self: *Lexer) !void { - self.advance(); // skip # - const start = self.pos; - - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else { - break; - } - } - - try self.addToken(.id, self.source[start..self.pos]); - - // Check for inline text after ID (if no more selectors/attrs follow) - try self.tryInlineTextAfterSelector(); + /// Helper to get mode as string + pub fn getMode(self: Token) ?[]const u8 { + return self.mode.getString(); } - /// Scans attribute list: (name=value, name2=value2, boolean) - /// Also handles mixin arguments: ('value', expr, name=value) - /// Handles quoted strings, expressions, and boolean attributes. - fn scanAttributes(self: *Lexer) !void { - self.advance(); // skip ( - try self.addToken(.lparen, "("); + /// Helper to get args as string + pub fn getArgs(self: Token) ?[]const u8 { + return self.args.getString(); + } - while (!self.isAtEnd() and self.peek() != ')') { - self.skipWhitespaceInAttrs(); - if (self.peek() == ')') break; + /// Helper to get key as string + pub fn getKey(self: Token) ?[]const u8 { + return self.key.getString(); + } - // Comma separator - if (self.peek() == ',') { - self.advance(); - try self.addToken(.comma, ","); - continue; - } + /// Helper to get code as string + pub fn getCode(self: Token) ?[]const u8 { + return self.code.getString(); + } - const c = self.peek(); + /// Helper to get attribute name as string + pub fn getName(self: Token) ?[]const u8 { + return self.name.getString(); + } +}; - // Check for quoted attribute name: '(click)'='play()' or "(click)"="play()" - if (c == '"' or c == '\'') { - // Look ahead to see if this is a quoted attribute name (followed by =) - const quote = c; - var lookahead = self.pos + 1; - while (lookahead < self.source.len and self.source[lookahead] != quote) { - lookahead += 1; - } - if (lookahead < self.source.len) { - lookahead += 1; // skip closing quote - // Skip whitespace - while (lookahead < self.source.len and (self.source[lookahead] == ' ' or self.source[lookahead] == '\t')) { - lookahead += 1; - } - // Check if followed by = (attribute name) or not (bare value) - if (lookahead < self.source.len and (self.source[lookahead] == '=' or - (self.source[lookahead] == '!' and lookahead + 1 < self.source.len and self.source[lookahead + 1] == '='))) - { - // This is a quoted attribute name - self.advance(); // skip opening quote - const name_start = self.pos; - while (!self.isAtEnd() and self.peek() != quote) { - self.advance(); - } - const attr_name = self.source[name_start..self.pos]; - if (self.peek() == quote) self.advance(); // skip closing quote - try self.addToken(.attr_name, attr_name); +// ============================================================================ +// Character Parser State (simplified) - Zig 0.15 style with ArrayListUnmanaged +// ============================================================================ - self.skipWhitespaceInAttrs(); +const BracketType = enum { paren, brace, bracket }; - // Value assignment: = or != - if (self.peek() == '!' and self.peekNext() == '=') { - self.advance(); - self.advance(); - try self.addToken(.attr_eq, "!="); - self.skipWhitespaceInAttrs(); - try self.scanAttrValue(); - } else if (self.peek() == '=') { - self.advance(); - try self.addToken(.attr_eq, "="); - self.skipWhitespaceInAttrs(); - try self.scanAttrValue(); - } - continue; - } - } - // Not followed by =, treat as bare value (mixin argument) - try self.scanAttrValue(); - continue; - } +const CharParserState = struct { + nesting_stack: std.ArrayListUnmanaged(BracketType) = .{}, + in_string: bool = false, + string_char: ?u8 = null, + in_template: bool = false, + escape_next: bool = false, - // Check for bare value (mixin argument): starts with backtick, brace, bracket, or digit - if (c == '`' or c == '{' or c == '[' or isDigit(c)) { - // This is a bare value (mixin argument), not name=value - try self.scanAttrValue(); - continue; - } + pub fn deinit(self: *CharParserState, allocator: Allocator) void { + self.nesting_stack.deinit(allocator); + } - // Check for parenthesized attribute name: (click)='play()' - // This is valid when preceded by comma or at start of attributes - if (c == '(') { - const name_start = self.pos; - self.advance(); // skip ( - var paren_depth: usize = 1; - while (!self.isAtEnd() and paren_depth > 0) { - const ch = self.peek(); - if (ch == '(') { - paren_depth += 1; - } else if (ch == ')') { - paren_depth -= 1; - } - if (paren_depth > 0) self.advance(); - } - if (self.peek() == ')') self.advance(); // skip closing ) - const attr_name = self.source[name_start..self.pos]; - try self.addToken(.attr_name, attr_name); + pub fn isNesting(self: *const CharParserState) bool { + return self.nesting_stack.items.len > 0; + } - self.skipWhitespaceInAttrs(); + pub fn isString(self: *const CharParserState) bool { + return self.in_string or self.in_template; + } - // Value assignment: = or != - if (self.peek() == '!' and self.peekNext() == '=') { - self.advance(); - self.advance(); - try self.addToken(.attr_eq, "!="); - self.skipWhitespaceInAttrs(); - try self.scanAttrValue(); - } else if (self.peek() == '=') { - self.advance(); - try self.addToken(.attr_eq, "="); - self.skipWhitespaceInAttrs(); - try self.scanAttrValue(); - } - continue; - } + pub fn getStringChar(self: *const CharParserState) ?u8 { + if (self.in_string) return self.string_char; + if (self.in_template) return '`'; + return null; + } - // Check for rest parameter: ...name - const name_start = self.pos; - if (c == '.' and self.peekAt(1) == '.' and self.peekAt(2) == '.') { - // Skip the three dots, include them in attr_name - self.advance(); - self.advance(); - self.advance(); - } - - // Attribute name (supports data-*, @event, :bind) - while (!self.isAtEnd()) { - const ch = self.peek(); - if (isAlphaNumeric(ch) or ch == '-' or ch == '_' or ch == ':' or ch == '@') { - self.advance(); - } else { - break; - } - } - - if (self.pos > name_start) { - try self.addToken(.attr_name, self.source[name_start..self.pos]); - } else { - // No attribute name found - skip unknown character to prevent infinite loop - // This can happen with operators like + in expressions - self.advance(); - continue; - } - - self.skipWhitespaceInAttrs(); - - // Value assignment: = or != - if (self.peek() == '!' and self.peekNext() == '=') { - self.advance(); - self.advance(); - try self.addToken(.attr_eq, "!="); - self.skipWhitespaceInAttrs(); - try self.scanAttrValue(); - } else if (self.peek() == '=') { - self.advance(); - try self.addToken(.attr_eq, "="); - self.skipWhitespaceInAttrs(); - try self.scanAttrValue(); - } - // No = means boolean attribute (e.g., checked, disabled) + pub fn parseChar(self: *CharParserState, allocator: Allocator, char: u8) !void { + if (self.escape_next) { + self.escape_next = false; + return; } - if (self.peek() == ')') { - self.advance(); - try self.addToken(.rparen, ")"); + if (char == '\\') { + self.escape_next = true; + return; + } - // Check for inline text after attributes: a(href='...') Click me - if (self.peek() == ' ') { - const next = self.peekAt(1); - // Don't consume if followed by selector, attr, or special syntax - if (next != '.' and next != '#' and next != '(' and next != '=' and next != ':' and - next != '\n' and next != '\r' and next != 0) + 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.advance(); // skip space - try self.scanInlineText(); - } - } - } - } - - /// Scans an attribute value: "string", 'string', `template`, {object}, or expression. - /// Handles expression continuation with operators like + for string concatenation. - /// Emits a single token for the entire expression (e.g., "btn btn-" + type). - fn scanAttrValue(self: *Lexer) !void { - const start = self.pos; - var after_operator = false; // Track if we just passed an operator - - // Scan the complete expression including operators - while (!self.isAtEnd()) { - const c = self.peek(); - - if (c == '"' or c == '\'') { - // Quoted string - const quote = c; - self.advance(); - while (!self.isAtEnd() and self.peek() != quote) { - if (self.peek() == '\\' and self.peekNext() == quote) { - self.advance(); // skip backslash - } - self.advance(); - } - if (self.peek() == quote) self.advance(); - after_operator = false; - } else if (c == '`') { - // Template literal - self.advance(); - while (!self.isAtEnd() and self.peek() != '`') { - self.advance(); - } - if (self.peek() == '`') self.advance(); - after_operator = false; - } else if (c == '{') { - // Object literal - scan matching braces - var depth: usize = 0; - while (!self.isAtEnd()) { - const ch = self.peek(); - if (ch == '{') depth += 1; - if (ch == '}') { - depth -= 1; - self.advance(); - if (depth == 0) break; - continue; - } - self.advance(); - } - after_operator = false; - } else if (c == '[') { - // Array literal - scan matching brackets - var depth: usize = 0; - while (!self.isAtEnd()) { - const ch = self.peek(); - if (ch == '[') depth += 1; - if (ch == ']') { - depth -= 1; - self.advance(); - if (depth == 0) break; - continue; - } - self.advance(); - } - after_operator = false; - } else if (c == '(') { - // Function call - scan matching parens - var depth: usize = 0; - while (!self.isAtEnd()) { - const ch = self.peek(); - if (ch == '(') depth += 1; - if (ch == ')') { - depth -= 1; - self.advance(); - if (depth == 0) break; - continue; - } - self.advance(); - } - after_operator = false; - } else if (c == ')' or c == ',') { - // End of attribute value - break; - } else if (c == ' ' or c == '\t') { - // Whitespace handling depends on context - if (after_operator) { - // After an operator, skip whitespace and continue to get the operand - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - after_operator = false; - continue; - } else { - // Not after operator - check if followed by operator (continue) or not (end) - const ws_start = self.pos; - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - const next = self.peek(); - if (next == '+' or next == '-' or next == '*' or next == '/') { - // Operator follows - continue scanning (include whitespace) - continue; - } else { - // Not an operator - rewind and end - self.pos = ws_start; - break; - } - } - } else if (c == '+' or c == '-' or c == '*' or c == '/') { - // Operator - include it and mark that we need to continue for the operand - self.advance(); - after_operator = true; - } else if (c == '\n' or c == '\r') { - // Newline ends the value - break; - } else { - // Regular character (alphanumeric, etc.) - self.advance(); - after_operator = false; - } - } - - const value = std.mem.trim(u8, self.source[start..self.pos], " \t"); - if (value.len > 0) { - try self.addToken(.attr_value, value); - } - } - - /// Scans an object literal {...} handling nested braces. - /// Returns error if braces are unmatched. - fn scanObjectLiteral(self: *Lexer) !void { - const start = self.pos; - var brace_depth: usize = 0; - - while (!self.isAtEnd()) { - const c = self.peek(); - if (c == '{') { - brace_depth += 1; - } else if (c == '}') { - if (brace_depth == 0) { - self.setDiagnostic( - "Unmatched closing brace '}'", - "Remove the extra '}' or add a matching '{'", - ); - return LexerError.UnmatchedBrace; - } - brace_depth -= 1; - if (brace_depth == 0) { - self.advance(); - break; - } - } - self.advance(); - } - - // Check for unterminated object literal - if (brace_depth > 0) { - self.setDiagnostic( - "Unterminated object literal - missing closing '}'", - "Add a closing '}' to complete the object", - ); - return LexerError.UnterminatedString; - } - - try self.addToken(.attr_value, self.source[start..self.pos]); - } - - /// Scans an array literal [...] handling nested brackets. - fn scanArrayLiteral(self: *Lexer) !void { - const start = self.pos; - var bracket_depth: usize = 0; - - while (!self.isAtEnd()) { - const c = self.peek(); - if (c == '[') { - bracket_depth += 1; - } else if (c == ']') { - if (bracket_depth == 0) { - self.setDiagnostic( - "Unmatched closing bracket ']'", - "Remove the extra ']' or add a matching '['", - ); - return LexerError.UnmatchedBrace; - } - bracket_depth -= 1; - if (bracket_depth == 0) { - self.advance(); - break; - } - } - self.advance(); - } - - if (bracket_depth > 0) { - self.setDiagnostic( - "Unterminated array literal - missing closing ']'", - "Add a closing ']' to complete the array", - ); - return LexerError.UnterminatedString; - } - - try self.addToken(.attr_value, self.source[start..self.pos]); - } - - /// Skips whitespace within attribute lists (allows multi-line attributes). - /// Properly tracks line and column for error reporting. - fn skipWhitespaceInAttrs(self: *Lexer) void { - while (!self.isAtEnd()) { - const c = self.peek(); - switch (c) { - ' ', '\t' => self.advance(), - '\n' => { - self.pos += 1; - self.line += 1; - self.column = 1; - }, - '\r' => { - self.pos += 1; - if (!self.isAtEnd() and self.source[self.pos] == '\n') { - self.pos += 1; - } - self.line += 1; - self.column = 1; - }, - else => break, - } - } - } - - /// Scans pipe text: | followed by text content. - fn scanPipeText(self: *Lexer) !void { - self.advance(); // skip | - if (self.peek() == ' ') self.advance(); // skip optional space - - try self.addToken(.pipe_text, "|"); - try self.scanInlineText(); - } - - /// Scans literal HTML: lines starting with < are passed through as-is. - fn scanLiteralHtml(self: *Lexer) !void { - const start = self.pos; - - // Scan until end of line - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - self.advance(); - } - - const html = self.source[start..self.pos]; - try self.addToken(.literal_html, html); - } - - /// Scans a raw line of text (used inside dot blocks). - /// Captures everything until end of line as a single text token. - /// Preserves indentation relative to the base raw block indent. - /// Takes line_start position to include proper indentation from source. - fn scanRawLineFrom(self: *Lexer, line_start: usize) !void { - // Scan until end of line - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - self.advance(); - } - - // Include all content from line_start, preserving the indentation from source - if (self.pos > line_start) { - const text = self.source[line_start..self.pos]; - try self.addToken(.text, text); - } - } - - /// Scans a raw line for comment blocks, stripping base indentation. - /// Preserves relative indentation beyond the base comment indent. - fn scanCommentRawLine(self: *Lexer, current_indent: usize) !void { - var result = std.ArrayList(u8).empty; - errdefer result.deinit(self.allocator); - - // Add relative indentation (indent beyond the base) - if (current_indent > self.comment_base_indent) { - const relative_indent = current_indent - self.comment_base_indent; - for (0..relative_indent) |_| { - try result.append(self.allocator, ' '); - } - } - - // Scan the rest of the line content - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - try result.append(self.allocator, self.peek()); - self.advance(); - } - - if (result.items.len > 0) { - try self.addToken(.text, try result.toOwnedSlice(self.allocator)); - } - } - - /// Scans inline text until end of line, handling interpolation markers. - /// Uses iterative approach instead of recursion to avoid stack overflow. - fn scanInlineText(self: *Lexer) !void { - if (self.peek() == ' ') self.advance(); // skip leading space - - outer: while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - const start = self.pos; - - // Scan until interpolation or end of line - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - const c = self.peek(); - const next = self.peekNext(); - - // Handle escaped interpolation: \#{ or \!{ or \#[ - // The backslash escapes the interpolation, treating #{ as literal text - if (c == '\\' and (next == '#' or next == '!')) { - const after_next = self.peekAt(2); - if (after_next == '{' or (next == '#' and after_next == '[')) { - // Emit text before backslash (if any) - if (self.pos > start) { - try self.addToken(.text, self.source[start..self.pos]); - } - self.advance(); // skip backslash - // Now emit the escaped sequence as literal text - // For \#{ we want to output "#{" literally - const esc_start = self.pos; - self.advance(); // include # or ! - self.advance(); // include { or [ - // For \#{text} we want #{text} as literal, so include until } - if (after_next == '{') { - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r' and self.peek() != '}') { - self.advance(); - } - if (self.peek() == '}') self.advance(); // include } - } else if (after_next == '[') { - while (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r' and self.peek() != ']') { - self.advance(); - } - if (self.peek() == ']') self.advance(); // include ] - } - try self.addToken(.text, self.source[esc_start..self.pos]); - // Continue outer loop to process rest of line - continue :outer; - } - } - - // Check for interpolation start: #{, !{, or #[ - if ((c == '#' or c == '!') and next == '{') { - break; - } - if (c == '#' and next == '[') { - break; - } - self.advance(); - } - - // Emit text before interpolation (if any) - if (self.pos > start) { - try self.addToken(.text, self.source[start..self.pos]); - } - - // Handle interpolation if found - if (!self.isAtEnd() and self.peek() != '\n' and self.peek() != '\r') { - const c = self.peek(); - if (c == '#' and self.peekNext() == '{') { - self.advance(); - self.advance(); - try self.addToken(.interp_start, "#{"); - try self.scanInterpolationContent(); - } else if (c == '!' and self.peekNext() == '{') { - self.advance(); - self.advance(); - try self.addToken(.interp_start_unesc, "!{"); - try self.scanInterpolationContent(); - } else if (c == '#' and self.peekNext() == '[') { - self.advance(); - self.advance(); - try self.addToken(.tag_interp_start, "#["); - try self.scanTagInterpolation(); - } - } - } - } - - /// Scans tag interpolation content: #[tag(attrs) text] - /// This needs to handle the tag, optional attributes, optional text, and closing ] - fn scanTagInterpolation(self: *Lexer) !void { - // Skip whitespace - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - - // Scan tag name - if (isAlpha(self.peek()) or self.peek() == '_') { - const tag_start = self.pos; - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else { - break; - } - } - try self.addToken(.tag, self.source[tag_start..self.pos]); - } - - // Scan classes and ids (inline to avoid circular dependencies) - while (self.peek() == '.' or self.peek() == '#') { - if (self.peek() == '.') { - // Inline class scanning - self.advance(); // skip . - const class_start = self.pos; - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else { - break; - } - } - try self.addToken(.class, self.source[class_start..self.pos]); - } else if (self.peek() == '#' and self.peekNext() != '[' and self.peekNext() != '{') { - // Inline id scanning - self.advance(); // skip # - const id_start = self.pos; - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else { - break; - } - } - try self.addToken(.id, self.source[id_start..self.pos]); - } else { - break; - } - } - - // Scan attributes if present (inline to avoid circular dependencies) - if (self.peek() == '(') { - self.advance(); // skip ( - try self.addToken(.lparen, "("); - - while (!self.isAtEnd() and self.peek() != ')') { - // Skip whitespace - while (self.peek() == ' ' or self.peek() == '\t' or self.peek() == '\n' or self.peek() == '\r') { - if (self.peek() == '\n' or self.peek() == '\r') { - self.line += 1; - self.column = 1; - } - self.advance(); - } - if (self.peek() == ')') break; - - // Comma separator - if (self.peek() == ',') { - self.advance(); - try self.addToken(.comma, ","); - continue; - } - - // Attribute name - const name_start = self.pos; - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_' or c == ':' or c == '@') { - self.advance(); - } else { - break; - } - } - if (self.pos > name_start) { - try self.addToken(.attr_name, self.source[name_start..self.pos]); - } - - // Skip whitespace - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - - // Value assignment - if (self.peek() == '!' and self.peekNext() == '=') { - self.advance(); - self.advance(); - try self.addToken(.attr_eq, "!="); - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - try self.scanAttrValue(); - } else if (self.peek() == '=') { - self.advance(); - try self.addToken(.attr_eq, "="); - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - try self.scanAttrValue(); - } - } - - if (self.peek() == ')') { - self.advance(); - try self.addToken(.rparen, ")"); - } - } - - // Skip whitespace before text content - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - - // Scan text content until ] (handling nested #[ ]) - if (self.peek() != ']') { - const text_start = self.pos; - var bracket_depth: usize = 1; - - while (!self.isAtEnd() and bracket_depth > 0) { - const c = self.peek(); - if (c == '#' and self.peekNext() == '[') { - bracket_depth += 1; - self.advance(); - } else if (c == ']') { - bracket_depth -= 1; - if (bracket_depth == 0) break; - } else if (c == '\n' or c == '\r') { - break; - } - self.advance(); - } - - if (self.pos > text_start) { - try self.addToken(.text, self.source[text_start..self.pos]); - } - } - - // Emit closing ] - if (self.peek() == ']') { - self.advance(); - try self.addToken(.tag_interp_end, "]"); - } - } - - /// Scans interpolation content between { and }, handling nested braces. - fn scanInterpolationContent(self: *Lexer) !void { - const start = self.pos; - var brace_depth: usize = 1; - - while (!self.isAtEnd() and brace_depth > 0) { - const c = self.peek(); - if (c == '{') { - brace_depth += 1; - } else if (c == '}') { - brace_depth -= 1; - if (brace_depth == 0) break; - } - self.advance(); - } - - try self.addToken(.text, self.source[start..self.pos]); - - if (!self.isAtEnd() and self.peek() == '}') { - self.advance(); - try self.addToken(.interp_end, "}"); - } - } - - /// Scans a mixin call: +mixinName - fn scanMixinCall(self: *Lexer) !void { - self.advance(); // skip + - const start = self.pos; - - while (!self.isAtEnd()) { - const c = self.peek(); - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else { - break; - } - } - - try self.addToken(.mixin_call, self.source[start..self.pos]); - } - - /// Scans &attributes syntax for attribute spreading. - fn scanAmpersandAttrs(self: *Lexer) !void { - const start = self.pos; - const remaining = self.source.len - self.pos; - - if (remaining >= 11 and std.mem.eql(u8, self.source[self.pos..][0..11], "&attributes")) { - self.pos += 11; - self.column += 11; - try self.addToken(.ampersand_attrs, "&attributes"); - - // Parse the (...) that follows &attributes - if (self.peek() == '(') { - self.advance(); // skip ( - const obj_start = self.pos; - var paren_depth: usize = 1; - - while (!self.isAtEnd() and paren_depth > 0) { - const c = self.peek(); - if (c == '(') { - paren_depth += 1; - } else if (c == ')') { - paren_depth -= 1; - } - if (paren_depth > 0) self.advance(); - } - - try self.addToken(.attr_value, self.source[obj_start..self.pos]); - if (self.peek() == ')') self.advance(); // skip ) - } - } else { - // Lone & treated as text - self.advance(); - try self.addToken(.text, self.source[start..self.pos]); - } - } - - /// Checks if inline text follows after a class/ID selector. - /// Only scans inline text if the next char is space followed by non-selector content. - fn tryInlineTextAfterSelector(self: *Lexer) !void { - if (self.peek() != ' ') return; - - const next = self.peekAt(1); - const next2 = self.peekAt(2); - - // Don't consume if followed by another selector, attribute, or special syntax - // BUT: #{...} and #[...] are interpolation, not ID selectors - const is_id_selector = next == '#' and next2 != '{' and next2 != '['; - if (next == '.' or is_id_selector or next == '(' or next == '=' or next == ':' or - next == '\n' or next == '\r' or next == 0) - { - return; - } - - self.advance(); // skip space - try self.scanInlineText(); - } - - /// Scans a tag name or keyword, then optionally inline text. - /// Uses static map for O(1) keyword lookup. - fn scanTagOrKeyword(self: *Lexer) !void { - const start = self.pos; - - while (!self.isAtEnd()) { - const c = self.peek(); - // Include colon for namespaced tags like fb:user:role - // But only if followed by alphanumeric (not for block expansion like tag: child) - if (isAlphaNumeric(c) or c == '-' or c == '_') { - self.advance(); - } else if (c == ':' and isAlpha(self.peekNext())) { - // Colon followed by letter is part of namespace, not block expansion - self.advance(); - } else { - break; - } - } - - const value = self.source[start..self.pos]; - - // O(1) keyword lookup using static map - const token_type = keywords.get(value) orelse .tag; - - try self.addToken(token_type, value); - - // Keywords that take expressions: scan rest of line as text - // This allows `if user.description` to keep the dot notation intact - switch (token_type) { - .kw_if, .kw_unless, .kw_each, .kw_for, .kw_while, .kw_case, .kw_when, .kw_doctype, .kw_extends, .kw_include => { - // Skip whitespace after keyword - while (self.peek() == ' ' or self.peek() == '\t') { - self.advance(); - } - // Scan rest of line as expression/path text - if (!self.isAtEnd() and self.peek() != '\n') { - try self.scanExpressionText(); + _ = self.nesting_stack.pop(); } }, - .tag => { - // Tags may have inline text: p Hello world - if (self.peek() == ' ') { - const next = self.peekAt(1); - const next2 = self.peekAt(2); - // Don't consume text if followed by selector/attr syntax - // Note: # followed by { or [ is interpolation, not ID selector - // Note: . followed by alphanumeric is class selector, but lone . is text - const is_id_selector = next == '#' and next2 != '{' and next2 != '['; - const is_class_selector = next == '.' and (isAlpha(next2) or next2 == '-' or next2 == '_'); - if (!is_class_selector and !is_id_selector and next != '(' and next != '=' and next != ':') { - self.advance(); - try self.scanInlineText(); - } + '}' => { + 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 => {}, } } +}; - /// Scans expression text (rest of line) preserving dots and other chars. - fn scanExpressionText(self: *Lexer) !void { - const start = self.pos; +// ============================================================================ +// Lexer Error +// ============================================================================ - // Scan until end of line - while (!self.isAtEnd() and self.peek() != '\n') { - self.advance(); +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..]; } - const text = self.source[start..self.pos]; - if (text.len > 0) { - try self.addToken(.text, text); + // 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); } } - // ───────────────────────────────────────────────────────────────────────── - // Helper functions for character inspection and position management - // ───────────────────────────────────────────────────────────────────────── + // ======================================================================== + // Error handling + // ======================================================================== - /// Returns true if at end of source. - inline fn isAtEnd(self: *const Lexer) bool { - return self.pos >= self.source.len; + 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, + }; } - /// Returns current character or 0 if at end. - inline fn peek(self: *const Lexer) u8 { - if (self.pos >= self.source.len) return 0; - return self.source[self.pos]; + /// 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; } - /// Returns next character or 0 if at/past end. - inline fn peekNext(self: *const Lexer) u8 { - if (self.pos + 1 >= self.source.len) return 0; - return self.source[self.pos + 1]; + /// 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; } - /// Returns character at pos + offset or 0 if out of bounds. - inline fn peekAt(self: *const Lexer, offset: usize) u8 { - const target = self.pos + offset; - if (target >= self.source.len) return 0; - return self.source[target]; + // ======================================================================== + // 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, + }, + }; } - /// Advances position and column by one. - inline fn advance(self: *Lexer) void { - if (self.pos < self.source.len) { - self.pos += 1; - self.column += 1; + 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; + } }; -// ───────────────────────────────────────────────────────────────────────────── -// Character classification utilities (inlined for performance) -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ +// Options +// ============================================================================ -inline fn isAlpha(c: u8) bool { - return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z'); +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 }; } -inline fn isDigit(c: u8) bool { - return c >= '0' and c <= '9'; +/// Frees resources from a lex() call +pub fn freeLexResult(allocator: Allocator, lexer: *Lexer) void { + lexer.deinit(); + allocator.destroy(lexer); } -inline fn isAlphaNumeric(c: u8) bool { - return isAlpha(c) or isDigit(c); -} - -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ // Tests -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ -test "tokenize simple tag" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "div"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); - try std.testing.expectEqual(@as(usize, 2), tokens.len); - try std.testing.expectEqual(TokenType.tag, tokens[0].type); - try std.testing.expectEqualStrings("div", tokens[0].value); +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 "tokenize tag with class" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "div.container"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); - 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].value); +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 "tokenize tag with id" { +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 = Lexer.init(allocator, "div#main"); + var lexer = try Lexer.init(allocator, "div", .{}); defer lexer.deinit(); - const tokens = try lexer.tokenize(); + 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].value); + try std.testing.expectEqualStrings("main", tokens[1].getVal().?); } -test "tokenize nested tags" { +test "tag with class" { const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, - \\div - \\ p Hello - ); + var lexer = try Lexer.init(allocator, "div.container", .{}); defer lexer.deinit(); - const tokens = try lexer.tokenize(); - - var found_indent = false; - var found_dedent = false; - for (tokens) |token| { - if (token.type == .indent) found_indent = true; - if (token.type == .dedent) found_dedent = true; - } - try std.testing.expect(found_indent); - try std.testing.expect(found_dedent); -} - -test "tokenize attributes" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "a(href=\"/link\" target=\"_blank\")"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); + 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.lparen, tokens[1].type); - try std.testing.expectEqual(TokenType.attr_name, tokens[2].type); - try std.testing.expectEqualStrings("href", tokens[2].value); - try std.testing.expectEqual(TokenType.attr_eq, tokens[3].type); - try std.testing.expectEqual(TokenType.attr_value, tokens[4].type); - // Quotes are preserved in token value for expression evaluation - try std.testing.expectEqualStrings("\"/link\"", tokens[4].value); + try std.testing.expectEqual(TokenType.class, tokens[1].type); + try std.testing.expectEqualStrings("container", tokens[1].getVal().?); } -test "tokenize interpolation" { +test "doctype" { const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "p Hello #{name}!"); + var lexer = try Lexer.init(allocator, "doctype html", .{}); defer lexer.deinit(); - const tokens = try lexer.tokenize(); + const tokens = try lexer.getTokens(); - var found_interp_start = false; - var found_interp_end = false; - for (tokens) |token| { - if (token.type == .interp_start) found_interp_start = true; - if (token.type == .interp_end) found_interp_end = true; - } - try std.testing.expect(found_interp_start); - try std.testing.expect(found_interp_end); + 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 "tokenize multiple interpolations" { +test "comment with buffer" { const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "p #{a} and #{b} and #{c}"); + var lexer = try Lexer.init(allocator, "// this is a comment", .{}); defer lexer.deinit(); - const tokens = try lexer.tokenize(); + const tokens = try lexer.getTokens(); - var interp_count: usize = 0; - for (tokens) |token| { - if (token.type == .interp_start) interp_count += 1; - } - try std.testing.expectEqual(@as(usize, 3), interp_count); -} - -test "tokenize if keyword" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "if condition"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); - try std.testing.expectEqual(TokenType.kw_if, tokens[0].type); -} - -test "tokenize each keyword" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "each item in items"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); - try std.testing.expectEqual(TokenType.kw_each, tokens[0].type); - // Rest of line is captured as text for parser to handle - try std.testing.expectEqual(TokenType.text, tokens[1].type); - try std.testing.expectEqualStrings("item in items", tokens[1].value); -} - -test "tokenize mixin call" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "+button"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); - try std.testing.expectEqual(TokenType.mixin_call, tokens[0].type); - try std.testing.expectEqualStrings("button", tokens[0].value); -} - -test "tokenize comment" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "// This is a comment"); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); + 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 "tokenize unbuffered comment" { +test "comment without buffer" { const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "//- Hidden comment"); + var lexer = try Lexer.init(allocator, "//- this is a silent comment", .{}); defer lexer.deinit(); - const tokens = try lexer.tokenize(); - try std.testing.expectEqual(TokenType.comment_unbuffered, tokens[0].type); + 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 "tokenize object literal in attributes" { +test "code with escape" { const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, "div(style={color: 'red', nested: {a: 1}})"); + var lexer = try Lexer.init(allocator, "= foo", .{}); defer lexer.deinit(); - const tokens = try lexer.tokenize(); + const tokens = try lexer.getTokens(); - // Find the attr_value token with object literal - var found_object = false; - for (tokens) |token| { - if (token.type == .attr_value and token.value.len > 0 and token.value[0] == '{') { - found_object = true; + 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(found_object); -} - -test "tokenize dot block" { - const allocator = std.testing.allocator; - var lexer = Lexer.init(allocator, - \\script. - \\ if (usingPug) - \\ console.log('hi') - ); - defer lexer.deinit(); - - const tokens = try lexer.tokenize(); - - var found_dot_block = false; - var text_count: usize = 0; - for (tokens) |token| { - if (token.type == .dot_block) found_dot_block = true; - if (token.type == .text) text_count += 1; - } - try std.testing.expect(found_dot_block); - try std.testing.expectEqual(@as(usize, 2), text_count); + try std.testing.expect(attr_found); } diff --git a/src/linker.zig b/src/linker.zig new file mode 100644 index 0000000..969d344 --- /dev/null +++ b/src/linker.zig @@ -0,0 +1,699 @@ +// 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/load.zig b/src/load.zig new file mode 100644 index 0000000..48cd449 --- /dev/null +++ b/src/load.zig @@ -0,0 +1,412 @@ +// 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/mixin.zig b/src/mixin.zig new file mode 100644 index 0000000..342d285 --- /dev/null +++ b/src/mixin.zig @@ -0,0 +1,581 @@ +// mixin.zig - Mixin registry and expansion +// +// Handles mixin definitions and calls: +// - Collects mixin definitions from AST into a registry +// - Expands mixin calls by substituting arguments and block content +// +// Usage pattern in Pug: +// mixin button(text, type) +// button(class="btn btn-" + type)= text +// +// +button("Click", "primary") +// +// Include pattern: +// include mixins/_buttons.pug +// +primary-button("Click") + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const mem = std.mem; + +const parser = @import("parser.zig"); +pub const Node = parser.Node; +pub const NodeType = parser.NodeType; + +// ============================================================================ +// Mixin Registry +// ============================================================================ + +/// Registry for mixin definitions +pub const MixinRegistry = struct { + allocator: Allocator, + mixins: std.StringHashMapUnmanaged(*Node), + + pub fn init(allocator: Allocator) MixinRegistry { + return .{ + .allocator = allocator, + .mixins = .{}, + }; + } + + pub fn deinit(self: *MixinRegistry) void { + self.mixins.deinit(self.allocator); + } + + /// Register a mixin definition + pub fn register(self: *MixinRegistry, name: []const u8, node: *Node) !void { + try self.mixins.put(self.allocator, name, node); + } + + /// Get a mixin definition by name + pub fn get(self: *const MixinRegistry, name: []const u8) ?*Node { + return self.mixins.get(name); + } + + /// Check if a mixin exists + pub fn contains(self: *const MixinRegistry, name: []const u8) bool { + return self.mixins.contains(name); + } +}; + +// ============================================================================ +// Mixin Collector - Collect definitions from AST +// ============================================================================ + +/// Collect all mixin definitions from an AST into the registry +pub fn collectMixins(allocator: Allocator, ast: *Node, registry: *MixinRegistry) !void { + try collectMixinsFromNode(allocator, ast, registry); +} + +fn collectMixinsFromNode(allocator: Allocator, node: *Node, registry: *MixinRegistry) !void { + // If this is a mixin definition (not a call), register it + if (node.type == .Mixin and !node.call) { + if (node.name) |name| { + try registry.register(name, node); + } + } + + // Recurse into children + for (node.nodes.items) |child| { + try collectMixinsFromNode(allocator, child, registry); + } +} + +// ============================================================================ +// Mixin Expander - Expand mixin calls in AST +// ============================================================================ + +/// Error types for mixin expansion +pub const MixinError = error{ + OutOfMemory, + MixinNotFound, + InvalidMixinCall, +}; + +/// Expand all mixin calls in an AST using the registry +/// Returns a new AST with mixin calls replaced by their expanded content +pub fn expandMixins(allocator: Allocator, ast: *Node, registry: *const MixinRegistry) MixinError!*Node { + return expandNode(allocator, ast, registry, null); +} + +fn expandNode( + allocator: Allocator, + node: *Node, + registry: *const MixinRegistry, + caller_block: ?*Node, +) MixinError!*Node { + // Handle mixin call + if (node.type == .Mixin and node.call) { + return expandMixinCall(allocator, node, registry, caller_block); + } + + // Handle MixinBlock - replace with caller's block content + if (node.type == .MixinBlock) { + if (caller_block) |block| { + // Clone the caller's block + return cloneNode(allocator, block); + } else { + // No block provided, return empty block + const empty = allocator.create(Node) catch return error.OutOfMemory; + empty.* = Node{ + .type = .Block, + .line = node.line, + .column = node.column, + }; + return empty; + } + } + + // For other nodes, clone and recurse into children + const new_node = allocator.create(Node) catch return error.OutOfMemory; + new_node.* = node.*; + new_node.nodes = .{}; + + // Clone and expand children + for (node.nodes.items) |child| { + const expanded_child = try expandNode(allocator, child, registry, caller_block); + new_node.nodes.append(allocator, expanded_child) catch return error.OutOfMemory; + } + + return new_node; +} + +fn expandMixinCall( + allocator: Allocator, + call_node: *Node, + registry: *const MixinRegistry, + _: ?*Node, +) MixinError!*Node { + const mixin_name = call_node.name orelse return error.InvalidMixinCall; + + // Look up mixin definition + const mixin_def = registry.get(mixin_name) orelse { + // Mixin not found - return a comment node indicating the error + const error_node = allocator.create(Node) catch return error.OutOfMemory; + error_node.* = Node{ + .type = .Comment, + .val = mixin_name, + .buffer = true, + .line = call_node.line, + .column = call_node.column, + }; + return error_node; + }; + + // Get the block content from the call (if any) + var call_block: ?*Node = null; + if (call_node.nodes.items.len > 0) { + // Create a block node containing the call's children + const block = allocator.create(Node) catch return error.OutOfMemory; + block.* = Node{ + .type = .Block, + .line = call_node.line, + .column = call_node.column, + }; + for (call_node.nodes.items) |child| { + const cloned = try cloneNode(allocator, child); + block.nodes.append(allocator, cloned) catch return error.OutOfMemory; + } + call_block = block; + } + + // Create argument bindings + var arg_bindings = std.StringHashMapUnmanaged([]const u8){}; + defer arg_bindings.deinit(allocator); + + // Bind call arguments to mixin parameters + if (mixin_def.args) |params| { + if (call_node.args) |args| { + try bindArguments(allocator, params, args, &arg_bindings); + } + } + + // Clone and expand the mixin body + const result = allocator.create(Node) catch return error.OutOfMemory; + result.* = Node{ + .type = .Block, + .line = call_node.line, + .column = call_node.column, + }; + + // Expand each node in the mixin definition's body + for (mixin_def.nodes.items) |child| { + const expanded = try expandNodeWithArgs(allocator, child, registry, call_block, &arg_bindings); + result.nodes.append(allocator, expanded) catch return error.OutOfMemory; + } + + return result; +} + +fn expandNodeWithArgs( + allocator: Allocator, + node: *Node, + registry: *const MixinRegistry, + caller_block: ?*Node, + arg_bindings: *const std.StringHashMapUnmanaged([]const u8), +) MixinError!*Node { + // Handle mixin call (nested) + if (node.type == .Mixin and node.call) { + return expandMixinCall(allocator, node, registry, caller_block); + } + + // Handle MixinBlock - replace with caller's block content + if (node.type == .MixinBlock) { + if (caller_block) |block| { + return cloneNode(allocator, block); + } else { + const empty = allocator.create(Node) catch return error.OutOfMemory; + empty.* = Node{ + .type = .Block, + .line = node.line, + .column = node.column, + }; + return empty; + } + } + + // Clone the node + const new_node = allocator.create(Node) catch return error.OutOfMemory; + new_node.* = node.*; + new_node.nodes = .{}; + new_node.attrs = .{}; + + // Substitute argument references in text/val + if (node.val) |val| { + new_node.val = try substituteArgs(allocator, val, arg_bindings); + } + + // Clone attributes with argument substitution + for (node.attrs.items) |attr| { + var new_attr = attr; + if (attr.val) |val| { + new_attr.val = try substituteArgs(allocator, val, arg_bindings); + } + new_node.attrs.append(allocator, new_attr) catch return error.OutOfMemory; + } + + // Recurse into children + for (node.nodes.items) |child| { + const expanded = try expandNodeWithArgs(allocator, child, registry, caller_block, arg_bindings); + new_node.nodes.append(allocator, expanded) catch return error.OutOfMemory; + } + + return new_node; +} + +/// Substitute argument references in a string and evaluate simple expressions +fn substituteArgs( + allocator: Allocator, + text: []const u8, + bindings: *const std.StringHashMapUnmanaged([]const u8), +) MixinError![]const u8 { + // Quick check - if no bindings or text doesn't contain any param names, return as-is + if (bindings.count() == 0) { + return text; + } + + // Check if any substitution is needed + var needs_substitution = false; + var iter = bindings.iterator(); + while (iter.next()) |entry| { + if (mem.indexOf(u8, text, entry.key_ptr.*) != null) { + needs_substitution = true; + break; + } + } + + if (!needs_substitution) { + return text; + } + + // Perform substitution + var result = std.ArrayListUnmanaged(u8){}; + errdefer result.deinit(allocator); + + var i: usize = 0; + while (i < text.len) { + var found_match = false; + + // Check for parameter match at current position + var iter2 = bindings.iterator(); + while (iter2.next()) |entry| { + const param = entry.key_ptr.*; + const value = entry.value_ptr.*; + + if (i + param.len <= text.len and mem.eql(u8, text[i .. i + param.len], param)) { + // Check it's a word boundary (not part of a larger identifier) + const before_ok = i == 0 or !isIdentChar(text[i - 1]); + const after_ok = i + param.len >= text.len or !isIdentChar(text[i + param.len]); + + if (before_ok and after_ok) { + result.appendSlice(allocator, value) catch return error.OutOfMemory; + i += param.len; + found_match = true; + break; + } + } + } + + if (!found_match) { + result.append(allocator, text[i]) catch return error.OutOfMemory; + i += 1; + } + } + + const substituted = result.toOwnedSlice(allocator) catch return error.OutOfMemory; + + // Evaluate string concatenation expressions like "btn btn-" + "primary" + return evaluateStringConcat(allocator, substituted) catch return error.OutOfMemory; +} + +/// Evaluate simple string concatenation expressions +/// Handles: "btn btn-" + primary -> "btn btn-primary" +/// Also handles: "btn btn-" + "primary" -> "btn btn-primary" +fn evaluateStringConcat(allocator: Allocator, expr: []const u8) ![]const u8 { + // Check if there's a + operator (string concat) + _ = mem.indexOf(u8, expr, " + ") orelse return expr; + + var result = std.ArrayListUnmanaged(u8){}; + errdefer result.deinit(allocator); + + var remaining = expr; + var is_first_part = true; + + while (remaining.len > 0) { + const next_plus = mem.indexOf(u8, remaining, " + "); + const part = if (next_plus) |pos| remaining[0..pos] else remaining; + + // Extract string value (strip quotes and whitespace) + const stripped = mem.trim(u8, part, " \t"); + const unquoted = stripQuotes(stripped); + + // For the first part, we might want to keep it quoted in the final output + // For subsequent parts, just append the value + if (is_first_part) { + // If the first part is a quoted string, we'll build an unquoted result + result.appendSlice(allocator, unquoted) catch return error.OutOfMemory; + is_first_part = false; + } else { + result.appendSlice(allocator, unquoted) catch return error.OutOfMemory; + } + + if (next_plus) |pos| { + remaining = remaining[pos + 3 ..]; // Skip " + " + } else { + break; + } + } + + // Free original and return concatenated result + allocator.free(expr); + return result.toOwnedSlice(allocator); +} + +fn isIdentChar(c: u8) bool { + return (c >= 'a' and c <= 'z') or + (c >= 'A' and c <= 'Z') or + (c >= '0' and c <= '9') or + c == '_' or c == '-'; +} + +/// Bind call arguments to mixin parameters +fn bindArguments( + allocator: Allocator, + params: []const u8, + args: []const u8, + bindings: *std.StringHashMapUnmanaged([]const u8), +) MixinError!void { + // Parse parameter names from definition: "text, type" or "text, type='primary'" + var param_names = std.ArrayListUnmanaged([]const u8){}; + defer param_names.deinit(allocator); + + var param_iter = mem.splitSequence(u8, params, ","); + while (param_iter.next()) |param_part| { + const trimmed = mem.trim(u8, param_part, " \t"); + if (trimmed.len == 0) continue; + + // Handle default values: "type='primary'" -> just get "type" + var param_name = trimmed; + if (mem.indexOf(u8, trimmed, "=")) |eq_pos| { + param_name = mem.trim(u8, trimmed[0..eq_pos], " \t"); + } + + // Handle rest args: "...items" -> "items" + if (mem.startsWith(u8, param_name, "...")) { + param_name = param_name[3..]; + } + + param_names.append(allocator, param_name) catch return error.OutOfMemory; + } + + // Parse argument values from call: "'Click', 'primary'" or "text='Click'" + var arg_values = std.ArrayListUnmanaged([]const u8){}; + defer arg_values.deinit(allocator); + + // Simple argument parsing - split by comma but respect quotes + var in_string = false; + var string_char: u8 = 0; + var paren_depth: usize = 0; + var start: usize = 0; + + for (args, 0..) |c, idx| { + if (!in_string) { + if (c == '"' or c == '\'') { + in_string = true; + string_char = c; + } else if (c == '(') { + paren_depth += 1; + } else if (c == ')') { + if (paren_depth > 0) paren_depth -= 1; + } else if (c == ',' and paren_depth == 0) { + const arg_val = mem.trim(u8, args[start..idx], " \t"); + arg_values.append(allocator, stripQuotes(arg_val)) catch return error.OutOfMemory; + start = idx + 1; + } + } else { + if (c == string_char) { + in_string = false; + } + } + } + + // Add last argument + if (start < args.len) { + const arg_val = mem.trim(u8, args[start..], " \t"); + if (arg_val.len > 0) { + arg_values.append(allocator, stripQuotes(arg_val)) catch return error.OutOfMemory; + } + } + + // Bind positional arguments + const min_len = @min(param_names.items.len, arg_values.items.len); + for (0..min_len) |i| { + bindings.put(allocator, param_names.items[i], arg_values.items[i]) catch return error.OutOfMemory; + } +} + +fn stripQuotes(val: []const u8) []const u8 { + if (val.len < 2) return val; + const first = val[0]; + const last = val[val.len - 1]; + if ((first == '"' and last == '"') or (first == '\'' and last == '\'')) { + return val[1 .. val.len - 1]; + } + return val; +} + +/// Clone a node and all its children +fn cloneNode(allocator: Allocator, node: *Node) MixinError!*Node { + const new_node = allocator.create(Node) catch return error.OutOfMemory; + new_node.* = node.*; + new_node.nodes = .{}; + new_node.attrs = .{}; + + // Clone attributes + for (node.attrs.items) |attr| { + new_node.attrs.append(allocator, attr) catch return error.OutOfMemory; + } + + // Clone children recursively + for (node.nodes.items) |child| { + const cloned_child = try cloneNode(allocator, child); + new_node.nodes.append(allocator, cloned_child) catch return error.OutOfMemory; + } + + return new_node; +} + +// ============================================================================ +// Tests +// ============================================================================ + +test "MixinRegistry - basic operations" { + const allocator = std.testing.allocator; + + var registry = MixinRegistry.init(allocator); + defer registry.deinit(); + + // Create a mock mixin node + var mixin_node = Node{ + .type = .Mixin, + .name = "button", + .line = 1, + .column = 1, + }; + + try registry.register("button", &mixin_node); + + try std.testing.expect(registry.contains("button")); + try std.testing.expect(!registry.contains("nonexistent")); + + const retrieved = registry.get("button"); + try std.testing.expect(retrieved != null); + try std.testing.expectEqualStrings("button", retrieved.?.name.?); +} + +test "bindArguments - simple positional" { + const allocator = std.testing.allocator; + + var bindings = std.StringHashMapUnmanaged([]const u8){}; + defer bindings.deinit(allocator); + + try bindArguments(allocator, "text, type", "'Click', 'primary'", &bindings); + + try std.testing.expectEqualStrings("Click", bindings.get("text").?); + try std.testing.expectEqualStrings("primary", bindings.get("type").?); +} + +test "substituteArgs - basic substitution" { + const allocator = std.testing.allocator; + + var bindings = std.StringHashMapUnmanaged([]const u8){}; + defer bindings.deinit(allocator); + + bindings.put(allocator, "title", "Hello") catch unreachable; + bindings.put(allocator, "name", "World") catch unreachable; + + const result = try substituteArgs(allocator, "title is title and name is name", &bindings); + defer allocator.free(result); + + try std.testing.expectEqualStrings("Hello is Hello and World is World", result); +} + +test "stripQuotes" { + try std.testing.expectEqualStrings("hello", stripQuotes("'hello'")); + try std.testing.expectEqualStrings("hello", stripQuotes("\"hello\"")); + try std.testing.expectEqualStrings("hello", stripQuotes("hello")); + try std.testing.expectEqualStrings("", stripQuotes("''")); +} + +test "substituteArgs - string concatenation expression" { + const allocator = std.testing.allocator; + + var bindings = std.StringHashMapUnmanaged([]const u8){}; + defer bindings.deinit(allocator); + + try bindings.put(allocator, "type", "primary"); + + // Test the exact format that comes from the parser + const input = "\"btn btn-\" + type"; + const result = try substituteArgs(allocator, input, &bindings); + defer allocator.free(result); + + // After substitution and concatenation evaluation, should be: btn btn-primary + try std.testing.expectEqualStrings("btn btn-primary", result); +} + +test "evaluateStringConcat - basic" { + const allocator = std.testing.allocator; + + // Test with quoted + unquoted + const input1 = try allocator.dupe(u8, "\"btn btn-\" + primary"); + const result1 = try evaluateStringConcat(allocator, input1); + defer allocator.free(result1); + try std.testing.expectEqualStrings("btn btn-primary", result1); + + // Test with both quoted + const input2 = try allocator.dupe(u8, "\"btn btn-\" + \"primary\""); + const result2 = try evaluateStringConcat(allocator, input2); + defer allocator.free(result2); + try std.testing.expectEqualStrings("btn btn-primary", result2); +} diff --git a/src/parser.zig b/src/parser.zig index d2f3925..53f97c8 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1,1397 +1,1646 @@ -//! Pug Parser - Converts token stream into an AST. -//! -//! The parser processes tokens from the lexer and builds a hierarchical -//! AST representing the document structure. It handles: -//! - Indentation-based nesting via indent/dedent tokens -//! - Element construction (tag, classes, id, attributes) -//! - Control flow (if/else, each, while) -//! - Mixins, includes, and template inheritance -//! -//! ## Error Diagnostics -//! When parsing fails, call `getDiagnostic()` to get rich error info: -//! ```zig -//! var parser = Parser.init(allocator, tokens); -//! const doc = parser.parse() catch |err| { -//! if (parser.getDiagnostic()) |diag| { -//! std.debug.print("{}\n", .{diag}); -//! } -//! return err; -//! }; -//! ``` - const std = @import("std"); +const mem = std.mem; +const Allocator = std.mem.Allocator; + +// Import token types from lexer const lexer = @import("lexer.zig"); -const ast = @import("ast.zig"); -const diagnostic = @import("diagnostic.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; -const Token = lexer.Token; -const TokenType = lexer.TokenType; -const Node = ast.Node; -const Attribute = ast.Attribute; -const TextSegment = ast.TextSegment; +// ============================================================================ +// Inline Tags (tags that are typically inline in HTML) +// ============================================================================ -pub const Diagnostic = diagnostic.Diagnostic; - -/// Errors that can occur during parsing. -pub const ParserError = error{ - UnexpectedToken, - UnexpectedEof, - InvalidSyntax, - MissingCondition, - MissingIterator, - MissingCollection, - MissingMixinName, - MissingBlockName, - MissingPath, - OutOfMemory, +const inline_tags = [_][]const u8{ + "a", + "abbr", + "acronym", + "b", + "br", + "code", + "em", + "font", + "i", + "img", + "ins", + "kbd", + "map", + "samp", + "small", + "span", + "strong", + "sub", + "sup", }; -/// Combined error set for all parser operations. -pub const Error = ParserError || std.mem.Allocator.Error; - -/// Parser for Pug templates. -/// -/// Converts a token slice into an AST. Uses an arena allocator for all -/// AST node allocations, making cleanup simple and efficient. -pub const Parser = struct { - tokens: []const Token, - pos: usize, - allocator: std.mem.Allocator, - /// Original source text (for error snippets) - source: ?[]const u8, - /// Last error diagnostic (populated on error) - last_diagnostic: ?Diagnostic, - - /// Creates a new parser for the given tokens. - pub fn init(allocator: std.mem.Allocator, tokens: []const Token) Parser { - return .{ - .tokens = tokens, - .pos = 0, - .allocator = allocator, - .source = null, - .last_diagnostic = null, - }; +fn isInlineTag(name: []const u8) bool { + for (inline_tags) |tag| { + if (mem.eql(u8, name, tag)) return true; } + return false; +} - /// Creates a parser with source text for better error messages. - pub fn initWithSource(allocator: std.mem.Allocator, tokens: []const Token, source: []const u8) Parser { - return .{ - .tokens = tokens, - .pos = 0, - .allocator = allocator, - .source = source, - .last_diagnostic = null, - }; - } +// ============================================================================ +// AST Node Types +// ============================================================================ - /// Returns the last error diagnostic, if any. - /// Call this after parse() returns an error to get detailed error info. - pub fn getDiagnostic(self: *const Parser) ?Diagnostic { - return self.last_diagnostic; - } +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, +}; - /// Sets a diagnostic error with context from the current token. - fn setDiagnostic(self: *Parser, message: []const u8, suggestion: ?[]const u8) void { - const token = if (self.pos < self.tokens.len) self.tokens[self.pos] else self.tokens[self.tokens.len - 1]; - const source_line = if (self.source) |src| - diagnostic.extractSourceLine(src, 0) // Would need position mapping - else - null; +// ============================================================================ +// AST Node - A tagged union representing all possible AST nodes +// ============================================================================ - self.last_diagnostic = .{ - .line = @intCast(token.line), - .column = @intCast(token.column), - .message = message, - .source_line = source_line, - .suggestion = suggestion, - }; - } +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 +}; - /// Sets a diagnostic error for a specific token. - fn setDiagnosticAtToken(self: *Parser, token: Token, message: []const u8, suggestion: ?[]const u8) void { - self.last_diagnostic = .{ - .line = @intCast(token.line), - .column = @intCast(token.column), - .message = message, - .source_line = null, - .suggestion = suggestion, - }; - } +pub const AttributeBlock = struct { + val: []const u8, + line: usize, + column: usize, + filename: ?[]const u8, +}; - /// Parses all tokens and returns the document AST. - pub fn parse(self: *Parser) Error!ast.Document { - var nodes = std.ArrayList(Node).empty; - errdefer nodes.deinit(self.allocator); +pub const FileReference = struct { + path: ?[]const u8, + line: usize, + column: usize, + filename: ?[]const u8, +}; - var extends_path: ?[]const u8 = null; +pub const Node = struct { + type: NodeType, + line: usize = 0, + column: usize = 0, + filename: ?[]const u8 = null, - // Check for extends directive (must be first) - if (self.check(.kw_extends)) { - extends_path = try self.parseExtends(); - self.skipNewlines(); - } + // Block fields + nodes: std.ArrayListUnmanaged(*Node) = .{}, - // Parse all top-level nodes - while (!self.isAtEnd()) { - self.skipNewlines(); - if (self.isAtEnd()) break; + // NamedBlock additional fields + name: ?[]const u8 = null, // Also used for Tag, Mixin, Filter + mode: ?[]const u8 = null, // "prepend", "append", "replace" - const node = try self.parseNode(); - if (node) |n| { - try nodes.append(self.allocator, n); + // 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 .{ - .nodes = try nodes.toOwnedSlice(self.allocator), - .extends_path = extends_path, + .allocator = allocator, + .tokens = tokens, + .filename = filename, + .src = src, }; } - /// Parses a single node based on current token. - fn parseNode(self: *Parser) Error!?Node { - self.skipNewlines(); - if (self.isAtEnd()) return null; + pub fn deinit(self: *Parser) void { + self.deferred.deinit(self.allocator); + } - const token = self.peek(); + // ======================================================================== + // Token Stream Methods + // ======================================================================== - return switch (token.type) { - .tag => try self.parseElement(), - .class, .id => try self.parseElement(), // div-less element - .kw_doctype => try self.parseDoctype(), - .kw_if => try self.parseConditional(), - .kw_unless => try self.parseConditional(), - .kw_each, .kw_for => try self.parseEach(), - .kw_while => try self.parseWhile(), - .kw_case => try self.parseCase(), - .kw_mixin => try self.parseMixinDef(), - .mixin_call => try self.parseMixinCall(), - .kw_include => try self.parseInclude(), - .kw_block => try self.parseBlock(), - .kw_append => try self.parseBlockShorthand(.append), - .kw_prepend => try self.parseBlockShorthand(.prepend), - .pipe_text => try self.parsePipeText(), - .comment, .comment_unbuffered => try self.parseComment(), - .unbuffered_code => { - // Unbuffered JS code (- var x = 1) - skip entire line + /// 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(); - return null; + } 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; }, - .buffered_text => try self.parseBufferedCode(true), - .unescaped_text => try self.parseBufferedCode(false), - .text => try self.parseText(), - .literal_html => try self.parseLiteralHtml(), - .newline, .eof => null, - .indent, .dedent => { - // Consume structural tokens to prevent infinite loops - _ = self.advance(); - return null; + .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 => { - // Skip unknown tokens to prevent infinite loops - _ = self.advance(); - return null; + self.setError(.INVALID_TOKEN, "unexpected token", tok); + return error.InvalidToken; }, }; } - /// Parses an HTML element with optional tag, classes, id, attributes, and children. - fn parseElement(self: *Parser) Error!Node { - var tag: []const u8 = "div"; // default tag - var classes = std.ArrayList([]const u8).empty; - var id: ?[]const u8 = null; - var attributes = std.ArrayList(Attribute).empty; - var spread_attributes: ?[]const u8 = null; - var self_closing = false; + fn parseDot(self: *Parser) !*Node { + _ = self.advance(); + return self.parseTextBlock() orelse try self.emptyBlock(self.peek().loc.start.line); + } - errdefer classes.deinit(self.allocator); - errdefer attributes.deinit(self.allocator); + // ======================================================================== + // Text Parsing + // ======================================================================== - // Parse tag name if present - if (self.check(.tag)) { - tag = self.advance().value; - } + 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); - // Parse classes and ids in any order - while (self.check(.class) or self.check(.id)) { - if (self.check(.class)) { - try classes.append(self.allocator, self.advance().value); - } else if (self.check(.id)) { - id = self.advance().value; - } - } - - // Parse attributes - if (self.check(.lparen)) { - _ = self.advance(); // skip ( - try self.parseAttributes(&attributes); - if (self.check(.rparen)) { - _ = self.advance(); // skip ) - } - } - - // Parse additional classes and ids after attributes (e.g., a.foo(href='/').bar) - while (self.check(.class) or self.check(.id)) { - if (self.check(.class)) { - try classes.append(self.allocator, self.advance().value); - } else if (self.check(.id)) { - id = self.advance().value; - } - } - - // Parse &attributes({...}) - if (self.check(.ampersand_attrs)) { - _ = self.advance(); // skip &attributes - if (self.check(.attr_value)) { - spread_attributes = self.advance().value; - } - } - - // Check for self-closing marker (foo/ or foo(attr)/) - if (self.check(.self_close)) { - _ = self.advance(); - self_closing = true; - } - - // Check for block expansion (`:`) - if (self.check(.colon)) { - _ = self.advance(); - self.skipWhitespace(); - - // Parse the inline nested element - var children = std.ArrayList(Node).empty; - errdefer children.deinit(self.allocator); - - if (try self.parseNode()) |child| { - try children.append(self.allocator, child); - } - - return .{ - .element = .{ - .tag = tag, - .classes = try classes.toOwnedSlice(self.allocator), - .id = id, - .attributes = try attributes.toOwnedSlice(self.allocator), - .spread_attributes = spread_attributes, - .children = try children.toOwnedSlice(self.allocator), - .self_closing = self_closing, - .inline_text = null, - .buffered_code = null, - .is_inline = true, // Block expansion renders children inline + 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, }; } - // Parse inline text or buffered code if present - var inline_text: ?[]TextSegment = null; - var buffered_code: ?ast.Code = null; - - if (self.check(.buffered_text) or self.check(.unescaped_text)) { - // Handle p= expr or p!= expr - const escaped = self.peek().type == .buffered_text; - _ = self.advance(); // skip = or != - - // Get the expression - var expr: []const u8 = ""; - if (self.check(.text)) { - expr = self.advance().value; - } - buffered_code = .{ .expression = expr, .escaped = escaped }; - } else if (self.check(.text) or self.check(.interp_start) or self.check(.interp_start_unesc)) { - inline_text = try self.parseTextSegments(); - } - - // Check for dot block (raw text) - if (self.check(.dot_block)) { - _ = self.advance(); - self.skipNewlines(); - - // Parse raw text block - if (self.check(.indent)) { - _ = self.advance(); - const raw_content = try self.parseRawTextBlock(); - - var children = std.ArrayList(Node).empty; - errdefer children.deinit(self.allocator); - try children.append(self.allocator, .{ .raw_text = .{ .content = raw_content } }); - - return .{ .element = .{ - .tag = tag, - .classes = try classes.toOwnedSlice(self.allocator), - .id = id, - .attributes = try attributes.toOwnedSlice(self.allocator), - .spread_attributes = spread_attributes, - .children = try children.toOwnedSlice(self.allocator), - .self_closing = self_closing, - .inline_text = inline_text, - .buffered_code = buffered_code, - } }; - } - } - - // Skip newline after element declaration - self.skipNewlines(); - - // Parse children if indented - var children = std.ArrayList(Node).empty; - errdefer children.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&children); - } - - return .{ .element = .{ - .tag = tag, - .classes = try classes.toOwnedSlice(self.allocator), - .id = id, - .attributes = try attributes.toOwnedSlice(self.allocator), - .spread_attributes = spread_attributes, - .children = try children.toOwnedSlice(self.allocator), - .self_closing = self_closing, - .inline_text = inline_text, - .buffered_code = buffered_code, - } }; + return node; } - /// Parses attributes within parentheses. - fn parseAttributes(self: *Parser, attributes: *std.ArrayList(Attribute)) Error!void { - while (!self.check(.rparen) and !self.isAtEnd()) { - // Skip commas - if (self.check(.comma)) { - _ = self.advance(); - continue; - } - - // Parse attribute name - if (!self.check(.attr_name)) break; - const name = self.advance().value; - - // Check for value - var value: ?[]const u8 = null; - var escaped = true; - - if (self.check(.attr_eq)) { - const eq_token = self.advance(); - escaped = !std.mem.eql(u8, eq_token.value, "!="); - - if (self.check(.attr_value)) { - value = self.advance().value; - } - } - - try attributes.append(self.allocator, .{ - .name = name, - .value = value, - .escaped = escaped, - }); - } - } - - /// Parses text segments (literals and interpolations). - fn parseTextSegments(self: *Parser) Error![]TextSegment { - var segments = std.ArrayList(TextSegment).empty; - errdefer segments.deinit(self.allocator); - - while (self.check(.text) or self.check(.interp_start) or self.check(.interp_start_unesc) or self.check(.tag_interp_start)) { - if (self.check(.text)) { - try segments.append(self.allocator, .{ .literal = self.advance().value }); - } else if (self.check(.interp_start)) { - _ = self.advance(); // skip #{ - if (self.check(.text)) { - try segments.append(self.allocator, .{ .interp_escaped = self.advance().value }); - } - if (self.check(.interp_end)) { - _ = self.advance(); // skip } - } - } else if (self.check(.interp_start_unesc)) { - _ = self.advance(); // skip !{ - if (self.check(.text)) { - try segments.append(self.allocator, .{ .interp_unescaped = self.advance().value }); - } - if (self.check(.interp_end)) { - _ = self.advance(); // skip } - } - } else if (self.check(.tag_interp_start)) { - const inline_tag = try self.parseTagInterpolation(); - try segments.append(self.allocator, .{ .interp_tag = inline_tag }); - } - } - - return segments.toOwnedSlice(self.allocator); - } - - /// Parses tag interpolation: #[tag.class#id(attrs) text] - fn parseTagInterpolation(self: *Parser) Error!ast.InlineTag { - _ = self.advance(); // skip #[ - - var tag: []const u8 = "span"; // default tag - var classes = std.ArrayList([]const u8).empty; - var id: ?[]const u8 = null; - var attributes = std.ArrayList(Attribute).empty; - - errdefer classes.deinit(self.allocator); - errdefer attributes.deinit(self.allocator); - - // Parse tag name if present - if (self.check(.tag)) { - tag = self.advance().value; - } - - // Parse classes and ids - while (self.check(.class) or self.check(.id)) { - if (self.check(.class)) { - try classes.append(self.allocator, self.advance().value); - } else if (self.check(.id)) { - id = self.advance().value; - } - } - - // Parse attributes if present - if (self.check(.lparen)) { - _ = self.advance(); // skip ( - try self.parseAttributes(&attributes); - if (self.check(.rparen)) { - _ = self.advance(); // skip ) - } - } - - // Parse inner text segments (may contain nested interpolations) - var text_segments = std.ArrayList(TextSegment).empty; - errdefer text_segments.deinit(self.allocator); - - while (!self.check(.tag_interp_end) and !self.check(.newline) and !self.isAtEnd()) { - if (self.check(.text)) { - try text_segments.append(self.allocator, .{ .literal = self.advance().value }); - } else if (self.check(.interp_start)) { - _ = self.advance(); // skip #{ - if (self.check(.text)) { - try text_segments.append(self.allocator, .{ .interp_escaped = self.advance().value }); - } - if (self.check(.interp_end)) { - _ = self.advance(); // skip } - } - } else if (self.check(.interp_start_unesc)) { - _ = self.advance(); // skip !{ - if (self.check(.text)) { - try text_segments.append(self.allocator, .{ .interp_unescaped = self.advance().value }); - } - if (self.check(.interp_end)) { - _ = self.advance(); // skip } - } - } else if (self.check(.tag_interp_start)) { - // Nested tag interpolation - const nested_tag = try self.parseTagInterpolation(); - try text_segments.append(self.allocator, .{ .interp_tag = nested_tag }); - } else { - break; - } - } - - // Skip closing ] - if (self.check(.tag_interp_end)) { - _ = self.advance(); - } - - return .{ - .tag = tag, - .classes = try classes.toOwnedSlice(self.allocator), - .id = id, - .attributes = try attributes.toOwnedSlice(self.allocator), - .text_segments = try text_segments.toOwnedSlice(self.allocator), + 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; } - /// Parses children within an indented block. - fn parseChildren(self: *Parser, children: *std.ArrayList(Node)) Error!void { - while (!self.check(.dedent) and !self.isAtEnd()) { - self.skipNewlines(); - if (self.check(.dedent) or self.isAtEnd()) break; + // ======================================================================== + // Code Parsing + // ======================================================================== - if (try self.parseNode()) |child| { - try children.append(self.allocator, child); + 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; } } - // Consume dedent - if (self.check(.dedent)) { - _ = self.advance(); + 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; } - /// Parses a raw text block (after `.`). - fn parseRawTextBlock(self: *Parser) Error![]const u8 { - var lines = std.ArrayList(u8).empty; - errdefer lines.deinit(self.allocator); + 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); - var line_count: usize = 0; - while (!self.check(.dedent) and !self.isAtEnd()) { - if (self.check(.text)) { - // Add newline before each line except the first - if (line_count > 0) { - try lines.append(self.allocator, '\n'); + // 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; } - line_count += 1; - const text = self.advance().value; - try lines.appendSlice(self.allocator, text); - } else if (self.check(.newline)) { - _ = self.advance(); + } else if (self.peek().type == .@"else") { + _ = try self.expect(.@"else"); + if (self.peek().type == .indent) { + current_node.alternate = try self.block_(); + } + break; } else { break; } } - // Add trailing newline only for multi-line content (for proper formatting) - if (line_count > 1) { - try lines.append(self.allocator, '\n'); - } - - if (self.check(.dedent)) { - _ = self.advance(); - } - - return lines.toOwnedSlice(self.allocator); + return node; } - /// Parses doctype declaration. - fn parseDoctype(self: *Parser) Error!Node { - _ = self.advance(); // skip 'doctype' + 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, + }; - // Get the doctype value (rest of line), defaults to "html" if empty - var value: []const u8 = "html"; - if (self.check(.text)) { - value = self.advance().value; - } - - return .{ .doctype = .{ .value = value } }; - } - - /// Parses conditional (if/else if/else/unless). - fn parseConditional(self: *Parser) Error!Node { - var branches = std.ArrayList(ast.Conditional.Branch).empty; - errdefer branches.deinit(self.allocator); - - // Parse initial if/unless - const is_unless = self.check(.kw_unless); - _ = self.advance(); // skip if/unless - - // Parse condition (rest of line as text) - const condition = try self.parseRestOfLine(); - - self.skipNewlines(); - - // Parse body - var body = std.ArrayList(Node).empty; - errdefer body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&body); - } - - try branches.append(self.allocator, .{ - .condition = condition, - .is_unless = is_unless, - .children = try body.toOwnedSlice(self.allocator), - }); - - // Parse else if / else branches - while (self.check(.kw_else)) { - _ = self.advance(); // skip else - - var else_condition: ?[]const u8 = null; - const else_is_unless = false; - - // Check for "else if" - if (self.check(.kw_if)) { - _ = self.advance(); - else_condition = try self.parseRestOfLine(); + // Handle block + if (self.peek().type == .indent) { + const block = try self.block_(); + for (block.nodes.items) |n| { + try node.addNode(self.allocator, n); } - - self.skipNewlines(); - - var else_body = std.ArrayList(Node).empty; - errdefer else_body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&else_body); - } - - try branches.append(self.allocator, .{ - .condition = else_condition, - .is_unless = else_is_unless, - .children = try else_body.toOwnedSlice(self.allocator), - }); - - // Plain else (no condition) is the last branch - if (else_condition == null) break; + block.nodes.deinit(self.allocator); + self.allocator.destroy(block); } - return .{ .conditional = .{ - .branches = try branches.toOwnedSlice(self.allocator), - } }; + return node; } - /// Parses each loop. - fn parseEach(self: *Parser) Error!Node { - _ = self.advance(); // skip 'each' or 'for' + fn parseBlockCode(self: *Parser) !*Node { + const tok = try self.expect(.blockcode); + const line = tok.loc.start.line; + const column = tok.loc.start.column; - // Parse: each value[, index] in collection - var value_name: []const u8 = ""; - var index_name: ?[]const u8 = null; - var collection: []const u8 = ""; + var text = std.ArrayListUnmanaged(u8){}; + defer text.deinit(self.allocator); - // The lexer captures "item in items" or "item, idx in items" as a single text token - if (self.check(.text)) { - const text = self.advance().value; - - // Parse: value[, index] in collection - // Find "in " to split the text - if (std.mem.indexOf(u8, text, " in ")) |in_pos| { - const before_in = std.mem.trim(u8, text[0..in_pos], " \t"); - collection = std.mem.trim(u8, text[in_pos + 4 ..], " \t"); - - // Check for comma (index variable) - if (std.mem.indexOf(u8, before_in, ",")) |comma_pos| { - value_name = std.mem.trim(u8, before_in[0..comma_pos], " \t"); - index_name = std.mem.trim(u8, before_in[comma_pos + 1 ..], " \t"); - } else { - value_name = before_in; + 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 { - self.setDiagnostic( - "Missing collection in 'each' loop - expected 'in' keyword", - "Use syntax: each item in collection", - ); - return ParserError.MissingCollection; - } - } else if (self.check(.tag)) { - // Fallback: lexer produced individual tokens - value_name = self.advance().value; - - // Check for index: each val, idx in ... - if (self.check(.comma)) { - _ = self.advance(); - if (self.check(.tag)) { - index_name = self.advance().value; + 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); } } - - // Expect 'in' - if (self.check(.kw_in)) { - _ = self.advance(); - } - - // Parse collection expression - collection = try self.parseRestOfLine(); - } else { - self.setDiagnostic( - "Missing iterator variable in 'each' loop", - "Use syntax: each item in collection", - ); - return ParserError.MissingIterator; } - - self.skipNewlines(); - - // Parse body - var body = std.ArrayList(Node).empty; - errdefer body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&body); - } - - // Check for else branch - var else_children = std.ArrayList(Node).empty; - errdefer else_children.deinit(self.allocator); - - if (self.check(.kw_else)) { - _ = self.advance(); - self.skipNewlines(); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&else_children); - } - } - - return .{ .each = .{ - .value_name = value_name, - .index_name = index_name, - .collection = collection, - .children = try body.toOwnedSlice(self.allocator), - .else_children = try else_children.toOwnedSlice(self.allocator), - } }; + _ = try self.expect(.outdent); + return block; } - /// Parses while loop. - fn parseWhile(self: *Parser) Error!Node { - _ = self.advance(); // skip 'while' + // ======================================================================== + // Interpolation/Tag Parsing + // ======================================================================== - const condition = try self.parseRestOfLine(); - - self.skipNewlines(); - - var body = std.ArrayList(Node).empty; - errdefer body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&body); - } - - return .{ .@"while" = .{ - .condition = condition, - .children = try body.toOwnedSlice(self.allocator), - } }; + 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; } - /// Parses case/switch statement. - fn parseCase(self: *Parser) Error!Node { - _ = self.advance(); // skip 'case' + 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; + } - const expression = try self.parseRestOfLine(); + 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); - self.skipNewlines(); - - var whens = std.ArrayList(ast.Case.When).empty; - errdefer whens.deinit(self.allocator); - - var default_children = std.ArrayList(Node).empty; - errdefer default_children.deinit(self.allocator); - - // Parse indented when/default clauses - if (self.check(.indent)) { - _ = self.advance(); - - while (!self.check(.dedent) and !self.isAtEnd()) { - self.skipNewlines(); - - if (self.check(.kw_when)) { - _ = self.advance(); // skip 'when' - - // Parse the value (rest of line or until colon for block expansion) - var value: []const u8 = ""; - if (self.check(.tag) or self.check(.text)) { - value = self.advance().value; - } else { - value = try self.parseRestOfLine(); - } - - var when_children = std.ArrayList(Node).empty; - errdefer when_children.deinit(self.allocator); - var has_break = false; - - // Check for block expansion (: element) - if (self.check(.colon)) { - _ = self.advance(); - self.skipWhitespace(); - if (try self.parseNode()) |child| { - try when_children.append(self.allocator, child); - } - } else { - self.skipNewlines(); - - // Parse indented children - if (self.check(.indent)) { - _ = self.advance(); - - // Check for explicit break (- break) - if (self.check(.buffered_text)) { - const next_tok = self.peek(); - if (next_tok.type == .text and std.mem.eql(u8, std.mem.trim(u8, next_tok.value, " \t"), "break")) { - _ = self.advance(); // skip = - _ = self.advance(); // skip break - has_break = true; - } - } - - if (!has_break) { - try self.parseChildren(&when_children); - } else { - // Skip remaining children after break - while (!self.check(.dedent) and !self.isAtEnd()) { - _ = self.advance(); - } - } - - if (self.check(.dedent)) { - _ = self.advance(); + // (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; } } - // Empty body = fall-through (children stays empty) + 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 whens.append(self.allocator, .{ - .value = value, - .children = try when_children.toOwnedSlice(self.allocator), - .has_break = has_break, + 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 }); - } else if (self.check(.kw_default)) { - _ = self.advance(); // skip 'default' + }, + .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 block expansion (: element) - if (self.check(.colon)) { - _ = self.advance(); - self.skipWhitespace(); - if (try self.parseNode()) |child| { - try default_children.append(self.allocator, child); - } - } else { - self.skipNewlines(); + // Check for textOnly (.) + if (self.peek().type == .dot) { + tag.text_only = true; + _ = self.advance(); + } - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&default_children); - if (self.check(.dedent)) { - _ = 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; } } - } else if (self.check(.dedent)) { - break; - } else { - // Skip unknown tokens - _ = self.advance(); + try names.append(self.allocator, attr_name); } } - if (self.check(.dedent)) { - _ = self.advance(); - } + 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(); } - return .{ .case = .{ - .expression = expression, - .whens = try whens.toOwnedSlice(self.allocator), - .default_children = try default_children.toOwnedSlice(self.allocator), - } }; - } + try self.defer_token(tok); + _ = try self.expect(.end_attributes); - /// Parses mixin definition. - fn parseMixinDef(self: *Parser) Error!Node { - _ = self.advance(); // skip 'mixin' - - // Parse mixin name - var name: []const u8 = ""; - if (self.check(.tag)) { - name = self.advance().value; - } else { - self.setDiagnostic( - "Missing mixin name after 'mixin' keyword", - "Use syntax: mixin name(params)", - ); - return ParserError.MissingMixinName; - } - - // Parse parameters if present - var params = std.ArrayList([]const u8).empty; - var defaults = std.ArrayList(?[]const u8).empty; - errdefer params.deinit(self.allocator); - errdefer defaults.deinit(self.allocator); - - var has_rest = false; - - if (self.check(.lparen)) { - _ = self.advance(); - - while (!self.check(.rparen) and !self.isAtEnd()) { - if (self.check(.comma)) { - _ = self.advance(); - continue; - } - - if (self.check(.attr_name) or self.check(.tag)) { - const param_name = self.advance().value; - - // Check for rest parameter - if (std.mem.startsWith(u8, param_name, "...")) { - try params.append(self.allocator, param_name[3..]); - try defaults.append(self.allocator, null); - has_rest = true; - } else { - try params.append(self.allocator, param_name); - - // Check for default value - if (self.check(.attr_eq)) { - _ = self.advance(); - if (self.check(.attr_value)) { - try defaults.append(self.allocator, self.advance().value); - } else { - try defaults.append(self.allocator, null); - } - } else { - try defaults.append(self.allocator, null); - } - } - } else { - break; - } - } - - if (self.check(.rparen)) { - _ = self.advance(); - } - } - - self.skipNewlines(); - - // Parse body - var body = std.ArrayList(Node).empty; - errdefer body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&body); - } - - return .{ .mixin_def = .{ - .name = name, - .params = try params.toOwnedSlice(self.allocator), - .defaults = try defaults.toOwnedSlice(self.allocator), - .has_rest = has_rest, - .children = try body.toOwnedSlice(self.allocator), - } }; - } - - /// Parses mixin call. - fn parseMixinCall(self: *Parser) Error!Node { - const name = self.advance().value; // +name - - var args = std.ArrayList([]const u8).empty; - var attributes = std.ArrayList(Attribute).empty; - errdefer args.deinit(self.allocator); - errdefer attributes.deinit(self.allocator); - - // Parse arguments - if (self.check(.lparen)) { - _ = self.advance(); - - while (!self.check(.rparen) and !self.isAtEnd()) { - if (self.check(.comma)) { - _ = self.advance(); - continue; - } - - if (self.check(.attr_value)) { - try args.append(self.allocator, self.advance().value); - } else if (self.check(.attr_name)) { - // Could be named arg or regular arg - const val = self.advance().value; - try args.append(self.allocator, val); - } else { - break; - } - } - - if (self.check(.rparen)) { - _ = self.advance(); - } - } - - // Parse attributes passed to mixin - if (self.check(.lparen)) { - _ = self.advance(); - try self.parseAttributes(&attributes); - if (self.check(.rparen)) { - _ = self.advance(); - } - } - - self.skipNewlines(); - - // Parse block content - var block_children = std.ArrayList(Node).empty; - errdefer block_children.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&block_children); - } - - return .{ .mixin_call = .{ - .name = name, - .args = try args.toOwnedSlice(self.allocator), - .attributes = try attributes.toOwnedSlice(self.allocator), - .block_children = try block_children.toOwnedSlice(self.allocator), - } }; - } - - /// Parses include directive. - fn parseInclude(self: *Parser) Error!Node { - _ = self.advance(); // skip 'include' - - var filter: ?[]const u8 = null; - - // Check for filter :markdown - if (self.check(.colon)) { - _ = self.advance(); - if (self.check(.tag)) { - filter = self.advance().value; - } - } - - // Parse path - const path = try self.parseRestOfLine(); - - return .{ .include = .{ - .path = path, - .filter = filter, - } }; - } - - /// Parses extends directive. - fn parseExtends(self: *Parser) Error![]const u8 { - _ = self.advance(); // skip 'extends' - return try self.parseRestOfLine(); - } - - /// Parses block directive. - fn parseBlock(self: *Parser) Error!Node { - _ = self.advance(); // skip 'block' - - var mode: ast.Block.Mode = .replace; - - // Check for append/prepend (may be tokenized as tag or keyword) - if (self.check(.tag)) { - const modifier = self.peek().value; - if (std.mem.eql(u8, modifier, "append")) { - mode = .append; - _ = self.advance(); - } else if (std.mem.eql(u8, modifier, "prepend")) { - mode = .prepend; - _ = self.advance(); - } - } else if (self.check(.kw_append)) { - mode = .append; - _ = self.advance(); - } else if (self.check(.kw_prepend)) { - mode = .prepend; - _ = self.advance(); - } - - // Parse block name - if no name follows, this is a mixin block placeholder - var name: []const u8 = ""; - if (self.check(.tag)) { - name = self.advance().value; - } else if (self.check(.text)) { - name = std.mem.trim(u8, self.advance().value, " \t"); - } else if (self.check(.newline) or self.check(.eof) or self.check(.indent) or self.check(.dedent)) { - // No name - this is a mixin block placeholder - return .{ .mixin_block = {} }; - } else { - self.setDiagnostic( - "Missing block name after 'block' keyword", - "Use syntax: block name", - ); - return ParserError.MissingBlockName; - } - - self.skipNewlines(); - - // Parse body - var body = std.ArrayList(Node).empty; - errdefer body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&body); - } - - return .{ .block = .{ - .name = name, - .mode = mode, - .children = try body.toOwnedSlice(self.allocator), - } }; - } - - /// Parses shorthand block syntax: `append name` or `prepend name` - fn parseBlockShorthand(self: *Parser, mode: ast.Block.Mode) Error!Node { - _ = self.advance(); // skip 'append' or 'prepend' - - // Parse block name - var name: []const u8 = ""; - if (self.check(.tag)) { - name = self.advance().value; - } else if (self.check(.text)) { - name = std.mem.trim(u8, self.advance().value, " \t"); - } else { - self.setDiagnostic( - "Missing block name after 'append' or 'prepend'", - "Use syntax: append blockname or prepend blockname", - ); - return ParserError.MissingBlockName; - } - - self.skipNewlines(); - - // Parse body - var body = std.ArrayList(Node).empty; - errdefer body.deinit(self.allocator); - - if (self.check(.indent)) { - _ = self.advance(); - try self.parseChildren(&body); - } - - return .{ .block = .{ - .name = name, - .mode = mode, - .children = try body.toOwnedSlice(self.allocator), - } }; - } - - /// Parses pipe text. - fn parsePipeText(self: *Parser) Error!Node { - _ = self.advance(); // skip | - - const segments = try self.parseTextSegments(); - - return .{ .text = .{ .segments = segments } }; - } - - /// Parses literal HTML (lines starting with <). - fn parseLiteralHtml(self: *Parser) Error!Node { - const html = self.advance().value; - return .{ .raw_text = .{ .content = html } }; - } - - /// Parses comment. - fn parseComment(self: *Parser) Error!Node { - const rendered = self.check(.comment); - const content = self.advance().value; // Preserve content exactly as captured (including leading space) - - self.skipNewlines(); - - // Parse nested comment content ONLY if this is a block comment - // Block comment: comment with no inline content, followed by indented block - // e.g., "//" on its own line followed by indented content - // vs inline comment: "// some text" which has no children - var children = std.ArrayList(Node).empty; - errdefer children.deinit(self.allocator); - - // Block comments can have indented content - // This includes both empty comments (//) and comments with text (// block) - // followed by indented content - if (self.check(.indent)) { - _ = self.advance(); - // Capture all content until dedent as raw text - const raw_content = try self.parseBlockCommentContent(); - if (raw_content.len > 0) { - try children.append(self.allocator, .{ .raw_text = .{ .content = raw_content } }); - } - } - - return .{ .comment = .{ - .content = content, - .rendered = rendered, - .children = try children.toOwnedSlice(self.allocator), - } }; - } - - /// Parses block comment content - collects raw text tokens until dedent - fn parseBlockCommentContent(self: *Parser) Error![]const u8 { - var lines = std.ArrayList(u8).empty; - errdefer lines.deinit(self.allocator); - - while (!self.isAtEnd()) { - const token = self.peek(); - - switch (token.type) { - .dedent => { - _ = self.advance(); - break; - }, - .newline => { - try lines.append(self.allocator, '\n'); - _ = self.advance(); - }, - .text => { - // Raw text from comment block mode - try lines.appendSlice(self.allocator, token.value); - _ = self.advance(); - }, - .eof => break, - else => { - // Skip any unexpected tokens - _ = self.advance(); - }, - } - } - - return lines.toOwnedSlice(self.allocator); - } - - /// Parses buffered code output (= or !=). - fn parseBufferedCode(self: *Parser, escaped: bool) Error!Node { - _ = self.advance(); // skip = or != - - const expression = try self.parseRestOfLine(); - - return .{ .code = .{ - .expression = expression, - .escaped = escaped, - } }; - } - - /// Parses plain text node. - fn parseText(self: *Parser) Error!Node { - const segments = try self.parseTextSegments(); - return .{ .text = .{ .segments = segments } }; - } - - /// Parses rest of line as text. - fn parseRestOfLine(self: *Parser) Error![]const u8 { - var result = std.ArrayList(u8).empty; - errdefer result.deinit(self.allocator); - - while (!self.check(.newline) and !self.check(.indent) and !self.check(.dedent) and !self.isAtEnd()) { - const token = self.advance(); - if (token.value.len > 0) { - if (result.items.len > 0) { - try result.append(self.allocator, ' '); - } - try result.appendSlice(self.allocator, token.value); - } - } - - return result.toOwnedSlice(self.allocator); - } - - // ───────────────────────────────────────────────────────────────────────── - // Helper functions - // ───────────────────────────────────────────────────────────────────────── - - /// Returns true if at end of tokens. - fn isAtEnd(self: *const Parser) bool { - return self.pos >= self.tokens.len or self.peek().type == .eof; - } - - /// Returns current token without advancing. - fn peek(self: *const Parser) Token { - if (self.pos >= self.tokens.len) { - return .{ .type = .eof, .value = "", .line = 0, .column = 0 }; - } - return self.tokens[self.pos]; - } - - /// Returns true if current token matches the given type. - fn check(self: *const Parser, token_type: TokenType) bool { - if (self.isAtEnd()) return false; - return self.peek().type == token_type; - } - - /// Returns true if current token matches the given type and value. - fn checkValue(self: *const Parser, token_type: TokenType, value: []const u8) bool { - if (self.isAtEnd()) return false; - const token = self.peek(); - return token.type == token_type and std.mem.eql(u8, token.value, value); - } - - /// Advances and returns current token. - fn advance(self: *Parser) Token { - if (!self.isAtEnd()) { - const token = self.tokens[self.pos]; - self.pos += 1; - return token; - } - return .{ .type = .eof, .value = "", .line = 0, .column = 0 }; - } - - /// Skips newline tokens. - fn skipNewlines(self: *Parser) void { - while (self.check(.newline)) { - _ = self.advance(); - } - } - - /// Skips whitespace (spaces in tokens). - fn skipWhitespace(self: *Parser) void { - // Whitespace is mostly handled by lexer, but skip any stray newlines - self.skipNewlines(); + return result; } }; -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ // Tests -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ -test "parse simple element" { +test "parser basic" { const allocator = std.testing.allocator; - var lex = lexer.Lexer.init(allocator, "div"); - defer lex.deinit(); - const tokens = try lex.tokenize(); + // 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); - const doc = try parser.parse(); + var parser = Parser.init(allocator, &tokens, "test.pug", null); + defer parser.deinit(); - try std.testing.expectEqual(@as(usize, 1), doc.nodes.len); - try std.testing.expectEqualStrings("div", doc.nodes[0].element.tag); + const ast = try parser.parse(); + defer { + ast.deinit(allocator); + allocator.destroy(ast); + } - // Clean up - allocator.free(doc.nodes[0].element.classes); - allocator.free(doc.nodes[0].element.attributes); - allocator.free(doc.nodes[0].element.children); - allocator.free(doc.nodes); + 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 "parse element with class and id" { +test "parser doctype" { const allocator = std.testing.allocator; - var lex = lexer.Lexer.init(allocator, "div#main.container.active"); - defer lex.deinit(); - const tokens = try lex.tokenize(); + 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); - const doc = try parser.parse(); + var parser = Parser.init(allocator, &tokens, "test.pug", null); + defer parser.deinit(); - const elem = doc.nodes[0].element; - try std.testing.expectEqualStrings("div", elem.tag); - try std.testing.expectEqualStrings("main", elem.id.?); - try std.testing.expectEqual(@as(usize, 2), elem.classes.len); - try std.testing.expectEqualStrings("container", elem.classes[0]); - try std.testing.expectEqualStrings("active", elem.classes[1]); + const ast = try parser.parse(); + defer { + ast.deinit(allocator); + allocator.destroy(ast); + } - // Clean up - allocator.free(elem.classes); - allocator.free(elem.attributes); - allocator.free(elem.children); - allocator.free(doc.nodes); -} - -test "parse nested elements" { - const allocator = std.testing.allocator; - - var lex = lexer.Lexer.init(allocator, - \\div - \\ p Hello - ); - defer lex.deinit(); - const tokens = try lex.tokenize(); - - var parser = Parser.init(allocator, tokens); - const doc = try parser.parse(); - - try std.testing.expectEqual(@as(usize, 1), doc.nodes.len); - - const div = doc.nodes[0].element; - try std.testing.expectEqualStrings("div", div.tag); - try std.testing.expectEqual(@as(usize, 1), div.children.len); - - const p = div.children[0].element; - try std.testing.expectEqualStrings("p", p.tag); - - // Clean up nested structures - if (p.inline_text) |text| allocator.free(text); - allocator.free(p.classes); - allocator.free(p.attributes); - allocator.free(p.children); - allocator.free(div.classes); - allocator.free(div.attributes); - allocator.free(div.children); - allocator.free(doc.nodes); + 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/playground/benchmark b/src/playground/benchmark new file mode 100755 index 0000000000000000000000000000000000000000..a4d3400053b0345d1b255ac80f4b43d955a2494c GIT binary patch literal 233448 zcmdqK3wTu3x%j>IOhRT7kW0cH%}pf%D^-MKBs7x*G=SHT7BB5d051uk6wyjVOD^p( z5N#c$wZ)z$fUViVSid7Ev^{@<oVFlpMOxe1_Am)rXA%@?AY>po-|yX*WHJOq+jGAE z`SLtF`!aj2^{#il_w}x|e|z%7PX;TcDgGQB!#NrUDfMmjMR}ygaujlul-!he?d;oT z7u_gN`~Mk*`#(D4PU=}j10^NJvu`Qx|Fe{@Tqmvf{n_`iJdz{M`l+Pk{ySIQ-}hd0 zeS3gi>hta}p_2Q4HOHvRzS08E%KIN!w6gb6bbYQC&8Y8V>kYY&tj}fD2M&!tv%ZqD zJC}X^ⅈ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 r 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 Y@rtGQ?%wx7|JuDZA8-5MhDO8qU#gaUAOHXW literal 0 HcmV?d00001 diff --git a/src/playground/benchmark.zig b/src/playground/benchmark.zig new file mode 100644 index 0000000..162bd57 --- /dev/null +++ b/src/playground/benchmark.zig @@ -0,0 +1,66 @@ +// benchmark.zig - Benchmark for pugz (Zig Pug implementation) +// +// This benchmark matches the JavaScript pug benchmark for comparison +// Uses exact same templates as packages/pug/support/benchmark.js + +const std = @import("std"); +const pug = @import("../pug.zig"); + +const MIN_ITERATIONS: usize = 200; +const MIN_TIME_NS: u64 = 200_000_000; // 200ms minimum + +fn benchmark(comptime name: []const u8, template: []const u8, iterations: *usize, elapsed_ns: *u64) !void { + const allocator = std.heap.page_allocator; + + // Warmup + for (0..10) |_| { + var result = try pug.compile(allocator, template, .{}); + result.deinit(allocator); + } + + var timer = try std.time.Timer.start(); + + var count: usize = 0; + while (count < MIN_ITERATIONS or timer.read() < MIN_TIME_NS) { + var result = try pug.compile(allocator, template, .{}); + result.deinit(allocator); + count += 1; + } + + const elapsed = timer.read(); + iterations.* = count; + elapsed_ns.* = elapsed; + + const ops_per_sec = @as(f64, @floatFromInt(count)) * 1_000_000_000.0 / @as(f64, @floatFromInt(elapsed)); + std.debug.print("{s}: {d:.0}\n", .{ name, ops_per_sec }); +} + +pub fn main() !void { + var iterations: usize = 0; + var elapsed_ns: u64 = 0; + + // Tiny template - exact match to JS: 'html\n body\n h1 Title' + const tiny = "html\n body\n h1 Title"; + try benchmark("tiny", tiny, &iterations, &elapsed_ns); + + // Small template - exact match to JS (note trailing \n on each line) + const small = + "html\n" ++ + " body\n" ++ + " h1 Title\n" ++ + " ul#menu\n" ++ + " li: a(href=\"#\") Home\n" ++ + " li: a(href=\"#\") About Us\n" ++ + " li: a(href=\"#\") Store\n" ++ + " li: a(href=\"#\") FAQ\n" ++ + " li: a(href=\"#\") Contact\n"; + try benchmark("small", small, &iterations, &elapsed_ns); + + // Medium template - Array(30).join(str) creates 29 copies in JS + const medium = small ** 29; + try benchmark("medium", medium, &iterations, &elapsed_ns); + + // Large template - Array(100).join(str) creates 99 copies in JS + const large = small ** 99; + try benchmark("large", large, &iterations, &elapsed_ns); +} diff --git a/src/playground/benchmark_examples.zig b/src/playground/benchmark_examples.zig new file mode 100644 index 0000000..21a434b --- /dev/null +++ b/src/playground/benchmark_examples.zig @@ -0,0 +1,274 @@ +// benchmark_examples.zig - Benchmark pug example files +// +// Tests the same example files as the JS benchmark + +const std = @import("std"); +const pug = @import("../pug.zig"); + +const Example = struct { + name: []const u8, + source: []const u8, +}; + +// Example templates (matching JS pug examples that don't use includes/extends) +const examples = [_]Example{ + .{ + .name = "attributes.pug", + .source = + \\div#id.left.container(class='user user-' + name) + \\ h1.title= name + \\ form + \\ //- unbuffered comment :) + \\ // An example of attributes. + \\ input(type='text' name='user[name]' value=name) + \\ input(checked, type='checkbox', name='user[blocked]') + \\ input(type='submit', value='Update') + , + }, + .{ + .name = "code.pug", + .source = + \\- var title = "Things" + \\ + \\- + \\ var subtitle = ["Really", "long", + \\ "list", "of", + \\ "words"] + \\h1= title + \\h2= subtitle.join(" ") + \\ + \\ul#users + \\ each user, name in users + \\ // expands to if (user.isA == 'ferret') + \\ if user.isA == 'ferret' + \\ li(class='user-' + name) #{name} is just a ferret + \\ else + \\ li(class='user-' + name) #{name} #{user.email} + , + }, + .{ + .name = "dynamicscript.pug", + .source = + \\html + \\ head + \\ title Dynamic Inline JavaScript + \\ script. + \\ var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")} + , + }, + .{ + .name = "each.pug", + .source = + \\ul#users + \\ each user, name in users + \\ li(class='user-' + name) #{name} #{user.email} + , + }, + .{ + .name = "extend-layout.pug", + .source = + \\html + \\ head + \\ h1 My Site - #{title} + \\ block scripts + \\ script(src='/jquery.js') + \\ body + \\ block content + \\ block foot + \\ #footer + \\ p some footer content + , + }, + .{ + .name = "form.pug", + .source = + \\form(method="post") + \\ fieldset + \\ legend General + \\ p + \\ label(for="user[name]") Username: + \\ input(type="text", name="user[name]", value=user.name) + \\ p + \\ label(for="user[email]") Email: + \\ input(type="text", name="user[email]", value=user.email) + \\ .tip. + \\ Enter a valid + \\ email address + \\ such as <em>tj@vision-media.ca</em>. + \\ fieldset + \\ legend Location + \\ p + \\ label(for="user[city]") City: + \\ input(type="text", name="user[city]", value=user.city) + \\ p + \\ select(name="user[province]") + \\ option(value="") -- Select Province -- + \\ option(value="AB") Alberta + \\ option(value="BC") British Columbia + \\ option(value="SK") Saskatchewan + \\ option(value="MB") Manitoba + \\ option(value="ON") Ontario + \\ option(value="QC") Quebec + \\ p.buttons + \\ input(type="submit", value="Save") + , + }, + .{ + .name = "layout.pug", + .source = + \\doctype html + \\html(lang="en") + \\ head + \\ title Example + \\ script. + \\ if (foo) { + \\ bar(); + \\ } + \\ body + \\ h1 Pug - node template engine + \\ #container + , + }, + .{ + .name = "pet.pug", + .source = + \\.pet + \\ h2= pet.name + \\ p #{pet.name} is <em>#{pet.age}</em> year(s) old. + , + }, + .{ + .name = "rss.pug", + .source = + \\doctype xml + \\rss(version='2.0') + \\channel + \\ title RSS Title + \\ description Some description here + \\ link http://google.com + \\ lastBuildDate Mon, 06 Sep 2010 00:01:00 +0000 + \\ pubDate Mon, 06 Sep 2009 16:45:00 +0000 + \\ + \\ each item in items + \\ item + \\ title= item.title + \\ description= item.description + \\ link= item.link + , + }, + .{ + .name = "text.pug", + .source = + \\| An example of an + \\a(href='#') inline + \\| link. + \\ + \\form + \\ label Username: + \\ input(type='text', name='user[name]') + \\ p + \\ | Just an example of some text usage. + \\ | You can have <em>inline</em> html, + \\ | as well as + \\ strong tags + \\ | . + \\ + \\ | Interpolation is also supported. The + \\ | username is currently "#{name}". + \\ + \\ label Email: + \\ input(type='text', name='user[email]') + \\ p + \\ | Email is currently + \\ em= email + \\ | . + , + }, + .{ + .name = "whitespace.pug", + .source = + \\- var js = '<script></script>' + \\doctype html + \\html + \\ + \\ head + \\ title= "Some " + "JavaScript" + \\ != js + \\ + \\ + \\ + \\ body + , + }, +}; + +pub fn main() !void { + const allocator = std.heap.page_allocator; + + std.debug.print("=== Zig Pugz Example Benchmark ===\n\n", .{}); + + var passed: usize = 0; + var failed: usize = 0; + var total_time_ns: u64 = 0; + var html_outputs: [examples.len]?[]const u8 = undefined; + for (&html_outputs) |*h| h.* = null; + + for (examples, 0..) |example, idx| { + const iterations: usize = 100; + var success = false; + var time_ns: u64 = 0; + + // Warmup + for (0..5) |_| { + var result = pug.compile(allocator, example.source, .{}) catch continue; + result.deinit(allocator); + } + + // Benchmark + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + var result = pug.compile(allocator, example.source, .{}) catch break; + if (i == iterations - 1) { + // Keep last HTML for output + html_outputs[idx] = result.html; + } else { + result.deinit(allocator); + } + success = true; + } + time_ns = timer.read(); + + if (success and i == iterations) { + const time_ms = @as(f64, @floatFromInt(time_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations)); + std.debug.print("{s}: OK ({d:.3} ms)\n", .{ example.name, time_ms }); + passed += 1; + total_time_ns += time_ns; + } else { + std.debug.print("{s}: FAILED\n", .{example.name}); + failed += 1; + } + } + + std.debug.print("\n=== Summary ===\n", .{}); + std.debug.print("Passed: {d}/{d}\n", .{ passed, examples.len }); + std.debug.print("Failed: {d}/{d}\n", .{ failed, examples.len }); + + if (passed > 0) { + const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0; + std.debug.print("Total time (successful): {d:.3} ms\n", .{total_ms}); + std.debug.print("Average time: {d:.3} ms\n", .{total_ms / @as(f64, @floatFromInt(passed))}); + } + + // Output HTML for comparison + std.debug.print("\n=== HTML Output ===\n", .{}); + for (examples, 0..) |example, idx| { + if (html_outputs[idx]) |html| { + std.debug.print("\n--- {s} ---\n", .{example.name}); + const max_len = @min(html.len, 500); + std.debug.print("{s}", .{html[0..max_len]}); + if (html.len > 500) std.debug.print("...", .{}); + std.debug.print("\n", .{}); + } + } +} diff --git a/src/playground/examples/attributes.pug b/src/playground/examples/attributes.pug new file mode 100644 index 0000000..b8baa5f --- /dev/null +++ b/src/playground/examples/attributes.pug @@ -0,0 +1,8 @@ +div#id.left.container(class='user user-' + name) + h1.title= name + form + //- unbuffered comment :) + // An example of attributes. + input(type='text' name='user[name]' value=name) + input(checked, type='checkbox', name='user[blocked]') + input(type='submit', value='Update') \ No newline at end of file diff --git a/src/playground/examples/code.pug b/src/playground/examples/code.pug new file mode 100644 index 0000000..d0cab66 --- /dev/null +++ b/src/playground/examples/code.pug @@ -0,0 +1,17 @@ + +- var title = "Things" + +- + var subtitle = ["Really", "long", + "list", "of", + "words"] +h1= title +h2= subtitle.join(" ") + +ul#users + each user, name in users + // expands to if (user.isA == 'ferret') + if user.isA == 'ferret' + li(class='user-' + name) #{name} is just a ferret + else + li(class='user-' + name) #{name} #{user.email} \ No newline at end of file diff --git a/src/playground/examples/dynamicscript.pug b/src/playground/examples/dynamicscript.pug new file mode 100644 index 0000000..4e1c895 --- /dev/null +++ b/src/playground/examples/dynamicscript.pug @@ -0,0 +1,5 @@ +html + head + title Dynamic Inline JavaScript + script. + var users = !{JSON.stringify(users).replace(/<\//g, "<\\/")} diff --git a/src/playground/examples/each.pug b/src/playground/examples/each.pug new file mode 100644 index 0000000..206c740 --- /dev/null +++ b/src/playground/examples/each.pug @@ -0,0 +1,3 @@ +ul#users + each user, name in users + li(class='user-' + name) #{name} #{user.email} \ No newline at end of file diff --git a/src/playground/examples/extend-layout.pug b/src/playground/examples/extend-layout.pug new file mode 100644 index 0000000..0767f5f --- /dev/null +++ b/src/playground/examples/extend-layout.pug @@ -0,0 +1,10 @@ +html + head + h1 My Site - #{title} + block scripts + script(src='/jquery.js') + body + block content + block foot + #footer + p some footer content \ No newline at end of file diff --git a/src/playground/examples/extend.pug b/src/playground/examples/extend.pug new file mode 100644 index 0000000..0de55bd --- /dev/null +++ b/src/playground/examples/extend.pug @@ -0,0 +1,11 @@ + +extends extend-layout.pug + +block scripts + script(src='/jquery.js') + script(src='/pets.js') + +block content + h1= title + each pet in pets + include pet.pug diff --git a/src/playground/examples/form.pug b/src/playground/examples/form.pug new file mode 100644 index 0000000..afe3249 --- /dev/null +++ b/src/playground/examples/form.pug @@ -0,0 +1,29 @@ +form(method="post") + fieldset + legend General + p + label(for="user[name]") Username: + input(type="text", name="user[name]", value=user.name) + p + label(for="user[email]") Email: + input(type="text", name="user[email]", value=user.email) + .tip. + Enter a valid + email address + such as <em>tj@vision-media.ca</em>. + fieldset + legend Location + p + label(for="user[city]") City: + input(type="text", name="user[city]", value=user.city) + p + select(name="user[province]") + option(value="") -- Select Province -- + option(value="AB") Alberta + option(value="BC") British Columbia + option(value="SK") Saskatchewan + option(value="MB") Manitoba + option(value="ON") Ontario + option(value="QC") Quebec + p.buttons + input(type="submit", value="Save") \ No newline at end of file diff --git a/src/playground/examples/includes.pug b/src/playground/examples/includes.pug new file mode 100644 index 0000000..470c476 --- /dev/null +++ b/src/playground/examples/includes.pug @@ -0,0 +1,7 @@ + +html + include includes/head.pug + body + h1 My Site + p Welcome to my super lame site. + include includes/foot.pug diff --git a/src/playground/examples/layout.pug b/src/playground/examples/layout.pug new file mode 100644 index 0000000..767f99a --- /dev/null +++ b/src/playground/examples/layout.pug @@ -0,0 +1,14 @@ +doctype html +html(lang="en") + head + title Example + script. + if (foo) { + bar(); + } + body + h1 Pug - node template engine + #container + :markdown-it + Pug is a _high performance_ template engine for [node](http://nodejs.org), + inspired by [haml](http://haml-lang.com/), and written by [TJ Holowaychuk](http://github.com/visionmedia). diff --git a/src/playground/examples/mixins.pug b/src/playground/examples/mixins.pug new file mode 100644 index 0000000..09f00fd --- /dev/null +++ b/src/playground/examples/mixins.pug @@ -0,0 +1,14 @@ +include mixins/dialog.pug +include mixins/profile.pug + +.one + +dialog + +.two + +dialog-title('Whoop') + +.three + +dialog-title-desc('Whoop', 'Just a mixin') + +#profile + +profile(user) diff --git a/src/playground/examples/pet.pug b/src/playground/examples/pet.pug new file mode 100644 index 0000000..e5dbab9 --- /dev/null +++ b/src/playground/examples/pet.pug @@ -0,0 +1,3 @@ +.pet + h2= pet.name + p #{pet.name} is <em>#{pet.age}</em> year(s) old. \ No newline at end of file diff --git a/src/playground/examples/rss.pug b/src/playground/examples/rss.pug new file mode 100644 index 0000000..165dffb --- /dev/null +++ b/src/playground/examples/rss.pug @@ -0,0 +1,14 @@ +doctype xml +rss(version='2.0') +channel + title RSS Title + description Some description here + link http://google.com + lastBuildDate Mon, 06 Sep 2010 00:01:00 +0000 + pubDate Mon, 06 Sep 2009 16:45:00 +0000 + + each item in items + item + title= item.title + description= item.description + link= item.link diff --git a/src/playground/examples/text.pug b/src/playground/examples/text.pug new file mode 100644 index 0000000..6e99a89 --- /dev/null +++ b/src/playground/examples/text.pug @@ -0,0 +1,36 @@ +| An example of an +a(href='#') inline +| link. + +form + label Username: + input(type='text', name='user[name]') + p + | Just an example of some text usage. + | You can have <em>inline</em> html, + | as well as + strong tags + | . + + | Interpolation is also supported. The + | username is currently "#{name}". + + label Email: + input(type='text', name='user[email]') + p + | Email is currently + em= email + | . + + // alternatively, if we plan on having only + // text or inline-html, we can use a trailing + // "." to let pug know we want to omit pipes + + label Username: + input(type='text') + p. + Just an example, like before + however now we can omit those + annoying pipes!. + + Wahoo. \ No newline at end of file diff --git a/src/playground/examples/whitespace.pug b/src/playground/examples/whitespace.pug new file mode 100644 index 0000000..ae7ebd9 --- /dev/null +++ b/src/playground/examples/whitespace.pug @@ -0,0 +1,11 @@ +- var js = '<script></script>' +doctype html +html + + head + title= "Some " + "JavaScript" + != js + + + + body \ No newline at end of file diff --git a/src/playground/run_js.js b/src/playground/run_js.js new file mode 100644 index 0000000..f1c6819 --- /dev/null +++ b/src/playground/run_js.js @@ -0,0 +1,70 @@ +/** + * JS Pug - Process all .pug files in playground folder + */ + +const fs = require('fs'); +const path = require('path'); +const pug = require('../../pug'); + +const dir = path.join(__dirname, 'examples'); + +// Get all .pug files +const pugFiles = fs.readdirSync(dir) + .filter(f => f.endsWith('.pug')) + .sort(); + +console.log('=== JS Pug Playground ===\n'); +console.log(`Found ${pugFiles.length} .pug files\n`); + +let passed = 0; +let failed = 0; +let totalTimeMs = 0; + +for (const file of pugFiles) { + const filePath = path.join(dir, file); + const source = fs.readFileSync(filePath, 'utf8'); + + const iterations = 100; + let success = false; + let html = ''; + let error = ''; + let timeMs = 0; + + try { + const start = process.hrtime.bigint(); + + for (let i = 0; i < iterations; i++) { + html = pug.render(source, { + filename: filePath, + basedir: dir + }); + } + + const end = process.hrtime.bigint(); + timeMs = Number(end - start) / 1_000_000 / iterations; + success = true; + passed++; + totalTimeMs += timeMs; + } catch (e) { + error = e.message.split('\n')[0]; + failed++; + } + + if (success) { + console.log(`✓ ${file} (${timeMs.toFixed(3)} ms)`); + // Show first 200 chars of output + const preview = html.replace(/\s+/g, ' ').substring(0, 200); + console.log(` → ${preview}${html.length > 200 ? '...' : ''}\n`); + } else { + console.log(`✗ ${file}`); + console.log(` → ${error}\n`); + } +} + +console.log('=== Summary ==='); +console.log(`Passed: ${passed}/${pugFiles.length}`); +console.log(`Failed: ${failed}/${pugFiles.length}`); +if (passed > 0) { + console.log(`Total time: ${totalTimeMs.toFixed(3)} ms`); + console.log(`Average: ${(totalTimeMs / passed).toFixed(3)} ms per file`); +} diff --git a/src/playground/run_zig b/src/playground/run_zig new file mode 100755 index 0000000..e69de29 diff --git a/src/playground/run_zig.zig b/src/playground/run_zig.zig new file mode 100644 index 0000000..c4d73cb --- /dev/null +++ b/src/playground/run_zig.zig @@ -0,0 +1,120 @@ +// Zig Pugz - Process all .pug files in playground/examples folder + +const std = @import("std"); +const pug = @import("../pug.zig"); +const fs = std.fs; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== Zig Pugz Playground ===\n\n", .{}); + + // Open the examples directory relative to cwd + var dir = fs.cwd().openDir("playground/examples", .{ .iterate = true }) catch |err| { + // Try from playground directory + dir = fs.cwd().openDir("examples", .{ .iterate = true }) catch { + std.debug.print("Error opening examples directory: {}\n", .{err}); + return; + }; + }; + defer dir.close(); + + // Collect .pug files + var files = std.ArrayList([]const u8).init(allocator); + defer { + for (files.items) |f| allocator.free(f); + files.deinit(); + } + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".pug")) { + const name = try allocator.dupe(u8, entry.name); + try files.append(name); + } + } + + // Sort files + std.mem.sort([]const u8, files.items, {}, struct { + fn lessThan(_: void, a: []const u8, b: []const u8) bool { + return std.mem.lessThan(u8, a, b); + } + }.lessThan); + + std.debug.print("Found {d} .pug files\n\n", .{files.items.len}); + + var passed: usize = 0; + var failed: usize = 0; + var total_time_ns: u64 = 0; + + for (files.items) |filename| { + // Read file + const file = dir.openFile(filename, .{}) catch { + std.debug.print("✗ {s}\n → Could not open file\n\n", .{filename}); + failed += 1; + continue; + }; + defer file.close(); + + const source = file.readToEndAlloc(allocator, 1024 * 1024) catch { + std.debug.print("✗ {s}\n → Could not read file\n\n", .{filename}); + failed += 1; + continue; + }; + defer allocator.free(source); + + // Benchmark + const iterations: usize = 100; + var success = false; + var last_html: ?[]const u8 = null; + + // Warmup + for (0..5) |_| { + var result = pug.compile(allocator, source, .{}) catch continue; + result.deinit(allocator); + } + + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + var result = pug.compile(allocator, source, .{}) catch break; + if (i == iterations - 1) { + last_html = result.html; + } else { + result.deinit(allocator); + } + success = true; + } + const elapsed_ns = timer.read(); + + if (success and i == iterations) { + const time_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations)); + std.debug.print("✓ {s} ({d:.3} ms)\n", .{ filename, time_ms }); + + // Show preview + if (last_html) |html| { + const max_len = @min(html.len, 200); + std.debug.print(" → {s}{s}\n\n", .{ html[0..max_len], if (html.len > 200) "..." else "" }); + allocator.free(html); + } + + passed += 1; + total_time_ns += elapsed_ns; + } else { + std.debug.print("✗ {s}\n → Compilation failed\n\n", .{filename}); + failed += 1; + } + } + + std.debug.print("=== Summary ===\n", .{}); + std.debug.print("Passed: {d}/{d}\n", .{ passed, files.items.len }); + std.debug.print("Failed: {d}/{d}\n", .{ failed, files.items.len }); + + if (passed > 0) { + const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0; + std.debug.print("Total time: {d:.3} ms\n", .{total_ms}); + std.debug.print("Average: {d:.3} ms per file\n", .{total_ms / @as(f64, @floatFromInt(passed))}); + } +} diff --git a/src/pug.zig b/src/pug.zig new file mode 100644 index 0000000..8d9704d --- /dev/null +++ b/src/pug.zig @@ -0,0 +1,457 @@ +// 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("<div></div>", 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("<p>Hello</p>", 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("<div><span>Hello</span></div>", 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("<br>", 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, "<!DOCTYPE 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, "<div>") != 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, "<!--") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "visible") != null); +} + +test "render - convenience function" { + const allocator = std.testing.allocator; + + const html = try render(allocator, "p test"); + defer allocator.free(html); + + try std.testing.expectEqualStrings("<p>test</p>", html); +} + +test "compile - multiple tags" { + const allocator = std.testing.allocator; + + var result = try compile(allocator, "p First\np Second", .{}); + defer result.deinit(allocator); + + try std.testing.expectEqualStrings("<p>First</p><p>Second</p>", result.html); +} + +test "compile - interpolation text" { + const allocator = std.testing.allocator; + + var result = try compile(allocator, "p Hello, World!", .{}); + defer result.deinit(allocator); + + try std.testing.expectEqualStrings("<p>Hello, World!</p>", result.html); +} + +test "compile - multiple classes" { + const allocator = std.testing.allocator; + + var result = try compile(allocator, "div.foo.bar", .{}); + defer result.deinit(allocator); + + try std.testing.expect(mem.indexOf(u8, result.html, "class=\"") != null); +} + +test "compile - class and id" { + const allocator = std.testing.allocator; + + var result = try compile(allocator, "div#main.container", .{}); + defer result.deinit(allocator); + + // Parser stores values with quotes, check that both id and class are present + try std.testing.expect(mem.indexOf(u8, result.html, "id=") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "main") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "class=") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "container") != null); +} + +test "compile - deeply nested" { + const allocator = std.testing.allocator; + + var result = try compile(allocator, + \\html + \\ head + \\ title Test + \\ body + \\ div Hello + , .{}); + defer result.deinit(allocator); + + try std.testing.expect(mem.indexOf(u8, result.html, "<html>") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "<head>") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "<title>Test") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "") != null); + try std.testing.expect(mem.indexOf(u8, result.html, "
Hello
") != null); +} diff --git a/src/root.zig b/src/root.zig index eb40fd9..ec6062e 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,69 +1,23 @@ -//! Pugz - A Pug-like HTML template engine written in Zig. -//! -//! Pugz provides a clean, indentation-based syntax for writing HTML templates, -//! inspired by Pug (formerly Jade). It supports: -//! - Indentation-based nesting -//! - Tag, class, and ID shorthand syntax -//! - Attributes and text interpolation -//! - Control flow (if/else, each, while) -//! - Mixins and template inheritance -//! -//! ## Quick Start (Server Usage) -//! -//! ```zig -//! const pugz = @import("pugz"); -//! -//! // Initialize view engine once at startup -//! var engine = try pugz.ViewEngine.init(allocator, .{ -//! .views_dir = "src/views", -//! }); -//! defer engine.deinit(); -//! -//! // Render templates (use arena allocator per request) -//! var arena = std.heap.ArenaAllocator.init(allocator); -//! defer arena.deinit(); -//! -//! const html = try engine.render(arena.allocator(), "pages/home", .{ -//! .title = "Home", -//! }); -//! ``` +// Pugz - A Pug-like HTML template engine written in Zig +// +// Quick Start: +// const pugz = @import("pugz"); +// const engine = pugz.ViewEngine.init(.{ .views_dir = "views" }); +// const html = try engine.render(allocator, "index", .{ .title = "Home" }); -pub const lexer = @import("lexer.zig"); -pub const ast = @import("ast.zig"); -pub const parser = @import("parser.zig"); -pub const codegen = @import("codegen.zig"); -pub const runtime = @import("runtime.zig"); +pub const pug = @import("pug.zig"); pub const view_engine = @import("view_engine.zig"); -pub const diagnostic = @import("diagnostic.zig"); +pub const template = @import("template.zig"); -// Re-export main types for convenience -pub const Lexer = lexer.Lexer; -pub const Token = lexer.Token; -pub const TokenType = lexer.TokenType; - -pub const Parser = parser.Parser; -pub const Node = ast.Node; -pub const Document = ast.Document; - -pub const CodeGen = codegen.CodeGen; -pub const generate = codegen.generate; - -pub const Runtime = runtime.Runtime; -pub const Context = runtime.Context; -pub const Value = runtime.Value; -pub const render = runtime.render; -pub const renderWithOptions = runtime.renderWithOptions; -pub const RenderOptions = runtime.RenderOptions; -pub const renderTemplate = runtime.renderTemplate; - -// High-level API +// Re-export main types pub const ViewEngine = view_engine.ViewEngine; -pub const CompiledTemplate = view_engine.CompiledTemplate; +pub const compile = pug.compile; +pub const compileFile = pug.compileFile; +pub const render = pug.render; +pub const renderFile = pug.renderFile; +pub const CompileOptions = pug.CompileOptions; +pub const CompileResult = pug.CompileResult; +pub const CompileError = pug.CompileError; -// Build-time template compilation -pub const build_templates = @import("build_templates.zig"); -pub const compileTemplates = build_templates.compileTemplates; - -test { - _ = @import("std").testing.refAllDecls(@This()); -} +// Convenience function for inline templates with data +pub const renderTemplate = template.renderWithData; diff --git a/src/run_playground.zig b/src/run_playground.zig new file mode 100644 index 0000000..0c3e7ef --- /dev/null +++ b/src/run_playground.zig @@ -0,0 +1,118 @@ +// Zig Pugz - Process all .pug files in playground/examples folder + +const std = @import("std"); +const pug = @import("pug.zig"); +const fs = std.fs; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== Zig Pugz Playground ===\n\n", .{}); + + // Open the examples directory + var dir = fs.cwd().openDir("playground/examples", .{ .iterate = true }) catch { + std.debug.print("Error: Could not open playground/examples directory\n", .{}); + std.debug.print("Run from packages/pugz/ directory\n", .{}); + return; + }; + defer dir.close(); + + // Collect .pug files + var files = std.ArrayListUnmanaged([]const u8){}; + defer { + for (files.items) |f| allocator.free(f); + files.deinit(allocator); + } + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".pug")) { + const name = try allocator.dupe(u8, entry.name); + try files.append(allocator, name); + } + } + + // Sort files + std.mem.sort([]const u8, files.items, {}, struct { + fn lessThan(_: void, a: []const u8, b: []const u8) bool { + return std.mem.lessThan(u8, a, b); + } + }.lessThan); + + std.debug.print("Found {d} .pug files\n\n", .{files.items.len}); + + var passed: usize = 0; + var failed: usize = 0; + var total_time_ns: u64 = 0; + + for (files.items) |filename| { + // Read file + const file = dir.openFile(filename, .{}) catch { + std.debug.print("x {s}\n -> Could not open file\n\n", .{filename}); + failed += 1; + continue; + }; + defer file.close(); + + const source = file.readToEndAlloc(allocator, 1024 * 1024) catch { + std.debug.print("x {s}\n -> Could not read file\n\n", .{filename}); + failed += 1; + continue; + }; + defer allocator.free(source); + + // Benchmark + const iterations: usize = 100; + var success = false; + var last_html: ?[]const u8 = null; + + // Warmup + for (0..5) |_| { + var result = pug.compile(allocator, source, .{}) catch continue; + result.deinit(allocator); + } + + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + var result = pug.compile(allocator, source, .{}) catch break; + if (i == iterations - 1) { + last_html = result.html; + } else { + result.deinit(allocator); + } + success = true; + } + const elapsed_ns = timer.read(); + + if (success and i == iterations) { + const time_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0 / @as(f64, @floatFromInt(iterations)); + std.debug.print("OK {s} ({d:.3} ms)\n", .{ filename, time_ms }); + + // Show preview + if (last_html) |html| { + const max_len = @min(html.len, 200); + std.debug.print(" -> {s}{s}\n\n", .{ html[0..max_len], if (html.len > 200) "..." else "" }); + allocator.free(html); + } + + passed += 1; + total_time_ns += elapsed_ns; + } else { + std.debug.print("FAIL {s}\n -> Compilation failed\n\n", .{filename}); + failed += 1; + } + } + + std.debug.print("=== Summary ===\n", .{}); + std.debug.print("Passed: {d}/{d}\n", .{ passed, files.items.len }); + std.debug.print("Failed: {d}/{d}\n", .{ failed, files.items.len }); + + if (passed > 0) { + const total_ms = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000.0 / 100.0; + std.debug.print("Total time: {d:.3} ms\n", .{total_ms}); + std.debug.print("Average: {d:.3} ms per file\n", .{total_ms / @as(f64, @floatFromInt(passed))}); + } +} diff --git a/src/runtime.zig b/src/runtime.zig index be58d0a..9f9d931 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -1,2490 +1,1504 @@ -//! Pugz Runtime - Evaluates templates with data context. -//! -//! The runtime takes a parsed AST and a data context, then produces -//! the final HTML output by: -//! - Substituting variables in interpolations -//! - Evaluating conditionals -//! - Iterating over collections -//! - Calling mixins -//! - Template inheritance (extends/block) -//! - Includes -//! -//! **Memory Management**: Use an arena allocator for best performance and -//! automatic cleanup. The runtime allocates intermediate strings during -//! template processing that are cleaned up when the arena is reset/deinitialized. -//! -//! ```zig -//! var arena = std.heap.ArenaAllocator.init(gpa.allocator()); -//! defer arena.deinit(); -//! -//! const html = try engine.renderTpl(arena.allocator(), template, data); -//! // Use html... arena.deinit() frees everything -//! ``` - const std = @import("std"); -const ast = @import("ast.zig"); -const Lexer = @import("lexer.zig").Lexer; -const Parser = @import("parser.zig").Parser; +const mem = std.mem; +const Allocator = std.mem.Allocator; +const ArrayListUnmanaged = std.ArrayListUnmanaged; -const log = std.log.scoped(.@"pugz/runtime"); +// ============================================================================ +// Pug Runtime - HTML generation utilities +// ============================================================================ -/// A value in the template context. -pub const Value = union(enum) { - /// Null/undefined value. - null, - /// Boolean value. - bool: bool, - /// Integer value. - int: i64, - /// Floating point value. - float: f64, - /// String value. +/// Escape HTML special characters in a string. +/// Characters escaped: " & < > +pub fn escape(allocator: Allocator, html: []const u8) ![]const u8 { + // Quick check if escaping is needed + var needs_escape = false; + for (html) |c| { + if (c == '"' or c == '&' or c == '<' or c == '>') { + needs_escape = true; + break; + } + } + + if (!needs_escape) { + return try allocator.dupe(u8, html); + } + + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + + for (html) |c| { + switch (c) { + '"' => try result.appendSlice(allocator, """), + '&' => try result.appendSlice(allocator, "&"), + '<' => try result.appendSlice(allocator, "<"), + '>' => try result.appendSlice(allocator, ">"), + else => try result.append(allocator, c), + } + } + + return try result.toOwnedSlice(allocator); +} + +/// Style value types +pub const StyleValue = union(enum) { string: []const u8, - /// Array of values. - array: []const Value, - /// Object/map of string keys to values. - object: std.StringHashMapUnmanaged(Value), - - /// Returns the value as a string for output. - /// For integers, uses pre-computed strings for small values to avoid allocation. - pub fn toString(self: Value, allocator: std.mem.Allocator) ![]const u8 { - // Fast path: strings are most common in templates (branch hint) - if (self == .string) { - @branchHint(.likely); - return self.string; - } - return switch (self) { - .string => unreachable, // handled above - .null => "", - .bool => |b| if (b) "true" else "false", - .int => |i| blk: { - // Fast path for common small integers (0-99) - if (i >= 0 and i < 100) { - break :blk small_int_strings[@intCast(i)]; - } - // Allocate for larger integers - break :blk try std.fmt.allocPrint(allocator, "{d}", .{i}); - }, - .float => |f| try std.fmt.allocPrint(allocator, "{d}", .{f}), - .array => "[Array]", - .object => "[Object]", - }; - } - - /// Pre-computed strings for small integers 0-99 (common in loops) - const small_int_strings = [_][]const u8{ - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", - "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", - "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", - "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", - "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", - "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", - "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", - "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", - "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", - "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", - }; - - /// Returns the value as a boolean for conditionals. - pub fn isTruthy(self: Value) bool { - return switch (self) { - .null => false, - .bool => |b| b, - .int => |i| i != 0, - .float => |f| f != 0.0, - .string => |s| s.len > 0, - .array => |a| a.len > 0, - .object => true, - }; - } - - /// Creates a string value. - pub fn str(s: []const u8) Value { - return .{ .string = s }; - } - - /// Creates an integer value. - pub fn integer(i: i64) Value { - return .{ .int = i }; - } - - /// Creates a boolean value. - pub fn boolean(b: bool) Value { - return .{ .bool = b }; - } + object: []const StyleProperty, + none, }; -/// Runtime errors. -pub const RuntimeError = error{ - OutOfMemory, - UndefinedVariable, - TypeError, - InvalidExpression, - ParseError, - /// Template include/extends depth exceeded maximum (prevents infinite recursion) - MaxIncludeDepthExceeded, - /// Template path attempts to escape base directory (security violation) - PathTraversalDetected, -}; - -/// Template rendering context with variable scopes. -pub const Context = struct { - allocator: std.mem.Allocator, - /// Stack of variable scopes (innermost last). - /// We keep all scopes allocated and track active depth with scope_depth. - scopes: std.ArrayList(std.StringHashMapUnmanaged(Value)), - /// Current active scope depth (scopes[0..scope_depth] are active). - scope_depth: usize, - /// Mixin definitions available in this context. - mixins: std.StringHashMapUnmanaged(ast.MixinDef), - - pub fn init(allocator: std.mem.Allocator) Context { - return .{ - .allocator = allocator, - .scopes = .empty, - .scope_depth = 0, - .mixins = .empty, - }; - } - - pub fn deinit(self: *Context) void { - for (self.scopes.items) |*scope| { - scope.*.deinit(self.allocator); - } - self.scopes.deinit(self.allocator); - self.mixins.deinit(self.allocator); - } - - /// Pushes a new scope onto the stack. - /// Reuses previously allocated scopes when possible to avoid allocation overhead. - pub fn pushScope(self: *Context) !void { - if (self.scope_depth < self.scopes.items.len) { - // Reuse existing scope slot (already cleared on pop) - } else { - // Need to allocate a new scope - try self.scopes.append(self.allocator, .empty); - } - self.scope_depth += 1; - } - - /// Pops the current scope from the stack. - /// Clears scope for reuse but does NOT deallocate. - pub fn popScope(self: *Context) void { - if (self.scope_depth > 0) { - self.scope_depth -= 1; - // Clear the scope so old values don't leak into next use - self.scopes.items[self.scope_depth].clearRetainingCapacity(); - } - } - - /// Sets a variable in the current scope. - pub fn set(self: *Context, name: []const u8, value: Value) !void { - if (self.scope_depth == 0) { - try self.pushScope(); - } - const current = &self.scopes.items[self.scope_depth - 1]; - try current.put(self.allocator, name, value); - } - - /// Gets or creates a slot for a variable, returning a pointer to the value. - /// Use this for loop variables that are updated repeatedly. - pub fn getOrPutPtr(self: *Context, name: []const u8) !*Value { - if (self.scope_depth == 0) { - try self.pushScope(); - } - const current = &self.scopes.items[self.scope_depth - 1]; - const gop = try current.getOrPut(self.allocator, name); - if (!gop.found_existing) { - gop.value_ptr.* = Value.null; - } - return gop.value_ptr; - } - - /// Gets a variable, searching from innermost to outermost scope. - pub fn get(self: *Context, name: []const u8) ?Value { - // Fast path: most lookups are in the innermost scope - if (self.scope_depth > 0) { - @branchHint(.likely); - if (self.scopes.items[self.scope_depth - 1].get(name)) |value| { - return value; - } - } - // Search remaining scopes (less common) - var i = self.scope_depth -| 1; - while (i > 0) { - i -= 1; - if (self.scopes.items[i].get(name)) |value| { - return value; - } - } - return null; - } - - /// Registers a mixin definition. - pub fn defineMixin(self: *Context, mixin: ast.MixinDef) !void { - try self.mixins.put(self.allocator, mixin.name, mixin); - } - - /// Gets a mixin definition by name. - pub fn getMixin(self: *Context, name: []const u8) ?ast.MixinDef { - return self.mixins.get(name); - } -}; - -/// File resolver function type for loading templates. -/// Takes a path and returns the file contents, or null if not found. -pub const FileResolver = *const fn (allocator: std.mem.Allocator, path: []const u8) ?[]const u8; - -/// Block definition collected from child templates. -const BlockDef = struct { +pub const StyleProperty = struct { name: []const u8, - mode: ast.Block.Mode, - children: []const ast.Node, + value: []const u8, }; -/// Runtime engine for evaluating templates. -pub const Runtime = struct { - allocator: std.mem.Allocator, - context: *Context, - output: std.ArrayList(u8), +/// Convert a style value to a CSS string. +/// If val is an object, formats as "key:value;key:value;" +/// If val is a string, returns it as-is. +pub fn style(allocator: Allocator, val: StyleValue) ![]const u8 { + switch (val) { + .none => return try allocator.dupe(u8, ""), + .string => |s| { + if (s.len == 0) return try allocator.dupe(u8, ""); + return try allocator.dupe(u8, s); + }, + .object => |props| { + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + + for (props) |prop| { + try result.appendSlice(allocator, prop.name); + try result.append(allocator, ':'); + try result.appendSlice(allocator, prop.value); + try result.append(allocator, ';'); + } + + return try result.toOwnedSlice(allocator); + }, + } +} + +/// Attribute value types +pub const AttrValue = union(enum) { + string: []const u8, + boolean: bool, + number: i64, + none, // null/undefined equivalent +}; + +/// Render a single HTML attribute. +/// Returns empty string for false/null values. +/// For true values, returns terse form " key" or full form " key="key"". +pub fn attr(allocator: Allocator, key: []const u8, val: AttrValue, escaped: bool, terse: bool) ![]const u8 { + switch (val) { + .none => return try allocator.dupe(u8, ""), + .boolean => |b| { + if (!b) return try allocator.dupe(u8, ""); + // true value + if (terse) { + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + try result.append(allocator, ' '); + try result.appendSlice(allocator, key); + return try result.toOwnedSlice(allocator); + } else { + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + try result.append(allocator, ' '); + try result.appendSlice(allocator, key); + try result.appendSlice(allocator, "=\""); + try result.appendSlice(allocator, key); + try result.append(allocator, '"'); + return try result.toOwnedSlice(allocator); + } + }, + .number => |n| { + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + try result.append(allocator, ' '); + try result.appendSlice(allocator, key); + try result.appendSlice(allocator, "=\""); + + // Format number + var buf: [32]u8 = undefined; + const num_str = std.fmt.bufPrint(&buf, "{d}", .{n}) catch return error.FormatError; + try result.appendSlice(allocator, num_str); + + try result.append(allocator, '"'); + return try result.toOwnedSlice(allocator); + }, + .string => |s| { + // Empty class or style returns empty + if (s.len == 0 and (mem.eql(u8, key, "class") or mem.eql(u8, key, "style"))) { + return try allocator.dupe(u8, ""); + } + + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + + try result.append(allocator, ' '); + try result.appendSlice(allocator, key); + try result.appendSlice(allocator, "=\""); + + if (escaped) { + const escaped_val = try escape(allocator, s); + defer allocator.free(escaped_val); + try result.appendSlice(allocator, escaped_val); + } else { + try result.appendSlice(allocator, s); + } + + try result.append(allocator, '"'); + return try result.toOwnedSlice(allocator); + }, + } +} + +/// Class value types for the classes function +pub const ClassValue = union(enum) { + string: []const u8, + array: []const ClassValue, + object: []const ClassCondition, + none, +}; + +pub const ClassCondition = struct { + name: []const u8, + condition: bool, +}; + +/// Process class values into a space-delimited string. +/// Arrays are flattened, objects include keys with truthy values. +/// Optimized to minimize allocations by writing directly to result buffer. +pub fn classes(allocator: Allocator, val: ClassValue, escaping: ?[]const bool) ![]const u8 { + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + + try classesInternal(allocator, val, escaping, &result, 0); + + if (result.items.len == 0) { + result.deinit(allocator); + return try allocator.dupe(u8, ""); + } + + return try result.toOwnedSlice(allocator); +} + +/// Internal recursive helper that writes directly to result buffer (avoids intermediate allocations) +fn classesInternal( + allocator: Allocator, + val: ClassValue, + escaping: ?[]const bool, + result: *ArrayListUnmanaged(u8), depth: usize, - options: Options, - /// File resolver for loading external templates. - file_resolver: ?FileResolver, - /// Base directory for resolving relative paths. - base_dir: []const u8, - /// Directory containing mixin files for lazy-loading. - mixins_dir: []const u8, - /// Block definitions from child template (for inheritance). - blocks: std.StringHashMapUnmanaged(BlockDef), - /// Current mixin block content (for `block` keyword inside mixins). - mixin_block_content: ?[]const ast.Node, - /// Current mixin attributes (for `attributes` variable inside mixins). - mixin_attributes: ?[]const ast.Attribute, - /// Current include/extends depth (for recursion protection). - include_depth: usize, +) !void { + switch (val) { + .none => {}, + .string => |s| { + if (s.len == 0) return; + // Add space separator if not first item + if (result.items.len > 0) try result.append(allocator, ' '); + try result.appendSlice(allocator, s); + }, + .object => |conditions| { + for (conditions) |cond| { + if (cond.condition and cond.name.len > 0) { + if (result.items.len > 0) try result.append(allocator, ' '); + try result.appendSlice(allocator, cond.name); + } + } + }, + .array => |items| { + for (items, 0..) |item, i| { + // Check if this item needs escaping (only at top level) + const should_escape = if (depth == 0) blk: { + break :blk if (escaping) |esc| (i < esc.len and esc[i]) else false; + } else false; - pub const Options = struct { - pretty: bool = true, - indent_str: []const u8 = " ", - self_closing: bool = true, - /// Base directory for resolving template paths. - base_dir: []const u8 = "", - /// File resolver for loading templates. - file_resolver: ?FileResolver = null, - /// Directory containing mixin files for lazy-loading. - /// If set, mixins not found in template will be loaded from here. - mixins_dir: []const u8 = "", - /// Maximum depth for include/extends to prevent infinite recursion. - /// Set to 0 to disable the limit (not recommended). - max_include_depth: usize = 100, + if (should_escape) { + // Need to escape: collect item first, then escape and append + const start_len = result.items.len; + const had_content = start_len > 0; + + // Temporarily collect the class string + var temp: ArrayListUnmanaged(u8) = .{}; + defer temp.deinit(allocator); + try classesInternal(allocator, item, null, &temp, depth + 1); + + if (temp.items.len > 0) { + if (had_content) try result.append(allocator, ' '); + // Escape directly into result + try appendEscaped(allocator, result, temp.items); + } + } else { + // No escaping: write directly to result + try classesInternal(allocator, item, null, result, depth + 1); + } + } + }, + } +} + +/// Append escaped HTML directly to result buffer (avoids intermediate allocation) +/// Public for use by codegen and other modules +pub fn appendEscaped(allocator: Allocator, result: *ArrayListUnmanaged(u8), html: []const u8) !void { + for (html) |c| { + if (escapeChar(c)) |escaped| { + try result.appendSlice(allocator, escaped); + } else { + try result.append(allocator, c); + } + } +} + +/// Escape a single character, returning the escape sequence or null if no escaping needed +pub fn escapeChar(c: u8) ?[]const u8 { + return switch (c) { + '"' => """, + '&' => "&", + '<' => "<", + '>' => ">", + else => null, }; +} - /// Error type for runtime operations. - pub const Error = RuntimeError || std.mem.Allocator.Error || error{TemplateNotFound}; +/// Attribute entry for attrs function +pub const AttrEntry = struct { + key: []const u8, + value: AttrValue, + is_class: bool = false, + is_style: bool = false, + class_value: ?ClassValue = null, + style_value: ?StyleValue = null, +}; - pub fn init(allocator: std.mem.Allocator, context: *Context, options: Options) Runtime { +/// Render multiple attributes. +/// Class attributes are processed specially and placed first. +pub fn attrs(allocator: Allocator, entries: []const AttrEntry, terse: bool) ![]const u8 { + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + + // First pass: find and render class attribute + for (entries) |entry| { + if (entry.is_class) { + if (entry.class_value) |cv| { + const class_str = try classes(allocator, cv, null); + defer allocator.free(class_str); + + if (class_str.len > 0) { + const attr_str = try attr(allocator, "class", .{ .string = class_str }, false, terse); + defer allocator.free(attr_str); + try result.appendSlice(allocator, attr_str); + } + } + break; + } + } + + // Second pass: render other attributes + for (entries) |entry| { + if (entry.is_class) continue; + + if (entry.is_style) { + if (entry.style_value) |sv| { + const style_str = try style(allocator, sv); + defer allocator.free(style_str); + + if (style_str.len > 0) { + const attr_str = try attr(allocator, "style", .{ .string = style_str }, false, terse); + defer allocator.free(attr_str); + try result.appendSlice(allocator, attr_str); + } + } + } else { + const attr_str = try attr(allocator, entry.key, entry.value, false, terse); + defer allocator.free(attr_str); + try result.appendSlice(allocator, attr_str); + } + } + + return try result.toOwnedSlice(allocator); +} + +/// Merge entry for combining attribute objects +pub const MergeEntry = struct { + key: []const u8, + value: MergeValue, +}; + +pub const MergeValue = union(enum) { + string: []const u8, + class_array: []const []const u8, + style_object: []const StyleProperty, + none, +}; + +/// Merge result for a single key +pub const MergedValue = struct { + key: []const u8, + value: MergeValue, + allocator: Allocator, + owned_strings: ArrayListUnmanaged([]const u8), + + pub fn deinit(self: *MergedValue) void { + for (self.owned_strings.items) |s| { + self.allocator.free(s); + } + self.owned_strings.deinit(self.allocator); + } +}; + +/// Ensure style string ends with semicolon +fn ensureTrailingSemicolon(allocator: Allocator, s: []const u8) ![]const u8 { + if (s.len == 0) return try allocator.dupe(u8, ""); + if (s[s.len - 1] == ';') return try allocator.dupe(u8, s); + + var result: ArrayListUnmanaged(u8) = .{}; + errdefer result.deinit(allocator); + try result.appendSlice(allocator, s); + try result.append(allocator, ';'); + return try result.toOwnedSlice(allocator); +} + +/// Convert style value to string with trailing semicolon +fn styleToString(allocator: Allocator, val: StyleValue) ![]const u8 { + const s = try style(allocator, val); + defer allocator.free(s); + return try ensureTrailingSemicolon(allocator, s); +} + +// ============================================================================ +// Merge function +// ============================================================================ + +/// Merged attributes result with O(1) lookups for class/style +pub const MergedAttrs = struct { + allocator: Allocator, + entries: ArrayListUnmanaged(MergedAttrEntry), + owned_strings: ArrayListUnmanaged([]const u8), + owned_class_arrays: ArrayListUnmanaged([][]const u8), + // O(1) index tracking for special keys + class_idx: ?usize = null, + style_idx: ?usize = null, + + pub fn init(allocator: Allocator) MergedAttrs { return .{ .allocator = allocator, - .context = context, - .output = .empty, - .depth = 0, - .options = options, - .file_resolver = options.file_resolver, - .base_dir = options.base_dir, - .mixins_dir = options.mixins_dir, - .blocks = .empty, - .mixin_block_content = null, - .mixin_attributes = null, - .include_depth = 0, + .entries = .{}, + .owned_strings = .{}, + .owned_class_arrays = .{}, + .class_idx = null, + .style_idx = null, }; } - pub fn deinit(self: *Runtime) void { - self.output.deinit(self.allocator); - self.blocks.deinit(self.allocator); + pub fn deinit(self: *MergedAttrs) void { + for (self.owned_strings.items) |s| { + self.allocator.free(s); + } + self.owned_strings.deinit(self.allocator); + + for (self.owned_class_arrays.items) |arr| { + self.allocator.free(arr); + } + self.owned_class_arrays.deinit(self.allocator); + + self.entries.deinit(self.allocator); } - /// Renders the document and returns the HTML output. - pub fn render(self: *Runtime, doc: ast.Document) Error![]const u8 { - // Pre-allocate buffer - 256KB handles most large templates without realloc - try self.output.ensureTotalCapacity(self.allocator, 256 * 1024); - - // Handle template inheritance - if (doc.extends_path) |extends_path| { - // Collect blocks from child template - try self.collectBlocks(doc.nodes); - - // Load and render parent template - const parent_doc = try self.loadTemplate(extends_path); - return self.render(parent_doc); - } - - for (doc.nodes) |node| { - try self.visitNode(node); - } - - return self.output.items; - } - - /// Collects block definitions from child template nodes. - fn collectBlocks(self: *Runtime, nodes: []const ast.Node) Error!void { - for (nodes) |node| { - switch (node) { - .block => |blk| { - try self.blocks.put(self.allocator, blk.name, .{ - .name = blk.name, - .mode = blk.mode, - .children = blk.children, - }); - }, - else => {}, + pub fn get(self: *const MergedAttrs, key: []const u8) ?MergedAttrValue { + // O(1) lookup for class and style + if (mem.eql(u8, key, "class")) { + if (self.class_idx) |idx| { + return self.entries.items[idx].value; } - } - } - - /// Loads and parses a template file. - /// Security: Validates path doesn't escape base_dir and enforces include depth limit. - fn loadTemplate(self: *Runtime, path: []const u8) Error!ast.Document { - // Security: Prevent infinite recursion via circular includes/extends - const max_depth = self.options.max_include_depth; - if (max_depth > 0 and self.include_depth >= max_depth) { - log.err("maximum include depth ({d}) exceeded - possible circular reference", .{max_depth}); - return error.MaxIncludeDepthExceeded; - } - self.include_depth += 1; - - const resolver = self.file_resolver orelse return error.TemplateNotFound; - - // Resolve path (add .pug extension if needed) - var resolved_path: []const u8 = path; - if (!std.mem.endsWith(u8, path, ".pug")) { - resolved_path = try std.fmt.allocPrint(self.allocator, "{s}.pug", .{path}); - } - - // Security: Reject absolute paths when base_dir is set (prevents /etc/passwd access) - if (self.base_dir.len > 0 and std.fs.path.isAbsolute(resolved_path)) { - log.err("absolute paths not allowed in include/extends: {s}", .{resolved_path}); - return error.PathTraversalDetected; - } - - // Security: Check for path traversal attempts (../ sequences) - if (std.mem.indexOf(u8, resolved_path, "..")) |_| { - log.err("path traversal detected in include/extends: {s}", .{resolved_path}); - return error.PathTraversalDetected; - } - - // Prepend base directory if path is relative - var full_path = resolved_path; - if (self.base_dir.len > 0) { - full_path = try std.fs.path.join(self.allocator, &.{ self.base_dir, resolved_path }); - } - - const source = resolver(self.allocator, full_path) orelse return error.TemplateNotFound; - - // Parse the template - var lexer = Lexer.init(self.allocator, source); - const tokens = lexer.tokenize() catch return error.TemplateNotFound; - - var parser = Parser.init(self.allocator, tokens); - return parser.parse() catch return error.TemplateNotFound; - } - - /// Renders and returns an owned copy of the output. - pub fn renderOwned(self: *Runtime, doc: ast.Document) Error![]u8 { - const result = try self.render(doc); - return try self.allocator.dupe(u8, result); - } - - fn visitNode(self: *Runtime, node: ast.Node) Error!void { - switch (node) { - .doctype => |dt| try self.visitDoctype(dt), - .element => |elem| try self.visitElement(elem), - .text => |text| try self.visitText(text), - .comment => |comment| try self.visitComment(comment), - .conditional => |cond| try self.visitConditional(cond), - .each => |each| try self.visitEach(each), - .@"while" => |whl| try self.visitWhile(whl), - .case => |c| try self.visitCase(c), - .mixin_def => |def| try self.context.defineMixin(def), - .mixin_call => |call| try self.visitMixinCall(call), - .mixin_block => try self.visitMixinBlock(), - .code => |code| try self.visitCode(code), - .raw_text => |raw| try self.visitRawText(raw), - .block => |blk| try self.visitBlock(blk), - .include => |inc| try self.visitInclude(inc), - .extends => {}, // Handled at document level - .document => |doc| { - for (doc.nodes) |child| { - try self.visitNode(child); - } - }, - } - } - - /// Renders a node inline (no indentation, no trailing newline). - /// Used for block expansion (`:` syntax) where children render on same line. - fn visitNodeInline(self: *Runtime, node: ast.Node) Error!void { - switch (node) { - .element => |elem| try self.visitElementInline(elem), - .text => |text| try self.writeTextSegments(text.segments), - else => try self.visitNode(node), // Fall back to normal rendering - } - } - - /// Renders an element inline (no indentation, no trailing newline). - fn visitElementInline(self: *Runtime, elem: ast.Element) Error!void { - const has_content = (elem.inline_text != null and elem.inline_text.?.len > 0) or - elem.buffered_code != null or elem.children.len > 0; - const is_void = (isVoidElement(elem.tag) or elem.self_closing) and !has_content; - - try self.write("<"); - try self.write(elem.tag); - - if (elem.id) |id| { - try self.write(" id=\""); - try self.writeEscaped(id); - try self.write("\""); - } - - // Output classes - if (elem.classes.len > 0) { - try self.write(" class=\""); - for (elem.classes, 0..) |class, i| { - if (i > 0) try self.write(" "); - try self.writeEscaped(class); - } - try self.write("\""); - } - - // Output attributes - for (elem.attributes) |attr| { - if (attr.value) |value| { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - var evaluated: []const u8 = undefined; - if (value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`')) { - evaluated = try self.evaluateString(value); - } else { - const expr_value = self.evaluateExpression(value); - evaluated = try expr_value.toString(self.allocator); - } - if (attr.escaped) { - try self.writeEscaped(evaluated); - } else { - try self.write(evaluated); - } - try self.write("\""); - } else { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - try self.write(attr.name); - try self.write("\""); - } - } - - if (is_void) { - try self.write("/>"); - return; - } - - try self.write(">"); - - // Render inline text - if (elem.inline_text) |text| { - try self.writeTextSegments(text); - } - - // Render buffered code - if (elem.buffered_code) |code| { - const value = self.evaluateExpression(code.expression); - const str = try value.toString(self.allocator); - if (code.escaped) { - try self.writeTextEscaped(str); - } else { - try self.write(str); - } - } - - // Render children inline - for (elem.children) |child| { - try self.visitNodeInline(child); - } - - try self.write(""); - } - - /// Doctype shortcuts mapping - const doctype_shortcuts = std.StaticStringMap([]const u8).initComptime(.{ - .{ "html", "" }, - .{ "xml", "" }, - .{ "transitional", "" }, - .{ "strict", "" }, - .{ "frameset", "" }, - .{ "1.1", "" }, - .{ "basic", "" }, - .{ "mobile", "" }, - .{ "plist", "" }, - }); - - fn visitDoctype(self: *Runtime, dt: ast.Doctype) Error!void { - // Look up shortcut or use custom doctype - if (doctype_shortcuts.get(dt.value)) |output| { - try self.write(output); - } else { - // Custom doctype: output as-is with "); - } - try self.writeNewline(); - } - - fn visitElement(self: *Runtime, elem: ast.Element) Error!void { - // Void elements can be self-closed, but only if they have no content - const has_content = (elem.inline_text != null and elem.inline_text.?.len > 0) or - elem.buffered_code != null or elem.children.len > 0; - const is_void = (isVoidElement(elem.tag) or elem.self_closing) and !has_content; - - try self.writeIndent(); - try self.write("<"); - try self.write(elem.tag); - - if (elem.id) |id| { - try self.write(" id=\""); - try self.writeEscaped(id); - try self.write("\""); - } - - // Collect all classes first: shorthand classes + class attributes (may be arrays) - // Class attribute must be output before other attributes per Pug convention - var all_classes = std.ArrayList(u8).empty; - defer all_classes.deinit(self.allocator); - - // Add shorthand classes first (e.g., .bang) - for (elem.classes, 0..) |class, i| { - if (i > 0) try all_classes.append(self.allocator, ' '); - try all_classes.appendSlice(self.allocator, class); - } - - // Collect class values from attributes - for (elem.attributes) |attr| { - if (std.mem.eql(u8, attr.name, "class")) { - if (attr.value) |value| { - var evaluated: []const u8 = undefined; - - if (value.len >= 1 and value[0] == '[') { - evaluated = try parseArrayToSpaceSeparated(self.allocator, value); - } else if (value.len >= 1 and value[0] == '{') { - evaluated = try parseObjectToClassList(self.allocator, value); - } else { - const expr_value = self.evaluateExpression(value); - evaluated = try expr_value.toString(self.allocator); - } - - if (evaluated.len > 0) { - if (all_classes.items.len > 0) { - try all_classes.append(self.allocator, ' '); - } - try all_classes.appendSlice(self.allocator, evaluated); - } - } - } - } - - // Output combined class attribute immediately after id (before other attributes) - if (all_classes.items.len > 0) { - try self.write(" class=\""); - try self.writeEscaped(all_classes.items); - try self.write("\""); - } - - // Output other attributes (skip class since already handled) - for (elem.attributes) |attr| { - if (std.mem.eql(u8, attr.name, "class")) continue; - - if (attr.value) |value| { - // Handle boolean literals: true -> checked="checked", false -> omit - if (std.mem.eql(u8, value, "true")) { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - try self.write(attr.name); - try self.write("\""); - } else if (std.mem.eql(u8, value, "false")) { - continue; - } else { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - var evaluated: []const u8 = undefined; - - if (value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`')) { - evaluated = try self.evaluateString(value); - } else if (value.len >= 1 and (value[0] == '{' or value[0] == '[')) { - evaluated = value; - } else { - const expr_value = self.evaluateExpression(value); - evaluated = try expr_value.toString(self.allocator); - } - - if (std.mem.eql(u8, attr.name, "style") and evaluated.len > 0 and evaluated[0] == '{') { - evaluated = try parseObjectToCSS(self.allocator, evaluated); - } - - if (attr.escaped) { - try self.writeEscaped(evaluated); - } else { - try self.write(evaluated); - } - try self.write("\""); - } - } else { - // Boolean attribute: checked -> checked="checked" - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - try self.write(attr.name); - try self.write("\""); - } - } - - // Output spread attributes: &attributes({'data-foo': 'bar'}) or &attributes(attributes) - if (elem.spread_attributes) |spread| { - // First try to evaluate as a variable (for mixin attributes) - const value = self.evaluateExpression(spread); - switch (value) { - .object => |obj| { - // Render object properties as attributes - var iter = obj.iterator(); - while (iter.next()) |entry| { - const attr_value = entry.value_ptr.*; - const str = try attr_value.toString(self.allocator); - try self.write(" "); - try self.write(entry.key_ptr.*); - try self.write("=\""); - try self.writeEscaped(str); - try self.write("\""); - } - }, - else => { - // Fall back to parsing as object literal string - try self.writeSpreadAttributes(spread); - }, - } - } - - if (is_void and self.options.self_closing) { - try self.write("/>"); - try self.writeNewline(); - return; - } - - try self.write(">"); - - const has_inline = elem.inline_text != null and elem.inline_text.?.len > 0; - const has_buffered = elem.buffered_code != null; - const has_children = elem.children.len > 0; - - if (has_inline) { - try self.writeTextSegments(elem.inline_text.?); - } - - if (has_buffered) { - const code = elem.buffered_code.?; - const value = self.evaluateExpression(code.expression); - const str = try value.toString(self.allocator); - if (code.escaped) { - try self.writeTextEscaped(str); - } else { - try self.write(str); - } - } - - if (has_children) { - // Check if single text child - render inline (like blockquote with one piped line) - const single_text = elem.children.len == 1 and elem.children[0] == .text; - // Check for whitespace-preserving elements (pre, script, style, textarea) - const preserve_ws = isWhitespacePreserving(elem.tag); - - if (single_text) { - // Render single text child inline (no newlines/indents) - try self.writeTextSegments(elem.children[0].text.segments); - } else if (elem.is_inline and canRenderInlineForParent(elem)) { - // Block expansion (`:` syntax) - render children inline only in specific cases - for (elem.children) |child| { - try self.visitNodeInline(child); - } - } else if (preserve_ws) { - // Whitespace-preserving element - render content without extra formatting - for (elem.children) |child| { - switch (child) { - .raw_text => |raw| { - // Check if content has multiple lines - if so, add leading newline - // Single-line content renders inline and stripped: - // Multi-line content has newline: - const has_multiple_lines = std.mem.indexOfScalar(u8, raw.content, '\n') != null; - if (has_multiple_lines and !has_inline and !has_buffered) { - try self.write("\n"); - try self.writeRawTextPreserved(raw.content); - } else { - // Single line - strip leading whitespace - const stripped = std.mem.trimLeft(u8, raw.content, " \t"); - try self.write(stripped); - } - }, - .element => |child_elem| { - // Nested element in whitespace-preserving context (e.g., pre > code) - try self.visitElementPreserved(child_elem); - }, - else => try self.visitNode(child), - } - } - } else { - if (!has_inline and !has_buffered) try self.writeNewline(); - self.depth += 1; - for (elem.children) |child| { - try self.visitNode(child); - } - self.depth -= 1; - if (!has_inline and !has_buffered) try self.writeIndent(); - } - } - - try self.write(""); - try self.writeNewline(); - } - - fn visitText(self: *Runtime, text: ast.Text) Error!void { - try self.writeIndent(); - try self.writeTextSegments(text.segments); - try self.writeNewline(); - } - - /// Writes raw text content as-is. - fn writeRawTextPreserved(self: *Runtime, content: []const u8) Error!void { - try self.write(content); - } - - /// Renders an element within a whitespace-preserving context (no indentation/newlines) - fn visitElementPreserved(self: *Runtime, elem: ast.Element) Error!void { - try self.write("<"); - try self.write(elem.tag); - - // Output classes - if (elem.classes.len > 0) { - try self.write(" class=\""); - for (elem.classes, 0..) |class, i| { - if (i > 0) try self.write(" "); - try self.writeEscaped(class); - } - try self.write("\""); - } - - // Output attributes - for (elem.attributes) |attr| { - if (attr.value) |value| { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - var evaluated: []const u8 = undefined; - if (value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`')) { - evaluated = try self.evaluateString(value); - } else { - const expr_value = self.evaluateExpression(value); - evaluated = try expr_value.toString(self.allocator); - } - if (attr.escaped) { - try self.writeEscaped(evaluated); - } else { - try self.write(evaluated); - } - try self.write("\""); - } else { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - try self.write(attr.name); - try self.write("\""); - } - } - - try self.write(">"); - - // Render children without formatting - for (elem.children) |child| { - switch (child) { - .raw_text => |raw| try self.writeRawTextPreserved(raw.content), - .text => |text| try self.writeTextSegments(text.segments), - else => {}, - } - } - - try self.write(""); - } - - fn visitComment(self: *Runtime, comment: ast.Comment) Error!void { - if (!comment.rendered) return; - - try self.writeIndent(); - try self.write(""); - } else { - // Inline comment - // Content already includes leading space if present (e.g., " foo" from "// foo") - if (comment.content.len > 0) { - try self.write(comment.content); - } - try self.write("-->"); - } - try self.writeNewline(); - } - - fn visitConditional(self: *Runtime, cond: ast.Conditional) Error!void { - for (cond.branches) |branch| { - const should_render = if (branch.condition) |condition| blk: { - const value = self.evaluateExpression(condition); - const truthy = value.isTruthy(); - break :blk if (branch.is_unless) !truthy else truthy; - } else true; // else branch - - if (should_render) { - for (branch.children) |child| { - try self.visitNode(child); - } - return; // Only render first matching branch - } - } - } - - fn visitEach(self: *Runtime, each: ast.Each) Error!void { - const collection = self.evaluateExpression(each.collection); - - switch (collection) { - .array => |items| { - if (items.len == 0) { - // Render else branch if collection is empty - for (each.else_children) |child| { - try self.visitNode(child); - } - return; - } - - // Push scope once before the loop - reuse for all iterations - try self.context.pushScope(); - defer self.context.popScope(); - - // Get direct pointers to loop variables - avoids hash lookup per iteration - const value_ptr = try self.context.getOrPutPtr(each.value_name); - const index_ptr: ?*Value = if (each.index_name) |idx_name| - try self.context.getOrPutPtr(idx_name) - else - null; - - for (items, 0..) |item, index| { - // Direct pointer update - no hash lookup! - value_ptr.* = item; - if (index_ptr) |ptr| { - ptr.* = Value.integer(@intCast(index)); - } - - for (each.children) |child| { - try self.visitNode(child); - } - } - }, - .object => |obj| { - if (obj.count() == 0) { - for (each.else_children) |child| { - try self.visitNode(child); - } - return; - } - - // Push scope once before the loop - reuse for all iterations - try self.context.pushScope(); - defer self.context.popScope(); - - // Get direct pointers to loop variables - const value_ptr = try self.context.getOrPutPtr(each.value_name); - const index_ptr: ?*Value = if (each.index_name) |idx_name| - try self.context.getOrPutPtr(idx_name) - else - null; - - var iter = obj.iterator(); - while (iter.next()) |entry| { - // Direct pointer update - no hash lookup! - value_ptr.* = entry.value_ptr.*; - if (index_ptr) |ptr| { - ptr.* = Value.str(entry.key_ptr.*); - } - - for (each.children) |child| { - try self.visitNode(child); - } - } - }, - else => { - // Not iterable - render else branch - for (each.else_children) |child| { - try self.visitNode(child); - } - }, - } - } - - fn visitWhile(self: *Runtime, whl: ast.While) Error!void { - var iterations: usize = 0; - const max_iterations: usize = 10000; // Safety limit - - while (iterations < max_iterations) { - const condition = self.evaluateExpression(whl.condition); - if (!condition.isTruthy()) break; - - for (whl.children) |child| { - try self.visitNode(child); - } - iterations += 1; - } - } - - fn visitCase(self: *Runtime, c: ast.Case) Error!void { - const expr_value = self.evaluateExpression(c.expression); - - // Find matching when clause - var matched = false; - var fall_through = false; - - for (c.whens) |when| { - // Check if we're falling through from previous match - if (fall_through) { - if (when.has_break) { - // Explicit break - stop here without output - return; - } - if (when.children.len > 0) { - // Has content - render it - for (when.children) |child| { - try self.visitNode(child); - } - return; - } - // Empty body - continue falling through - continue; - } - - // Parse when value and compare - const when_value = self.evaluateExpression(when.value); - - if (self.valuesEqual(expr_value, when_value)) { - matched = true; - - if (when.has_break) { - // Explicit break - output nothing - return; - } - - if (when.children.len == 0) { - // Empty body - fall through to next - fall_through = true; - continue; - } - - // Render matching case - for (when.children) |child| { - try self.visitNode(child); - } - return; - } - } - - // No match - render default if present - if (!matched or fall_through) { - for (c.default_children) |child| { - try self.visitNode(child); - } - } - } - - /// Compares two Values for equality. - fn valuesEqual(self: *Runtime, a: Value, b: Value) bool { - _ = self; - return switch (a) { - .int => |ai| switch (b) { - .int => |bi| ai == bi, - .float => |bf| @as(f64, @floatFromInt(ai)) == bf, - .string => |bs| blk: { - const parsed = std.fmt.parseInt(i64, bs, 10) catch break :blk false; - break :blk ai == parsed; - }, - else => false, - }, - .float => |af| switch (b) { - .int => |bi| af == @as(f64, @floatFromInt(bi)), - .float => |bf| af == bf, - else => false, - }, - .string => |as| switch (b) { - .string => |bs| std.mem.eql(u8, as, bs), - .int => |bi| blk: { - const parsed = std.fmt.parseInt(i64, as, 10) catch break :blk false; - break :blk parsed == bi; - }, - else => false, - }, - .bool => |ab| switch (b) { - .bool => |bb| ab == bb, - else => false, - }, - else => false, - }; - } - - fn visitMixinCall(self: *Runtime, call: ast.MixinCall) Error!void { - // First check if mixin is defined in current context (same template or preloaded) - var mixin = self.context.getMixin(call.name); - - // If not found and mixins_dir is configured, try loading from mixins directory - if (mixin == null and self.mixins_dir.len > 0) { - if (self.loadMixinFromDir(call.name)) |loaded_mixin| { - try self.context.defineMixin(loaded_mixin); - mixin = loaded_mixin; - } - } - - // If still not found, log warning and skip this mixin call - const mixin_def = mixin orelse { - log.warn("skipping, mixin '{s}' not found", .{call.name}); - return; - }; - - try self.context.pushScope(); - defer self.context.popScope(); - - // Save previous mixin context - const prev_block_content = self.mixin_block_content; - const prev_attributes = self.mixin_attributes; - defer { - self.mixin_block_content = prev_block_content; - self.mixin_attributes = prev_attributes; - } - - // Set current mixin's block content and attributes - // If block content is a single mixin_block node, pass through parent's block content - // to avoid infinite recursion when nesting mixins with `block` passthrough - self.mixin_block_content = blk: { - if (call.block_children.len == 1 and call.block_children[0] == .mixin_block) { - break :blk prev_block_content; - } - break :blk if (call.block_children.len > 0) call.block_children else null; - }; - self.mixin_attributes = if (call.attributes.len > 0) call.attributes else null; - - // Set 'attributes' variable with the passed attributes as an object - if (call.attributes.len > 0) { - var attrs_obj = std.StringHashMapUnmanaged(Value).empty; - for (call.attributes) |attr| { - if (attr.value) |val| { - // Strip quotes from attribute value for the object - const clean_val = try self.evaluateString(val); - attrs_obj.put(self.allocator, attr.name, Value.str(clean_val)) catch |err| { - log.warn("skipping attribute, failed to set '{s}': {}", .{ attr.name, err }); - }; - } else { - attrs_obj.put(self.allocator, attr.name, Value.boolean(true)) catch |err| { - log.warn("skipping attribute, failed to set '{s}': {}", .{ attr.name, err }); - }; - } - } - try self.context.set("attributes", .{ .object = attrs_obj }); - } else { - try self.context.set("attributes", .{ .object = std.StringHashMapUnmanaged(Value).empty }); - } - - // Bind arguments to parameters - const regular_params = if (mixin_def.has_rest and mixin_def.params.len > 0) - mixin_def.params.len - 1 - else - mixin_def.params.len; - - // Bind regular parameters - for (mixin_def.params[0..regular_params], 0..) |param, i| { - const value = if (i < call.args.len) - self.evaluateExpression(call.args[i]) - else if (i < mixin_def.defaults.len and mixin_def.defaults[i] != null) - self.evaluateExpression(mixin_def.defaults[i].?) - else - Value.null; - - try self.context.set(param, value); - } - - // Bind rest parameter if present - if (mixin_def.has_rest and mixin_def.params.len > 0) { - const rest_param = mixin_def.params[mixin_def.params.len - 1]; - const rest_start = regular_params; - - if (rest_start < call.args.len) { - // Collect remaining arguments into an array - const rest_count = call.args.len - rest_start; - const rest_array = self.allocator.alloc(Value, rest_count) catch return error.OutOfMemory; - for (call.args[rest_start..], 0..) |arg, i| { - rest_array[i] = self.evaluateExpression(arg); - } - try self.context.set(rest_param, .{ .array = rest_array }); - } else { - // No rest arguments, set empty array - const empty = self.allocator.alloc(Value, 0) catch return error.OutOfMemory; - try self.context.set(rest_param, .{ .array = empty }); - } - } - - // Render mixin body - for (mixin_def.children) |child| { - try self.visitNode(child); - } - } - - /// Loads a mixin from the mixins directory by name. - /// Searches for files named {name}.pug or iterates through all .pug files. - /// Note: The source file memory is intentionally not freed to keep AST slices valid. - fn loadMixinFromDir(self: *Runtime, name: []const u8) ?ast.MixinDef { - const resolver = self.file_resolver orelse return null; - - // First try: look for a file named {name}.pug - const specific_path = std.fs.path.join(self.allocator, &.{ self.mixins_dir, name }) catch |err| { - log.warn("skipping mixin lookup, failed to join path for '{s}': {}", .{ name, err }); return null; - }; - defer self.allocator.free(specific_path); - - const with_ext = std.fmt.allocPrint(self.allocator, "{s}.pug", .{specific_path}) catch |err| { - log.warn("skipping mixin lookup, failed to allocate path for '{s}': {}", .{ name, err }); - return null; - }; - defer self.allocator.free(with_ext); - - if (resolver(self.allocator, with_ext)) |source| { - // Note: source is intentionally not freed - AST nodes contain slices into it - if (self.parseMixinFromSource(source, name)) |mixin_def| { - return mixin_def; - } - // Only free if we didn't find the mixin we wanted - self.allocator.free(source); } - - // Second try: iterate through all .pug files in mixins directory - // Use cwd().openDir for relative paths, openDirAbsolute for absolute paths - var dir = if (std.fs.path.isAbsolute(self.mixins_dir)) - std.fs.openDirAbsolute(self.mixins_dir, .{ .iterate = true }) catch |err| { - log.warn("skipping mixins directory scan, failed to open '{s}': {}", .{ self.mixins_dir, err }); - return null; + if (mem.eql(u8, key, "style")) { + if (self.style_idx) |idx| { + return self.entries.items[idx].value; } - else - std.fs.cwd().openDir(self.mixins_dir, .{ .iterate = true }) catch |err| { - log.warn("skipping mixins directory scan, failed to open '{s}': {}", .{ self.mixins_dir, err }); - return null; - }; - defer dir.close(); - - var iter = dir.iterate(); - while (iter.next() catch |err| { - log.warn("skipping mixins directory scan, iteration failed: {}", .{err}); return null; - }) |entry| { - if (entry.kind != .file) continue; - if (!std.mem.endsWith(u8, entry.name, ".pug")) continue; - - const file_path = std.fs.path.join(self.allocator, &.{ self.mixins_dir, entry.name }) catch |err| { - log.warn("skipping mixin file, failed to join path for '{s}': {}", .{ entry.name, err }); - continue; - }; - defer self.allocator.free(file_path); - - if (resolver(self.allocator, file_path)) |source| { - // Note: source is intentionally not freed - AST nodes contain slices into it - if (self.parseMixinFromSource(source, name)) |mixin_def| { - return mixin_def; - } - // Only free if we didn't find the mixin we wanted - self.allocator.free(source); + } + // Linear search for other keys + for (self.entries.items) |entry| { + if (mem.eql(u8, entry.key, key)) { + return entry.value; } } - return null; } - /// Parses a source file and extracts a mixin definition by name. - fn parseMixinFromSource(self: *Runtime, source: []const u8, name: []const u8) ?ast.MixinDef { - var lexer = Lexer.init(self.allocator, source); - const tokens = lexer.tokenize() catch |err| { - log.warn("skipping mixin file, tokenize failed for '{s}': {}", .{ name, err }); - return null; - }; - // Note: lexer is not deinitialized - tokens contain slices into source - - var parser = Parser.init(self.allocator, tokens); - const doc = parser.parse() catch |err| { - log.warn("skipping mixin file, parse failed for '{s}': {}", .{ name, err }); - return null; - }; - - // Find the mixin definition with the matching name - for (doc.nodes) |node| { - if (node == .mixin_def) { - if (std.mem.eql(u8, node.mixin_def.name, name)) { - return node.mixin_def; - } - } + /// Find index of a key (O(1) for class/style, O(n) for others) + fn findKey(self: *const MergedAttrs, key: []const u8) ?usize { + if (mem.eql(u8, key, "class")) return self.class_idx; + if (mem.eql(u8, key, "style")) return self.style_idx; + for (self.entries.items, 0..) |entry, i| { + if (mem.eql(u8, entry.key, key)) return i; } - return null; } +}; - /// Renders the mixin block content (for `block` keyword inside mixins). - fn visitMixinBlock(self: *Runtime) Error!void { - if (self.mixin_block_content) |block_children| { - for (block_children) |child| { - try self.visitNode(child); - } - } +pub const MergedAttrEntry = struct { + key: []const u8, + value: MergedAttrValue, +}; + +pub const MergedAttrValue = union(enum) { + string: []const u8, + class_array: [][]const u8, + none, +}; + +/// Merge two attribute objects. +/// class attributes are combined into arrays. +/// style attributes are concatenated with semicolons. +/// Optimized with O(1) lookups for class/style and branch prediction hints. +pub fn merge(allocator: Allocator, a: []const MergedAttrEntry, b: []const MergedAttrEntry) !MergedAttrs { + var result = MergedAttrs.init(allocator); + errdefer result.deinit(); + + // Pre-allocate capacity to avoid reallocations (cache-friendly) + const total_entries = a.len + b.len; + if (total_entries > 0) { + try result.entries.ensureTotalCapacity(allocator, total_entries); } - fn visitCode(self: *Runtime, code: ast.Code) Error!void { - const value = self.evaluateExpression(code.expression); - const str = try value.toString(self.allocator); - - try self.writeIndent(); - if (code.escaped) { - try self.writeTextEscaped(str); - } else { - try self.write(str); - } - try self.writeNewline(); + // Process first object + for (a) |entry| { + try mergeEntry(&result, entry); } - fn visitRawText(self: *Runtime, raw: ast.RawText) Error!void { - // Raw text already includes its own indentation, don't add extra - try self.write(raw.content); - // Only add newline if content doesn't already end with one - // This prevents double newlines at end of dot blocks - if (raw.content.len == 0 or raw.content[raw.content.len - 1] != '\n') { - try self.writeNewline(); - } + // Process second object + for (b) |entry| { + try mergeEntry(&result, entry); } - /// Visits a block node, handling inheritance (replace/append/prepend). - fn visitBlock(self: *Runtime, blk: ast.Block) Error!void { - // Check if child template overrides this block - if (self.blocks.get(blk.name)) |child_block| { - switch (child_block.mode) { - .replace => { - // Child completely replaces parent block - for (child_block.children) |child| { - try self.visitNode(child); - } - }, - .append => { - // Parent content first, then child content - for (blk.children) |child| { - try self.visitNode(child); - } - for (child_block.children) |child| { - try self.visitNode(child); - } - }, - .prepend => { - // Child content first, then parent content - for (child_block.children) |child| { - try self.visitNode(child); - } - for (blk.children) |child| { - try self.visitNode(child); - } - }, - } - } else { - // No override - render default block content - for (blk.children) |child| { - try self.visitNode(child); - } - } + return result; +} + +/// Fast key classification for branch prediction +const KeyType = enum { class, style, other }; + +inline fn classifyKey(key: []const u8) KeyType { + // Most common case: short keys that aren't class/style + // Use length check first (branch-friendly, avoids string compare) + if (key.len == 5) { + if (key[0] == 'c' and mem.eql(u8, key, "class")) return .class; + if (key[0] == 's' and mem.eql(u8, key, "style")) return .style; } + return .other; +} - /// Visits an include node, loading and rendering the included template. - fn visitInclude(self: *Runtime, inc: ast.Include) Error!void { - const included_doc = try self.loadTemplate(inc.path); +fn mergeEntry(result: *MergedAttrs, entry: MergedAttrEntry) !void { + const allocator = result.allocator; - // TODO: Handle filters (inc.filter) like :markdown + // Branch prediction: classify key type once + const key_type = classifyKey(entry.key); - // Render included template inline - for (included_doc.nodes) |node| { - try self.visitNode(node); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Expression evaluation - // ───────────────────────────────────────────────────────────────────────── - - /// Evaluates a simple expression (variable lookup or literal). - /// Optimized for common cases: simple variable names without operators. - fn evaluateExpression(self: *Runtime, expr: []const u8) Value { - // Fast path: empty expression - if (expr.len == 0) return Value.null; - - const first = expr[0]; - - // Ultra-fast path: identifier starting with a-z (most common case) - // Covers: friend, name, friend.name, friend.email, tag, etc. - if (first >= 'a' and first <= 'z') { - // Scan for operators - if none found, direct variable lookup - for (expr) |c| { - // Check for operators that require complex evaluation - if (c == '+' or c == '[' or c == '(' or c == '{' or c == ' ' or c == '\t') { - break; - } - } else { - // No operators found - direct variable lookup (most common path) - return self.lookupVariable(expr); - } - } - - // Fast path: check if expression needs trimming - const last = expr[expr.len - 1]; - const needs_trim = first == ' ' or first == '\t' or last == ' ' or last == '\t'; - const trimmed = if (needs_trim) std.mem.trim(u8, expr, " \t") else expr; - - if (trimmed.len == 0) return Value.null; - - // Fast path: simple variable lookup (no special chars except dots) - // Most expressions in templates are just variable names like "name" or "friend.email" - const first_char = trimmed[0]; - if (first_char != '"' and first_char != '\'' and first_char != '-' and - (first_char < '0' or first_char > '9')) - { - // Quick scan: if no special operators, go straight to variable lookup - var has_operator = false; - for (trimmed) |c| { - if (c == '+' or c == '[' or c == '(' or c == '{') { - has_operator = true; - break; - } - } - if (!has_operator) { - // Check for boolean/null literals - if (trimmed.len <= 5) { - if (std.mem.eql(u8, trimmed, "true")) return Value.boolean(true); - if (std.mem.eql(u8, trimmed, "false")) return Value.boolean(false); - if (std.mem.eql(u8, trimmed, "null")) return Value.null; - } - // Simple variable lookup - return self.lookupVariable(trimmed); - } - } - - // Check for string concatenation with + operator - // e.g., "btn btn-" + type or "hello " + name + "!" - if (self.findConcatOperator(trimmed)) |op_pos| { - const left = std.mem.trim(u8, trimmed[0..op_pos], " \t"); - const right = std.mem.trim(u8, trimmed[op_pos + 1 ..], " \t"); - - const left_val = self.evaluateExpression(left); - const right_val = self.evaluateExpression(right); - - const left_str = left_val.toString(self.allocator) catch return Value.null; - const right_str = right_val.toString(self.allocator) catch return Value.null; - - const result = std.fmt.allocPrint(self.allocator, "{s}{s}", .{ left_str, right_str }) catch return Value.null; - return Value.str(result); - } - - // Check for string literal - if (trimmed.len >= 2) { - if ((first_char == '"' and trimmed[trimmed.len - 1] == '"') or - (first_char == '\'' and trimmed[trimmed.len - 1] == '\'')) - { - return Value.str(trimmed[1 .. trimmed.len - 1]); - } - } - - // Check for numeric literal - if (std.fmt.parseInt(i64, trimmed, 10)) |i| { - return Value.integer(i); - } else |_| {} - - // Check for boolean literals (fallback for complex expressions) - if (std.mem.eql(u8, trimmed, "true")) return Value.boolean(true); - if (std.mem.eql(u8, trimmed, "false")) return Value.boolean(false); - if (std.mem.eql(u8, trimmed, "null")) return Value.null; - - // Variable lookup (supports dot notation: user.name) - return self.lookupVariable(trimmed); - } - - /// Finds the position of a + operator that's not inside quotes or brackets. - /// Returns null if no such operator exists. - fn findConcatOperator(_: *Runtime, expr: []const u8) ?usize { - var in_string: u8 = 0; // 0 = not in string, '"' or '\'' = in that type of string - var bracket_depth: usize = 0; - var paren_depth: usize = 0; - var brace_depth: usize = 0; - - for (expr, 0..) |c, i| { - if (in_string != 0) { - if (c == in_string) { - in_string = 0; - } else if (c == '\\' and i + 1 < expr.len) { - // Skip escaped character - we'll handle it in next iteration - continue; - } - } else { - switch (c) { - '"', '\'' => in_string = c, - '[' => bracket_depth += 1, - ']' => bracket_depth -|= 1, - '(' => paren_depth += 1, - ')' => paren_depth -|= 1, - '{' => brace_depth += 1, - '}' => brace_depth -|= 1, - '+' => { - if (bracket_depth == 0 and paren_depth == 0 and brace_depth == 0) { - return i; - } - }, - else => {}, - } - } - } - - return null; - } - - /// Looks up a variable with dot notation support. - /// Optimized for the common case of single property access (e.g., "friend.name"). - fn lookupVariable(self: *Runtime, path: []const u8) Value { - // Fast path: find first dot position - var dot_pos: ?usize = null; - for (path, 0..) |c, i| { - if (c == '.') { - dot_pos = i; - break; - } - } - - if (dot_pos == null) { - // No dots - simple variable lookup - return self.context.get(path) orelse Value.null; - } - - // Has dots - get base variable first - const base_name = path[0..dot_pos.?]; - var current = self.context.get(base_name) orelse return Value.null; - - // Property access loop - objects are most common - var pos = dot_pos.? + 1; - while (pos < path.len) { - // Find next dot or end - var end = pos; - while (end < path.len and path[end] != '.') { - end += 1; - } - const prop = path[pos..end]; - - // Most values are objects in property chains (branch hint) - if (current == .object) { + switch (key_type) { + .class => { + // O(1) lookup using cached index + if (result.class_idx) |idx| { @branchHint(.likely); - current = current.object.get(prop) orelse return Value.null; + try mergeClassValue(result, idx, entry.value); } else { - return Value.null; + @branchHint(.unlikely); + try addNewClassEntry(result, entry.value); } - - pos = end + 1; - } - - return current; - } - - /// Evaluates a string value, stripping surrounding quotes and processing escape sequences. - /// Used for HTML attribute values. - fn evaluateString(self: *Runtime, str: []const u8) ![]const u8 { - // Strip surrounding quotes if present (single, double, or backtick) - if (str.len >= 2) { - const first = str[0]; - const last = str[str.len - 1]; - if ((first == '"' and last == '"') or - (first == '\'' and last == '\'') or - (first == '`' and last == '`')) - { - const inner = str[1 .. str.len - 1]; - // Process escape sequences (e.g., \\ -> \, \n -> newline) - return try self.processEscapeSequences(inner); - } - } - return str; - } - - /// Process JavaScript-style escape sequences in strings - fn processEscapeSequences(self: *Runtime, str: []const u8) ![]const u8 { - // Quick check - if no backslashes, return as-is - if (std.mem.indexOfScalar(u8, str, '\\') == null) { - return str; - } - - var result = std.ArrayList(u8).empty; - var i: usize = 0; - while (i < str.len) { - if (str[i] == '\\' and i + 1 < str.len) { - const next = str[i + 1]; - switch (next) { - '\\' => { - try result.append(self.allocator, '\\'); - i += 2; - }, - 'n' => { - try result.append(self.allocator, '\n'); - i += 2; - }, - 'r' => { - try result.append(self.allocator, '\r'); - i += 2; - }, - 't' => { - try result.append(self.allocator, '\t'); - i += 2; - }, - '\'' => { - try result.append(self.allocator, '\''); - i += 2; - }, - '"' => { - try result.append(self.allocator, '"'); - i += 2; - }, - else => { - // Unknown escape - keep the backslash and character - try result.append(self.allocator, str[i]); - i += 1; - }, - } + }, + .style => { + // O(1) lookup using cached index + if (result.style_idx) |idx| { + @branchHint(.likely); + try mergeStyleValue(result, idx, entry.value); } else { - try result.append(self.allocator, str[i]); - i += 1; + @branchHint(.unlikely); + try addNewStyleEntry(result, entry.value); } - } - return result.items; + }, + .other => { + // Regular attribute - linear search but rare in typical usage + const found_idx = result.findKey(entry.key); + if (found_idx) |idx| { + result.entries.items[idx].value = entry.value; + } else { + try result.entries.append(allocator, entry); + } + }, } +} - // ───────────────────────────────────────────────────────────────────────── - // Output helpers - // ───────────────────────────────────────────────────────────────────────── +/// Merge a class value with existing class at index +fn mergeClassValue(result: *MergedAttrs, idx: usize, value: MergedAttrValue) !void { + const allocator = result.allocator; + const existing = result.entries.items[idx].value; - fn writeTextSegments(self: *Runtime, segments: []const ast.TextSegment) Error!void { - for (segments) |seg| { - switch (seg) { - .literal => |lit| try self.writeTextEscaped(lit), - .interp_escaped => |expr| { - const value = self.evaluateExpression(expr); - const str = try value.toString(self.allocator); - try self.writeTextEscaped(str); + switch (value) { + .string => |s| { + switch (existing) { + .class_array => |arr| { + const new_arr = try allocator.alloc([]const u8, arr.len + 1); + @memcpy(new_arr[0..arr.len], arr); + new_arr[arr.len] = s; + try result.owned_class_arrays.append(allocator, new_arr); + result.entries.items[idx].value = .{ .class_array = new_arr }; }, - .interp_unescaped => |expr| { - const value = self.evaluateExpression(expr); - const str = try value.toString(self.allocator); - try self.write(str); + .string => |existing_s| { + const new_arr = try allocator.alloc([]const u8, 2); + new_arr[0] = existing_s; + new_arr[1] = s; + try result.owned_class_arrays.append(allocator, new_arr); + result.entries.items[idx].value = .{ .class_array = new_arr }; }, - .interp_tag => |inline_tag| { - try self.writeInlineTag(inline_tag); + .none => { + const new_arr = try allocator.alloc([]const u8, 1); + new_arr[0] = s; + try result.owned_class_arrays.append(allocator, new_arr); + result.entries.items[idx].value = .{ .class_array = new_arr }; }, } + }, + .class_array => |arr| { + switch (existing) { + .class_array => |existing_arr| { + const new_arr = try allocator.alloc([]const u8, existing_arr.len + arr.len); + @memcpy(new_arr[0..existing_arr.len], existing_arr); + @memcpy(new_arr[existing_arr.len..], arr); + try result.owned_class_arrays.append(allocator, new_arr); + result.entries.items[idx].value = .{ .class_array = new_arr }; + }, + .string => |existing_s| { + const new_arr = try allocator.alloc([]const u8, 1 + arr.len); + new_arr[0] = existing_s; + @memcpy(new_arr[1..], arr); + try result.owned_class_arrays.append(allocator, new_arr); + result.entries.items[idx].value = .{ .class_array = new_arr }; + }, + .none => { + result.entries.items[idx].value = .{ .class_array = arr }; + }, + } + }, + .none => { + // null class, convert existing to array if string + switch (existing) { + .string => |existing_s| { + const new_arr = try allocator.alloc([]const u8, 1); + new_arr[0] = existing_s; + try result.owned_class_arrays.append(allocator, new_arr); + result.entries.items[idx].value = .{ .class_array = new_arr }; + }, + else => {}, + } + }, + } +} + +/// Add a new class entry (first occurrence) +fn addNewClassEntry(result: *MergedAttrs, value: MergedAttrValue) !void { + const allocator = result.allocator; + switch (value) { + .string => |s| { + const new_arr = try allocator.alloc([]const u8, 1); + new_arr[0] = s; + try result.owned_class_arrays.append(allocator, new_arr); + result.class_idx = result.entries.items.len; + try result.entries.append(allocator, .{ .key = "class", .value = .{ .class_array = new_arr } }); + }, + .class_array => |arr| { + result.class_idx = result.entries.items.len; + try result.entries.append(allocator, .{ .key = "class", .value = .{ .class_array = arr } }); + }, + .none => {}, + } +} + +/// Merge a style value with existing style at index +fn mergeStyleValue(result: *MergedAttrs, idx: usize, value: MergedAttrValue) !void { + const allocator = result.allocator; + const existing = result.entries.items[idx].value; + + switch (value) { + .string => |s| { + switch (existing) { + .string => |existing_s| { + // Concatenate styles with semicolons + const s1 = try ensureTrailingSemicolon(allocator, existing_s); + defer allocator.free(s1); + const s2 = try ensureTrailingSemicolon(allocator, s); + defer allocator.free(s2); + + var combined: ArrayListUnmanaged(u8) = .{}; + errdefer combined.deinit(allocator); + try combined.appendSlice(allocator, s1); + try combined.appendSlice(allocator, s2); + const combined_str = try combined.toOwnedSlice(allocator); + try result.owned_strings.append(allocator, combined_str); + result.entries.items[idx].value = .{ .string = combined_str }; + }, + .none => { + const s_with_semi = try ensureTrailingSemicolon(allocator, s); + try result.owned_strings.append(allocator, s_with_semi); + result.entries.items[idx].value = .{ .string = s_with_semi }; + }, + else => {}, + } + }, + .none => { + // null style, ensure existing has trailing semicolon + switch (existing) { + .string => |existing_s| { + const s_with_semi = try ensureTrailingSemicolon(allocator, existing_s); + try result.owned_strings.append(allocator, s_with_semi); + result.entries.items[idx].value = .{ .string = s_with_semi }; + }, + else => {}, + } + }, + else => {}, + } +} + +/// Add a new style entry (first occurrence) +fn addNewStyleEntry(result: *MergedAttrs, value: MergedAttrValue) !void { + const allocator = result.allocator; + switch (value) { + .string => |s| { + const s_with_semi = try ensureTrailingSemicolon(allocator, s); + try result.owned_strings.append(allocator, s_with_semi); + result.style_idx = result.entries.items.len; + try result.entries.append(allocator, .{ .key = "style", .value = .{ .string = s_with_semi } }); + }, + .none => {}, + else => {}, + } +} + +// ============================================================================ +// Rethrow function for error handling +// ============================================================================ + +pub const PugError = struct { + message: []const u8, + filename: ?[]const u8, + line: usize, + src: ?[]const u8, + formatted_message: ?[]const u8, + allocator: Allocator, + + pub fn init(allocator: Allocator, err_message: []const u8, filename: ?[]const u8, line: usize, src: ?[]const u8) !PugError { + var pug_err = PugError{ + .message = err_message, + .filename = filename, + .line = line, + .src = src, + .formatted_message = null, + .allocator = allocator, + }; + + // Format the error message with context + if (src) |s| { + pug_err.formatted_message = try formatErrorMessage(allocator, err_message, filename, line, s); + } + + return pug_err; + } + + pub fn deinit(self: *PugError) void { + if (self.formatted_message) |msg| { + self.allocator.free(msg); } } - /// Writes an inline tag from tag interpolation: #[em text] - fn writeInlineTag(self: *Runtime, tag: ast.InlineTag) Error!void { - try self.write("<"); - try self.write(tag.tag); - - // Write ID if present - if (tag.id) |id| { - try self.write(" id=\""); - try self.writeEscaped(id); - try self.write("\""); + pub fn getMessage(self: *const PugError) []const u8 { + if (self.formatted_message) |msg| { + return msg; } - - // Write classes if present - if (tag.classes.len > 0) { - try self.write(" class=\""); - for (tag.classes, 0..) |class, i| { - if (i > 0) try self.write(" "); - try self.writeEscaped(class); - } - try self.write("\""); - } - - // Write attributes - for (tag.attributes) |attr| { - if (attr.value) |value| { - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - const evaluated = try self.evaluateString(value); - if (attr.escaped) { - try self.writeEscaped(evaluated); - } else { - try self.write(evaluated); - } - try self.write("\""); - } else { - // Boolean attribute - try self.write(" "); - try self.write(attr.name); - try self.write("=\""); - try self.write(attr.name); - try self.write("\""); - } - } - - try self.write(">"); - - // Write text content (may contain nested interpolations) - try self.writeTextSegments(tag.text_segments); - - try self.write(""); + return self.message; } - - /// Writes spread attributes from an object literal: {'data-foo': 'bar', 'data-baz': 'qux'} - fn writeSpreadAttributes(self: *Runtime, spread: []const u8) Error!void { - const trimmed = std.mem.trim(u8, spread, " \t\n\r"); - - // Must start with { and end with } - if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') { - return; - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return; - - var pos: usize = 0; - while (pos < content.len) { - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; - - // Parse property name (may be quoted with ' or ") - var name_start = pos; - var name_end = pos; - if (content[pos] == '\'' or content[pos] == '"') { - const quote = content[pos]; - pos += 1; - name_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - name_end = pos; - if (pos < content.len) pos += 1; // skip closing quote - } else { - // Unquoted name - while (pos < content.len and content[pos] != ':' and content[pos] != ' ') { - pos += 1; - } - name_end = pos; - } - const name = content[name_start..name_end]; - - // Skip to colon - while (pos < content.len and content[pos] != ':') { - pos += 1; - } - if (pos >= content.len) break; - pos += 1; // skip : - - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) { - pos += 1; - } - - // Parse value (handle quoted strings) - var value_start = pos; - var value_end = pos; - if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; // skip closing quote - } else { - // Unquoted value - while (pos < content.len and content[pos] != ',' and content[pos] != '}') { - pos += 1; - } - value_end = pos; - // Trim trailing whitespace - while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) { - value_end -= 1; - } - } - const value = content[value_start..value_end]; - - // Write attribute - if (name.len > 0) { - try self.write(" "); - try self.write(name); - try self.write("=\""); - try self.writeEscaped(value); - try self.write("\""); - } - - // Skip comma - while (pos < content.len and (content[pos] == ' ' or content[pos] == ',' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - } - } - - fn writeIndent(self: *Runtime) Error!void { - if (!self.options.pretty) return; - for (0..self.depth) |_| { - try self.write(self.options.indent_str); - } - } - - fn writeNewline(self: *Runtime) Error!void { - if (!self.options.pretty) return; - try self.write("\n"); - } - - fn write(self: *Runtime, str: []const u8) Error!void { - // Use addManyAsSlice for potentially faster bulk copy - const dest = try self.output.addManyAsSlice(self.allocator, str.len); - @memcpy(dest, str); - } - - fn writeEscaped(self: *Runtime, str: []const u8) Error!void { - // Fast path: use SIMD-friendly byte scan for escape characters - // Check if any escaping needed using a simple loop (compiler can vectorize) - var escape_needed: usize = str.len; - for (str, 0..) |c, i| { - // Use a lookup instead of multiple comparisons - if (escape_table[c]) { - escape_needed = i; - break; - } - } - - // No escaping needed - single fast write - if (escape_needed == str.len) { - const dest = try self.output.addManyAsSlice(self.allocator, str.len); - @memcpy(dest, str); - return; - } - - // Write prefix that doesn't need escaping - if (escape_needed > 0) { - const dest = try self.output.addManyAsSlice(self.allocator, escape_needed); - @memcpy(dest, str[0..escape_needed]); - } - - // Slow path: escape remaining characters - var start = escape_needed; - for (str[escape_needed..], escape_needed..) |c, i| { - if (escape_table[c]) { - // Write accumulated non-escaped chars first - if (i > start) { - const chunk = str[start..i]; - const dest = try self.output.addManyAsSlice(self.allocator, chunk.len); - @memcpy(dest, chunk); - } - const esc = escape_strings[c]; - const dest = try self.output.addManyAsSlice(self.allocator, esc.len); - @memcpy(dest, esc); - start = i + 1; - } - } - // Write remaining non-escaped chars - if (start < str.len) { - const chunk = str[start..]; - const dest = try self.output.addManyAsSlice(self.allocator, chunk.len); - @memcpy(dest, chunk); - } - } - - /// Writes text content with HTML escaping (no quote escaping needed in text) - /// Preserves existing HTML entities (e.g., ’ stays as ’) - fn writeTextEscaped(self: *Runtime, str: []const u8) Error!void { - var i: usize = 0; - var start: usize = 0; - - while (i < str.len) { - const c = str[i]; - if (c == '&') { - // Check if this is an existing HTML entity - don't double-escape - if (isHtmlEntity(str[i..])) { - i += 1; - continue; - } - // Not an entity, escape the & - if (i > start) { - const chunk = str[start..i]; - const dest = try self.output.addManyAsSlice(self.allocator, chunk.len); - @memcpy(dest, chunk); - } - const esc = "&"; - const dest = try self.output.addManyAsSlice(self.allocator, esc.len); - @memcpy(dest, esc); - start = i + 1; - i += 1; - } else if (c == '<') { - if (i > start) { - const chunk = str[start..i]; - const dest = try self.output.addManyAsSlice(self.allocator, chunk.len); - @memcpy(dest, chunk); - } - const esc = "<"; - const dest = try self.output.addManyAsSlice(self.allocator, esc.len); - @memcpy(dest, esc); - start = i + 1; - i += 1; - } else if (c == '>') { - if (i > start) { - const chunk = str[start..i]; - const dest = try self.output.addManyAsSlice(self.allocator, chunk.len); - @memcpy(dest, chunk); - } - const esc = ">"; - const dest = try self.output.addManyAsSlice(self.allocator, esc.len); - @memcpy(dest, esc); - start = i + 1; - i += 1; - } else { - i += 1; - } - } - - if (start < str.len) { - const chunk = str[start..]; - const dest = try self.output.addManyAsSlice(self.allocator, chunk.len); - @memcpy(dest, chunk); - } - } - - /// Checks if string starts with an HTML entity (&#nnnn; or &#xhhhh; or &name;) - fn isHtmlEntity(str: []const u8) bool { - if (str.len < 3 or str[0] != '&') return false; - - var i: usize = 1; - if (str[i] == '#') { - // Numeric entity: &#nnnn; or &#xhhhh; - i += 1; - if (i >= str.len) return false; - - if (str[i] == 'x' or str[i] == 'X') { - // Hex: &#xhhhh; - i += 1; - var has_hex = false; - while (i < str.len and i < 10) : (i += 1) { - const c = str[i]; - if (c == ';') return has_hex; - if ((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F')) { - has_hex = true; - } else { - return false; - } - } - } else { - // Decimal: &#nnnn; - var has_digit = false; - while (i < str.len and i < 10) : (i += 1) { - const c = str[i]; - if (c == ';') return has_digit; - if (c >= '0' and c <= '9') { - has_digit = true; - } else { - return false; - } - } - } - } else { - // Named entity: &name; - var has_alpha = false; - while (i < str.len and i < 32) : (i += 1) { - const c = str[i]; - if (c == ';') return has_alpha; - if ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9')) { - has_alpha = true; - } else { - return false; - } - } - } - return false; - } - - /// Lookup table for characters that need HTML escaping (for attributes - includes quotes) - const escape_table = blk: { - var table: [256]bool = [_]bool{false} ** 256; - table['&'] = true; - table['<'] = true; - table['>'] = true; - table['"'] = true; - table['\''] = true; - break :blk table; - }; - - /// Escape strings for each character (for attributes) - const escape_strings = blk: { - var strings: [256][]const u8 = [_][]const u8{""} ** 256; - strings['&'] = "&"; - strings['<'] = "<"; - strings['>'] = ">"; - strings['"'] = """; - strings['\''] = "'"; - break :blk strings; - }; - - /// Lookup table for text content (no quotes - only &, <, >) - const text_escape_table = blk: { - var table: [256]bool = [_]bool{false} ** 256; - table['&'] = true; - table['<'] = true; - table['>'] = true; - break :blk table; - }; - - /// Escape strings for text content - const text_escape_strings = blk: { - var strings: [256][]const u8 = [_][]const u8{""} ** 256; - strings['&'] = "&"; - strings['<'] = "<"; - strings['>'] = ">"; - break :blk strings; - }; }; -// ───────────────────────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────────────────────── - -fn isVoidElement(tag: []const u8) bool { - const void_elements = std.StaticStringMap(void).initComptime(.{ - .{ "area", {} }, .{ "base", {} }, .{ "br", {} }, - .{ "col", {} }, .{ "embed", {} }, .{ "hr", {} }, - .{ "img", {} }, .{ "input", {} }, .{ "link", {} }, - .{ "meta", {} }, .{ "param", {} }, .{ "source", {} }, - .{ "track", {} }, .{ "wbr", {} }, - }); - return void_elements.has(tag); -} - -/// Whitespace-preserving elements - don't add indentation or extra newlines -fn isWhitespacePreserving(tag: []const u8) bool { - const ws_elements = std.StaticStringMap(void).initComptime(.{ - .{ "pre", {} }, - .{ "script", {} }, - .{ "style", {} }, - .{ "textarea", {} }, - }); - return ws_elements.has(tag); -} - -/// Checks if children can be rendered inline (for block expansion). -/// For inline rendering, the direct child element must have NO content at all -/// (no children, no inline_text, no buffered_code) OR be a void element. -/// e.g., `a: img` can be inline (img is void element) -/// `li: a(href='#') foo` - the `a` has inline_text so renders inline -/// but `li: .foo: #bar baz` cannot (div.foo has child #bar) -/// Checks if a parent element can render its children inline. -/// For block expansion (`:` syntax), inline rendering is only allowed when: -/// - Child has no element children AND -/// - Child was not created via block expansion (not chained) AND -/// - Child has no text/buffered content if parent is in a chain (child.is_inline check handles this) -fn canRenderInlineForParent(parent: ast.Element) bool { - for (parent.children) |child| { - switch (child) { - .element => |elem| { - // If child has element children, can't render inline - if (elem.children.len > 0) return false; - // If child was created via block expansion (chained `:` syntax), can't render inline - // This handles `li: .foo: #bar` where .foo has is_inline=true - if (elem.is_inline) return false; - // If child has content AND parent's child will itself be inline-rendered, - // we need to check if this is a chain. Since parent.is_inline is true (we're here), - // check if any child element has text - if the depth > 1, don't render inline. - // This is approximated by: if child has inline_text AND is followed by `:` somewhere in the chain - // But we can't easily detect chain depth here. - // For now, leave as is - the is_inline check above should handle most cases. - }, - else => {}, - } - } - return true; -} - -/// Parses a JS array literal and converts it to space-separated string. -/// Input: ['foo', 'bar', 'baz'] -/// Output: foo bar baz -fn parseArrayToSpaceSeparated(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { - const trimmed = std.mem.trim(u8, input, " \t\n\r"); - - // Must start with [ and end with ] - if (trimmed.len < 2 or trimmed[0] != '[' or trimmed[trimmed.len - 1] != ']') { - return input; // Not an array, return as-is - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return ""; - - var result = std.ArrayList(u8).empty; +fn formatErrorMessage(allocator: Allocator, err_message: []const u8, filename: ?[]const u8, line: usize, src: []const u8) ![]const u8 { + var result: ArrayListUnmanaged(u8) = .{}; errdefer result.deinit(allocator); - var pos: usize = 0; - var first = true; - while (pos < content.len) { - // Skip whitespace and commas - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == ',' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; + // Add filename and line + if (filename) |f| { + try result.appendSlice(allocator, f); + } + try result.append(allocator, ':'); - // Parse value (handle quoted strings) - var value_start = pos; - var value_end = pos; - if (content[pos] == '\'' or content[pos] == '"') { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; // skip closing quote - } else { - // Unquoted value - while (pos < content.len and content[pos] != ',' and content[pos] != ']') { - pos += 1; - } - value_end = pos; - // Trim trailing whitespace - while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) { - value_end -= 1; - } + // Format line number + var line_buf: [32]u8 = undefined; + const line_str = std.fmt.bufPrint(&line_buf, "{d}", .{line}) catch return error.FormatError; + try result.appendSlice(allocator, line_str); + try result.append(allocator, '\n'); + + // Split source into lines and show context + var lines_iter = mem.splitSequence(u8, src, "\n"); + var line_num: usize = 1; + while (lines_iter.next()) |src_line| { + // Show lines around the error (context window) + const start_line = if (line > 3) line - 3 else 1; + const end_line = line + 3; + + if (line_num >= start_line and line_num <= end_line) { + // Line number prefix + var num_buf: [32]u8 = undefined; + const num_str = std.fmt.bufPrint(&num_buf, "{d: >4}| ", .{line_num}) catch return error.FormatError; + try result.appendSlice(allocator, num_str); + try result.appendSlice(allocator, src_line); + try result.append(allocator, '\n'); } - const value = content[value_start..value_end]; - if (value.len > 0) { - if (!first) { - try result.append(allocator, ' '); - } - try result.appendSlice(allocator, value); - first = false; - } + line_num += 1; + + if (line_num > end_line) break; } - return result.toOwnedSlice(allocator); + // Add the original error message + try result.appendSlice(allocator, err_message); + + return try result.toOwnedSlice(allocator); } -/// Parses a JS object literal and converts it to CSS style string. -/// Input: {color: 'red', background: 'green'} -/// Output: color:red;background:green; -fn parseObjectToCSS(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { - const trimmed = std.mem.trim(u8, input, " \t\n\r"); - - // Must start with { and end with } - if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') { - return input; // Not an object, return as-is - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return ""; - - var result = std.ArrayList(u8).empty; - errdefer result.deinit(allocator); - - var pos: usize = 0; - while (pos < content.len) { - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; - - // Parse property name - const name_start = pos; - while (pos < content.len and content[pos] != ':' and content[pos] != ' ') { - pos += 1; - } - const name = content[name_start..pos]; - - // Skip to colon - while (pos < content.len and content[pos] != ':') { - pos += 1; - } - if (pos >= content.len) break; - pos += 1; // skip : - - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) { - pos += 1; - } - - // Parse value (handle quoted strings) - var value_start = pos; - var value_end = pos; - if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; // skip closing quote - } else { - // Unquoted value - while (pos < content.len and content[pos] != ',' and content[pos] != '}') { - pos += 1; - } - value_end = pos; - // Trim trailing whitespace from value - while (value_end > value_start and (content[value_end - 1] == ' ' or content[value_end - 1] == '\t')) { - value_end -= 1; - } - } - const value = content[value_start..value_end]; - - // Append property:value; - try result.appendSlice(allocator, name); - try result.append(allocator, ':'); - try result.appendSlice(allocator, value); - try result.append(allocator, ';'); - - // Skip comma - while (pos < content.len and (content[pos] == ' ' or content[pos] == ',' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - } - - return result.toOwnedSlice(allocator); +/// Rethrow an error with file context. +/// Creates a PugError with formatted message including source line context. +pub fn rethrow(allocator: Allocator, err_message: []const u8, filename: ?[]const u8, line: usize, src: ?[]const u8) !PugError { + return try PugError.init(allocator, err_message, filename, line, src); } -/// Parses a JS object literal for class attribute and returns space-separated class names. -/// Only includes keys where the value is truthy (true, non-empty string, non-zero number). -/// Input: {foo: true, bar: false, baz: true} -/// Output: foo baz -fn parseObjectToClassList(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { - const trimmed = std.mem.trim(u8, input, " \t\n\r"); - - // Must start with { and end with } - if (trimmed.len < 2 or trimmed[0] != '{' or trimmed[trimmed.len - 1] != '}') { - return input; // Not an object, return as-is - } - - const content = std.mem.trim(u8, trimmed[1 .. trimmed.len - 1], " \t\n\r"); - if (content.len == 0) return ""; - - var result = std.ArrayList(u8).empty; - errdefer result.deinit(allocator); - - var pos: usize = 0; - while (pos < content.len) { - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - if (pos >= content.len) break; - - // Parse property name (class name) - const name_start = pos; - while (pos < content.len and content[pos] != ':' and content[pos] != ' ' and content[pos] != ',') { - pos += 1; - } - const name = content[name_start..pos]; - - // Skip to colon - while (pos < content.len and content[pos] != ':') { - pos += 1; - } - if (pos >= content.len) break; - pos += 1; // skip : - - // Skip whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == '\t')) { - pos += 1; - } - - // Parse value - var value_start = pos; - var value_end = pos; - if (pos < content.len and (content[pos] == '\'' or content[pos] == '"')) { - const quote = content[pos]; - pos += 1; - value_start = pos; - while (pos < content.len and content[pos] != quote) { - pos += 1; - } - value_end = pos; - if (pos < content.len) pos += 1; // skip closing quote - } else { - // Unquoted value (true, false, number, variable) - while (pos < content.len and content[pos] != ',' and content[pos] != '}' and content[pos] != ' ') { - pos += 1; - } - value_end = pos; - } - const value = std.mem.trim(u8, content[value_start..value_end], " \t"); - - // Check if value is truthy - const is_truthy = !std.mem.eql(u8, value, "false") and - !std.mem.eql(u8, value, "null") and - !std.mem.eql(u8, value, "undefined") and - !std.mem.eql(u8, value, "0") and - !std.mem.eql(u8, value, "''") and - !std.mem.eql(u8, value, "\"\"") and - value.len > 0; - - if (is_truthy and name.len > 0) { - if (result.items.len > 0) { - try result.append(allocator, ' '); - } - try result.appendSlice(allocator, name); - } - - // Skip comma and whitespace - while (pos < content.len and (content[pos] == ' ' or content[pos] == ',' or content[pos] == '\t' or content[pos] == '\n' or content[pos] == '\r')) { - pos += 1; - } - } - - return result.toOwnedSlice(allocator); -} - -// ───────────────────────────────────────────────────────────────────────────── -// Convenience function -// ───────────────────────────────────────────────────────────────────────────── - -/// Compiles and renders a template string with the given data context. -/// This is the simplest API for server use - one function call does everything. -/// -/// **Recommended:** Use an arena allocator for automatic cleanup: -/// ```zig -/// var arena = std.heap.ArenaAllocator.init(base_allocator); -/// defer arena.deinit(); // Frees all template memory at once -/// -/// const html = try pugz.renderTemplate(arena.allocator(), -/// \\html -/// \\ head -/// \\ title= title -/// \\ body -/// \\ h1 Hello, #{name}! -/// , .{ .title = "My Page", .name = "World" }); -/// // Use html... arena.deinit() frees everything -/// ``` -pub fn renderTemplate(allocator: std.mem.Allocator, source: []const u8, data: anytype) ![]u8 { - // Tokenize - var lexer = Lexer.init(allocator, source); - defer lexer.deinit(); - const tokens = lexer.tokenize() catch return error.ParseError; - - // Parse - var parser = Parser.init(allocator, tokens); - const doc = parser.parse() catch return error.ParseError; - - // Render with data - return render(allocator, doc, data); -} - -/// Renders a pre-parsed document with the given data context. -/// Use this when you want to parse once and render multiple times with different data. -/// Options for render function. -pub const RenderOptions = struct { - pretty: bool = true, -}; - -pub fn render(allocator: std.mem.Allocator, doc: ast.Document, data: anytype) ![]u8 { - return renderWithOptions(allocator, doc, data, .{}); -} - -pub fn renderWithOptions(allocator: std.mem.Allocator, doc: ast.Document, data: anytype, opts: RenderOptions) ![]u8 { - var ctx = Context.init(allocator); - defer ctx.deinit(); - - // Populate context from data struct - try ctx.pushScope(); - inline for (std.meta.fields(@TypeOf(data))) |field| { - const value = @field(data, field.name); - try ctx.set(field.name, toValue(allocator, value)); - } - - var runtime = Runtime.init(allocator, &ctx, .{ .pretty = opts.pretty }); - defer runtime.deinit(); - - return runtime.renderOwned(doc); -} - -/// Converts a Zig value to a runtime Value. -/// For best performance, use an arena allocator. -pub fn toValue(allocator: std.mem.Allocator, v: anytype) Value { - const T = @TypeOf(v); - - if (T == Value) return v; - - switch (@typeInfo(T)) { - .bool => return Value.boolean(v), - .int, .comptime_int => return Value.integer(@intCast(v)), - .float, .comptime_float => return .{ .float = @floatCast(v) }, - .pointer => |ptr| { - // Handle *const [N]u8 (string literals) - if (ptr.size == .one) { - const child_info = @typeInfo(ptr.child); - if (child_info == .array and child_info.array.child == u8) { - return Value.str(v); - } - // Handle pointer to array of non-u8 (e.g., *const [3][]const u8) - if (child_info == .array) { - const arr = allocator.alloc(Value, child_info.array.len) catch return Value.null; - for (v, 0..) |item, i| { - arr[i] = toValue(allocator, item); - } - return .{ .array = arr }; - } - } - // Handle []const u8 and []u8 - if (ptr.size == .slice and ptr.child == u8) { - return Value.str(v); - } - if (ptr.size == .slice) { - // Convert slice to array value - const arr = allocator.alloc(Value, v.len) catch return Value.null; - for (v, 0..) |item, i| { - arr[i] = toValue(allocator, item); - } - return .{ .array = arr }; - } - return Value.null; - }, - .optional => { - if (v) |inner| { - return toValue(allocator, inner); - } - return Value.null; - }, - .@"struct" => |info| { - // Convert struct to object - pre-allocate for known field count - var obj = std.StringHashMapUnmanaged(Value).empty; - obj.ensureTotalCapacity(allocator, info.fields.len) catch return Value.null; - inline for (info.fields) |field| { - const field_value = @field(v, field.name); - obj.putAssumeCapacity(field.name, toValue(allocator, field_value)); - } - return .{ .object = obj }; - }, - else => return Value.null, - } -} - -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ // Tests -// ───────────────────────────────────────────────────────────────────────────── +// ============================================================================ -test "context variable lookup" { +test "escape - no escaping needed" { const allocator = std.testing.allocator; - var ctx = Context.init(allocator); - defer ctx.deinit(); - - try ctx.pushScope(); - try ctx.set("name", Value.str("World")); - try ctx.set("count", Value.integer(42)); - - try std.testing.expectEqualStrings("World", ctx.get("name").?.string); - try std.testing.expectEqual(@as(i64, 42), ctx.get("count").?.int); - try std.testing.expect(ctx.get("undefined") == null); + const result = try escape(allocator, "foo"); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo", result); } -test "context scoping" { +test "escape - less than" { const allocator = std.testing.allocator; - var ctx = Context.init(allocator); - defer ctx.deinit(); - - try ctx.pushScope(); - try ctx.set("x", Value.integer(1)); - - try ctx.pushScope(); - try ctx.set("x", Value.integer(2)); - try std.testing.expectEqual(@as(i64, 2), ctx.get("x").?.int); - - ctx.popScope(); - try std.testing.expectEqual(@as(i64, 1), ctx.get("x").?.int); + const result = try escape(allocator, "fooHello, World!

\n", html); +test "escape - all special chars" { + const allocator = std.testing.allocator; + const result = try escape(allocator, "foo&<>\"bar\""); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo&<>"bar"", result); +} + +test "style - empty string" { + const allocator = std.testing.allocator; + const result = try style(allocator, .{ .string = "" }); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "style - none" { + const allocator = std.testing.allocator; + const result = try style(allocator, .none); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "style - string passthrough" { + const allocator = std.testing.allocator; + const result = try style(allocator, .{ .string = "foo: bar" }); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo: bar", result); +} + +test "style - object" { + const allocator = std.testing.allocator; + const props = [_]StyleProperty{ + .{ .name = "foo", .value = "bar" }, + }; + const result = try style(allocator, .{ .object = &props }); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo:bar;", result); +} + +test "style - object multiple" { + const allocator = std.testing.allocator; + const props = [_]StyleProperty{ + .{ .name = "foo", .value = "bar" }, + .{ .name = "baz", .value = "bash" }, + }; + const result = try style(allocator, .{ .object = &props }); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo:bar;baz:bash;", result); +} + +test "attr - boolean true terse" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = true }, true, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key", result); +} + +test "attr - boolean true not terse" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = true }, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"key\"", result); +} + +test "attr - boolean false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = false }, true, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - none" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .none, true, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - number" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .number = 500 }, true, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"500\"", result); +} + +test "attr - string" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo" }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo\"", result); +} + +test "attr - string escaped" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo>bar" }, true, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo>bar\"", result); +} + +test "attr - empty class" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "class", .{ .string = "" }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - empty style" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "style", .{ .string = "" }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "classes - string array" { + const allocator = std.testing.allocator; + const items = [_]ClassValue{ + .{ .string = "foo" }, + .{ .string = "bar" }, + }; + const result = try classes(allocator, .{ .array = &items }, null); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo bar", result); +} + +test "classes - nested array" { + const allocator = std.testing.allocator; + const inner1 = [_]ClassValue{ + .{ .string = "foo" }, + .{ .string = "bar" }, + }; + const inner2 = [_]ClassValue{ + .{ .string = "baz" }, + .{ .string = "bash" }, + }; + const items = [_]ClassValue{ + .{ .array = &inner1 }, + .{ .array = &inner2 }, + }; + const result = try classes(allocator, .{ .array = &items }, null); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo bar baz bash", result); +} + +test "classes - object" { + const allocator = std.testing.allocator; + const conditions = [_]ClassCondition{ + .{ .name = "baz", .condition = true }, + .{ .name = "bash", .condition = false }, + }; + const result = try classes(allocator, .{ .object = &conditions }, null); + defer allocator.free(result); + try std.testing.expectEqualStrings("baz", result); +} + +test "classes - mixed array and object" { + const allocator = std.testing.allocator; + const inner = [_]ClassValue{ + .{ .string = "foo" }, + .{ .string = "bar" }, + }; + const conditions = [_]ClassCondition{ + .{ .name = "baz", .condition = true }, + .{ .name = "bash", .condition = false }, + }; + const items = [_]ClassValue{ + .{ .array = &inner }, + .{ .object = &conditions }, + }; + const result = try classes(allocator, .{ .array = &items }, null); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo bar baz", result); +} + +test "classes - with escaping" { + const allocator = std.testing.allocator; + const inner = [_]ClassValue{ + .{ .string = "foz", result); +} + +test "attrs - simple" { + const allocator = std.testing.allocator; + const entries = [_]AttrEntry{ + .{ .key = "foo", .value = .{ .string = "bar" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" foo=\"bar\"", result); +} + +test "attrs - multiple" { + const allocator = std.testing.allocator; + const entries = [_]AttrEntry{ + .{ .key = "foo", .value = .{ .string = "bar" } }, + .{ .key = "hoo", .value = .{ .string = "boo" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" foo=\"bar\" hoo=\"boo\"", result); +} + +test "attrs - with class" { + const allocator = std.testing.allocator; + const class_items = [_]ClassValue{ + .{ .string = "foo" }, + .{ .object = &[_]ClassCondition{.{ .name = "bar", .condition = true }} }, + }; + const entries = [_]AttrEntry{ + .{ .key = "class", .value = .none, .is_class = true, .class_value = .{ .array = &class_items } }, + .{ .key = "foo", .value = .{ .string = "bar" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" class=\"foo bar\" foo=\"bar\"", result); +} + +test "attrs - with style object" { + const allocator = std.testing.allocator; + const style_props = [_]StyleProperty{ + .{ .name = "foo", .value = "bar" }, + }; + const entries = [_]AttrEntry{ + .{ .key = "style", .value = .none, .is_style = true, .style_value = .{ .object = &style_props } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" style=\"foo:bar;\"", result); +} + +// ============================================================================ +// Additional tests from index.test.js +// ============================================================================ + +// attr tests - boolean combinations +test "attr - boolean true escaped=false terse=true" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = true }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key", result); +} + +test "attr - boolean true escaped=true terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = true }, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"key\"", result); +} + +test "attr - boolean true escaped=false terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = true }, false, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"key\"", result); +} + +test "attr - boolean false escaped=false terse=true" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = false }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - boolean false escaped=true terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = false }, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - boolean false escaped=false terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .boolean = false }, false, false); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - none escaped=false terse=true" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .none, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - none escaped=true terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .none, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attr - none escaped=false terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .none, false, false); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +// attr number combinations +test "attr - number escaped=false terse=true" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .number = 500 }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"500\"", result); +} + +test "attr - number escaped=true terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .number = 500 }, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"500\"", result); +} + +test "attr - number escaped=false terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .number = 500 }, false, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"500\"", result); +} + +// attr string combinations +test "attr - string escaped=true terse=true" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo" }, true, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo\"", result); +} + +test "attr - string escaped=true terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo" }, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo\"", result); +} + +test "attr - string escaped=false terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo" }, false, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo\"", result); +} + +test "attr - string with > escaped=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo>bar" }, false, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo>bar\"", result); +} + +test "attr - string with > escaped=true terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo>bar" }, true, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo>bar\"", result); +} + +test "attr - string with > escaped=false terse=false" { + const allocator = std.testing.allocator; + const result = try attr(allocator, "key", .{ .string = "foo>bar" }, false, false); + defer allocator.free(result); + try std.testing.expectEqualStrings(" key=\"foo>bar\"", result); +} + +// attrs tests +test "attrs - empty string value" { + const allocator = std.testing.allocator; + const entries = [_]AttrEntry{ + .{ .key = "foo", .value = .{ .string = "" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" foo=\"\"", result); +} + +test "attrs - empty class" { + const allocator = std.testing.allocator; + const entries = [_]AttrEntry{ + .{ .key = "class", .value = .{ .string = "" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); +} + +test "attrs - style string" { + const allocator = std.testing.allocator; + const entries = [_]AttrEntry{ + .{ .key = "style", .value = .{ .string = "foo: bar;" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" style=\"foo: bar;\"", result); +} + +test "attrs - class first then foo" { + const allocator = std.testing.allocator; + const class_items = [_]ClassValue{ + .{ .string = "foo" }, + .{ .object = &[_]ClassCondition{.{ .name = "bar", .condition = true }} }, + }; + const entries = [_]AttrEntry{ + .{ .key = "class", .value = .none, .is_class = true, .class_value = .{ .array = &class_items } }, + .{ .key = "foo", .value = .{ .string = "bar" } }, + }; + const result = try attrs(allocator, &entries, true); + defer allocator.free(result); + try std.testing.expectEqualStrings(" class=\"foo bar\" foo=\"bar\"", result); +} + +test "attrs - foo then class reordered" { + const allocator = std.testing.allocator; + const class_items = [_]ClassValue{ + .{ .string = "foo" }, + .{ .object = &[_]ClassCondition{.{ .name = "bar", .condition = true }} }, + }; + const entries = [_]AttrEntry{ + .{ .key = "foo", .value = .{ .string = "bar" } }, + .{ .key = "class", .value = .none, .is_class = true, .class_value = .{ .array = &class_items } }, + }; + const result = try attrs(allocator, &entries, false); + defer allocator.free(result); + // Class should come first even if listed second + try std.testing.expectEqualStrings(" class=\"foo bar\" foo=\"bar\"", result); +} + +// style tests +test "style - string with trailing semicolon" { + const allocator = std.testing.allocator; + const result = try style(allocator, .{ .string = "foo: bar;" }); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo: bar;", result); +} + +// escape tests - additional +test "escape - ampersand less than greater than" { + const allocator = std.testing.allocator; + const result = try escape(allocator, "foo&<>bar"); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo&<>bar", result); +} + +test "escape - ampersand less than greater than quote" { + const allocator = std.testing.allocator; + const result = try escape(allocator, "foo&<>\"bar"); + defer allocator.free(result); + try std.testing.expectEqualStrings("foo&<>"bar", result); +} + +// ============================================================================ +// Merge tests from index.test.js +// ============================================================================ + +test "merge - simple merge" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "foo", .value = .{ .string = "bar" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "baz", .value = .{ .string = "bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + try std.testing.expectEqual(@as(usize, 2), result.entries.items.len); + try std.testing.expectEqualStrings("bar", result.get("foo").?.string); + try std.testing.expectEqualStrings("bash", result.get("baz").?.string); +} + +test "merge - class string + class string" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .string = "bar" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .string = "bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 2), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); + try std.testing.expectEqualStrings("bash", class_val.class_array[1]); +} + +test "merge - class array + class string" { + const allocator = std.testing.allocator; + const class_arr = [_][]const u8{"bar"}; + const a = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .string = "bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 2), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); + try std.testing.expectEqualStrings("bash", class_val.class_array[1]); +} + +test "merge - class string + class array" { + const allocator = std.testing.allocator; + const class_arr = [_][]const u8{"bash"}; + const a = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .string = "bar" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 2), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); + try std.testing.expectEqualStrings("bash", class_val.class_array[1]); +} + +test "merge - class string + class null" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .string = "bar" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "class", .value = .none }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); +} + +test "merge - class null + class array" { + const allocator = std.testing.allocator; + const class_arr = [_][]const u8{"bar"}; + const a = [_]MergedAttrEntry{ + .{ .key = "class", .value = .none }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); +} + +test "merge - empty + class array" { + const allocator = std.testing.allocator; + const class_arr = [_][]const u8{"bar"}; + const a = [_]MergedAttrEntry{}; + const b = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); +} + +test "merge - class array + empty" { + const allocator = std.testing.allocator; + const class_arr = [_][]const u8{"bar"}; + const a = [_]MergedAttrEntry{ + .{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } }, + }; + const b = [_]MergedAttrEntry{}; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const class_val = result.get("class").?; + try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len); + try std.testing.expectEqualStrings("bar", class_val.class_array[0]); +} + +test "merge - style string + style string" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "foo:bar" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "baz:bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const style_val = result.get("style").?; + try std.testing.expectEqualStrings("foo:bar;baz:bash;", style_val.string); +} + +test "merge - style with semicolon + style string" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "foo:bar;" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "baz:bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const style_val = result.get("style").?; + try std.testing.expectEqualStrings("foo:bar;baz:bash;", style_val.string); +} + +test "merge - style string + style null" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "foo:bar" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "style", .value = .none }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const style_val = result.get("style").?; + try std.testing.expectEqualStrings("foo:bar;", style_val.string); +} + +test "merge - style with semicolon + style null" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "foo:bar;" } }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "style", .value = .none }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const style_val = result.get("style").?; + try std.testing.expectEqualStrings("foo:bar;", style_val.string); +} + +test "merge - style null + style string" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{ + .{ .key = "style", .value = .none }, + }; + const b = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "baz:bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const style_val = result.get("style").?; + try std.testing.expectEqualStrings("baz:bash;", style_val.string); +} + +test "merge - empty + style string" { + const allocator = std.testing.allocator; + const a = [_]MergedAttrEntry{}; + const b = [_]MergedAttrEntry{ + .{ .key = "style", .value = .{ .string = "baz:bash" } }, + }; + var result = try merge(allocator, &a, &b); + defer result.deinit(); + + const style_val = result.get("style").?; + try std.testing.expectEqualStrings("baz:bash;", style_val.string); +} + +// ============================================================================ +// Rethrow tests +// ============================================================================ + +test "rethrow - basic error without src" { + const allocator = std.testing.allocator; + var pug_err = try rethrow(allocator, "test error", "foo.pug", 3, null); + defer pug_err.deinit(); + + try std.testing.expectEqualStrings("test error", pug_err.getMessage()); + try std.testing.expectEqualStrings("foo.pug", pug_err.filename.?); + try std.testing.expectEqual(@as(usize, 3), pug_err.line); +} + +test "rethrow - error with src shows context" { + const allocator = std.testing.allocator; + var pug_err = try rethrow(allocator, "test error", "foo.pug", 1, "hello world"); + defer pug_err.deinit(); + + const msg = pug_err.getMessage(); + // Should contain filename:line, source line, and error message + try std.testing.expect(mem.indexOf(u8, msg, "foo.pug:1") != null); + try std.testing.expect(mem.indexOf(u8, msg, "hello world") != null); + try std.testing.expect(mem.indexOf(u8, msg, "test error") != null); } diff --git a/src/strip_comments.zig b/src/strip_comments.zig new file mode 100644 index 0000000..2254ae7 --- /dev/null +++ b/src/strip_comments.zig @@ -0,0 +1,353 @@ +// strip_comments.zig - Zig port of pug-strip-comments +// +// Filters out comment tokens from a token stream. +// Handles both buffered and unbuffered comments with pipeless text support. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +// Import token types from lexer +const lexer = @import("lexer.zig"); +pub const Token = lexer.Token; +pub const TokenType = lexer.TokenType; + +// Import error types +const pug_error = @import("error.zig"); +pub const PugError = pug_error.PugError; + +// ============================================================================ +// Strip Comments Options +// ============================================================================ + +pub const StripCommentsOptions = struct { + /// Strip unbuffered comments (default: true) + strip_unbuffered: bool = true, + /// Strip buffered comments (default: false) + strip_buffered: bool = false, + /// Source filename for error messages + filename: ?[]const u8 = null, +}; + +// ============================================================================ +// Errors +// ============================================================================ + +pub const StripCommentsError = error{ + OutOfMemory, + UnexpectedToken, +}; + +// ============================================================================ +// Strip Comments Result +// ============================================================================ + +pub const StripCommentsResult = struct { + tokens: std.ArrayListUnmanaged(Token), + err: ?PugError = null, + + pub fn deinit(self: *StripCommentsResult, allocator: Allocator) void { + self.tokens.deinit(allocator); + } +}; + +// ============================================================================ +// Strip Comments Implementation +// ============================================================================ + +/// Strip comments from a token stream +/// Returns filtered tokens with comments removed based on options +pub fn stripComments( + allocator: Allocator, + input: []const Token, + options: StripCommentsOptions, +) StripCommentsError!StripCommentsResult { + var result = StripCommentsResult{ + .tokens = .{}, + }; + + // State tracking + var in_comment = false; + var in_pipeless_text = false; + var comment_is_buffered = false; + + for (input) |tok| { + const should_include = switch (tok.type) { + .comment => blk: { + if (in_comment) { + // Unexpected comment while already in comment + result.err = pug_error.makeError( + allocator, + "UNEXPECTED_TOKEN", + "`comment` encountered when already in a comment", + .{ + .line = tok.loc.start.line, + .column = tok.loc.start.column, + .filename = options.filename, + .src = null, + }, + ) catch null; + return error.UnexpectedToken; + } + // Check if this is a buffered comment + comment_is_buffered = tok.isBuffered(); + + // Determine if we should strip this comment + if (comment_is_buffered) { + in_comment = options.strip_buffered; + } else { + in_comment = options.strip_unbuffered; + } + break :blk !in_comment; + }, + + .start_pipeless_text => blk: { + if (!in_comment) { + break :blk true; + } + if (in_pipeless_text) { + // Unexpected start_pipeless_text + result.err = pug_error.makeError( + allocator, + "UNEXPECTED_TOKEN", + "`start-pipeless-text` encountered when already in pipeless text mode", + .{ + .line = tok.loc.start.line, + .column = tok.loc.start.column, + .filename = options.filename, + .src = null, + }, + ) catch null; + return error.UnexpectedToken; + } + in_pipeless_text = true; + break :blk false; + }, + + .end_pipeless_text => blk: { + if (!in_comment) { + break :blk true; + } + if (!in_pipeless_text) { + // Unexpected end_pipeless_text + result.err = pug_error.makeError( + allocator, + "UNEXPECTED_TOKEN", + "`end-pipeless-text` encountered when not in pipeless text mode", + .{ + .line = tok.loc.start.line, + .column = tok.loc.start.column, + .filename = options.filename, + .src = null, + }, + ) catch null; + return error.UnexpectedToken; + } + in_pipeless_text = false; + in_comment = false; + break :blk false; + }, + + // Text tokens right after comment but before pipeless text + .text, .text_html => !in_comment, + + // All other tokens + else => blk: { + if (in_pipeless_text) { + break :blk false; + } + in_comment = false; + break :blk true; + }, + }; + + if (should_include) { + try result.tokens.append(allocator, tok); + } + } + + return result; +} + +/// Convenience function - strip with default options (unbuffered only) +pub fn stripUnbufferedComments( + allocator: Allocator, + input: []const Token, +) StripCommentsError!StripCommentsResult { + return stripComments(allocator, input, .{}); +} + +/// Convenience function - strip all comments +pub fn stripAllComments( + allocator: Allocator, + input: []const Token, +) StripCommentsError!StripCommentsResult { + return stripComments(allocator, input, .{ + .strip_unbuffered = true, + .strip_buffered = true, + }); +} + +// ============================================================================ +// Tests +// ============================================================================ + +test "stripComments - no comments" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 2, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{}); + defer result.deinit(allocator); + + try std.testing.expectEqual(@as(usize, 3), result.tokens.items.len); +} + +test "stripComments - strip unbuffered comment" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } }, + .{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = false } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "comment text" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 16 } } }, + .{ .type = .tag, .loc = .{ .start = .{ .line = 3, .column = 1 } }, .val = .{ .string = "span" } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 4, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{}); + defer result.deinit(allocator); + + // Should strip comment and its text, keep tags and structure + try std.testing.expectEqual(@as(usize, 5), result.tokens.items.len); + try std.testing.expectEqual(TokenType.tag, result.tokens.items[0].type); + try std.testing.expectEqual(TokenType.newline, result.tokens.items[1].type); + try std.testing.expectEqual(TokenType.newline, result.tokens.items[2].type); + try std.testing.expectEqual(TokenType.tag, result.tokens.items[3].type); + try std.testing.expectEqual(TokenType.eos, result.tokens.items[4].type); +} + +test "stripComments - keep buffered comment by default" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } }, + .{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = true } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "buffered comment" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 20 } } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 3, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{}); + defer result.deinit(allocator); + + // Should keep buffered comment + try std.testing.expectEqual(@as(usize, 6), result.tokens.items.len); +} + +test "stripComments - strip buffered when option set" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "div" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 4 } } }, + .{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = true } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "buffered comment" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 20 } } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 3, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{ .strip_buffered = true }); + defer result.deinit(allocator); + + // Should strip buffered comment + try std.testing.expectEqual(@as(usize, 4), result.tokens.items.len); +} + +test "stripComments - pipeless text in comment" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .comment, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .buffer = .{ .boolean = false } }, + .{ .type = .start_pipeless_text, .loc = .{ .start = .{ .line = 1, .column = 1 } } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 3 } }, .val = .{ .string = "line 1" } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 3, .column = 3 } }, .val = .{ .string = "line 2" } }, + .{ .type = .end_pipeless_text, .loc = .{ .start = .{ .line = 4, .column = 1 } } }, + .{ .type = .tag, .loc = .{ .start = .{ .line = 5, .column = 1 } }, .val = .{ .string = "div" } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 6, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{}); + defer result.deinit(allocator); + + // Should strip everything in the comment including pipeless text + try std.testing.expectEqual(@as(usize, 2), result.tokens.items.len); + try std.testing.expectEqual(TokenType.tag, result.tokens.items[0].type); + try std.testing.expectEqual(TokenType.eos, result.tokens.items[1].type); +} + +test "stripComments - pipeless text outside comment" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .tag, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .val = .{ .string = "script" } }, + .{ .type = .dot, .loc = .{ .start = .{ .line = 1, .column = 7 } } }, + .{ .type = .start_pipeless_text, .loc = .{ .start = .{ .line = 1, .column = 8 } } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 3 } }, .val = .{ .string = "var x = 1;" } }, + .{ .type = .end_pipeless_text, .loc = .{ .start = .{ .line = 3, .column = 1 } } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 4, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{}); + defer result.deinit(allocator); + + // Should keep all tokens - no comments + try std.testing.expectEqual(@as(usize, 6), result.tokens.items.len); +} + +test "stripComments - keep unbuffered when option disabled" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .comment, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .buffer = .{ .boolean = false } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 1, .column = 4 } }, .val = .{ .string = "keep me" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 11 } } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 2, .column = 1 } } }, + }; + + var result = try stripComments(allocator, &tokens, .{ .strip_unbuffered = false }); + defer result.deinit(allocator); + + // Should keep unbuffered comment + try std.testing.expectEqual(@as(usize, 4), result.tokens.items.len); +} + +test "stripAllComments - strips both types" { + const allocator = std.testing.allocator; + + const tokens = [_]Token{ + .{ .type = .comment, .loc = .{ .start = .{ .line = 1, .column = 1 } }, .buffer = .{ .boolean = false } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 1, .column = 4 } }, .val = .{ .string = "unbuffered" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 1, .column = 14 } } }, + .{ .type = .comment, .loc = .{ .start = .{ .line = 2, .column = 1 } }, .buffer = .{ .boolean = true } }, + .{ .type = .text, .loc = .{ .start = .{ .line = 2, .column = 4 } }, .val = .{ .string = "buffered" } }, + .{ .type = .newline, .loc = .{ .start = .{ .line = 2, .column = 12 } } }, + .{ .type = .tag, .loc = .{ .start = .{ .line = 3, .column = 1 } }, .val = .{ .string = "div" } }, + .{ .type = .eos, .loc = .{ .start = .{ .line = 4, .column = 1 } } }, + }; + + var result = try stripAllComments(allocator, &tokens); + defer result.deinit(allocator); + + // Should strip both comments, keep tag and structure + try std.testing.expectEqual(@as(usize, 4), result.tokens.items.len); + try std.testing.expectEqual(TokenType.newline, result.tokens.items[0].type); + try std.testing.expectEqual(TokenType.newline, result.tokens.items[1].type); + try std.testing.expectEqual(TokenType.tag, result.tokens.items[2].type); + try std.testing.expectEqual(TokenType.eos, result.tokens.items[3].type); +} diff --git a/src/template.zig b/src/template.zig new file mode 100644 index 0000000..a9d2983 --- /dev/null +++ b/src/template.zig @@ -0,0 +1,683 @@ +// template.zig - Runtime template rendering with data binding +// +// This module provides runtime data binding for Pug templates. +// It allows passing a Zig struct and rendering dynamic content. +// Reuses utilities from runtime.zig for escaping and attribute rendering. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const pug = @import("pug.zig"); +const parser = @import("parser.zig"); +const Node = parser.Node; +const runtime = @import("runtime.zig"); + +pub const TemplateError = error{ + OutOfMemory, + LexerError, + ParserError, +}; + +/// Render context tracks state like doctype mode +pub const RenderContext = struct { + /// true = HTML5 terse mode (default), false = XHTML mode + terse: bool = true, +}; + +/// Render a template with data +pub fn renderWithData(allocator: Allocator, source: []const u8, data: anytype) ![]const u8 { + // Lex + var lex = pug.lexer.Lexer.init(allocator, source, .{}) catch return error.OutOfMemory; + defer lex.deinit(); + + const tokens = lex.getTokens() catch return error.LexerError; + + // Strip comments + var stripped = pug.strip_comments.stripComments(allocator, tokens, .{}) catch return error.OutOfMemory; + defer stripped.deinit(allocator); + + // Parse + var parse = pug.parser.Parser.init(allocator, stripped.tokens.items, null, source); + defer parse.deinit(); + + const ast = parse.parse() catch { + return error.ParserError; + }; + defer { + ast.deinit(allocator); + allocator.destroy(ast); + } + + // Render with data + var output = std.ArrayListUnmanaged(u8){}; + errdefer output.deinit(allocator); + + // Detect doctype to set terse mode + var ctx = RenderContext{}; + detectDoctype(ast, &ctx); + + try renderNode(allocator, &output, ast, data, &ctx); + + return output.toOwnedSlice(allocator); +} + +/// Scan AST for doctype and set terse mode accordingly +fn detectDoctype(node: *Node, ctx: *RenderContext) void { + if (node.type == .Doctype) { + if (node.val) |val| { + // XHTML doctypes use non-terse mode + if (std.mem.eql(u8, val, "xml") or + std.mem.eql(u8, val, "strict") or + std.mem.eql(u8, val, "transitional") or + std.mem.eql(u8, val, "frameset") or + std.mem.eql(u8, val, "1.1") or + std.mem.eql(u8, val, "basic") or + std.mem.eql(u8, val, "mobile")) + { + ctx.terse = false; + } + } + return; + } + + // Check children + for (node.nodes.items) |child| { + detectDoctype(child, ctx); + if (!ctx.terse) return; + } +} + +fn renderNode(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), node: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void { + switch (node.type) { + .Block, .NamedBlock => { + for (node.nodes.items) |child| { + try renderNode(allocator, output, child, data, ctx); + } + }, + .Tag, .InterpolatedTag => try renderTag(allocator, output, node, data, ctx), + .Text => try renderText(allocator, output, node, data), + .Code => try renderCode(allocator, output, node, data, ctx), + .Comment => try renderComment(allocator, output, node), + .BlockComment => try renderBlockComment(allocator, output, node, data, ctx), + .Doctype => try renderDoctype(allocator, output, node), + .Each => try renderEach(allocator, output, node, data, ctx), + .Mixin => { + // Mixin definitions are skipped (only mixin calls render) + if (!node.call) return; + for (node.nodes.items) |child| { + try renderNode(allocator, output, child, data, ctx); + } + }, + else => { + for (node.nodes.items) |child| { + try renderNode(allocator, output, child, data, ctx); + } + }, + } +} + +fn renderTag(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void { + const name = tag.name orelse "div"; + + try output.appendSlice(allocator, "<"); + try output.appendSlice(allocator, name); + + // Render attributes using runtime.attr() + for (tag.attrs.items) |attr| { + const attr_val = try evaluateAttrValue(allocator, attr.val, data); + const attr_str = runtime.attr(allocator, attr.name, attr_val, true, ctx.terse) catch |err| switch (err) { + error.FormatError => return error.OutOfMemory, + error.OutOfMemory => return error.OutOfMemory, + }; + defer allocator.free(attr_str); + try output.appendSlice(allocator, attr_str); + } + + // Self-closing logic differs by mode: + // - HTML5 terse: void elements are self-closing without /> + // - XHTML/XML: only explicit / makes tags self-closing + const is_void = isSelfClosing(name); + const is_self_closing = if (ctx.terse) + tag.self_closing or is_void + else + tag.self_closing; + + if (is_self_closing and tag.nodes.items.len == 0 and tag.val == null) { + if (ctx.terse and !tag.self_closing) { + try output.appendSlice(allocator, ">"); + } else { + try output.appendSlice(allocator, "/>"); + } + return; + } + + try output.appendSlice(allocator, ">"); + + // Render text content + if (tag.val) |val| { + try processInterpolation(allocator, output, val, false, data); + } + + // Render children + for (tag.nodes.items) |child| { + try renderNode(allocator, output, child, data, ctx); + } + + // Close tag + if (!is_self_closing) { + try output.appendSlice(allocator, ""); + } +} + +/// Evaluate attribute value from AST to runtime.AttrValue +fn evaluateAttrValue(allocator: Allocator, val: ?[]const u8, data: anytype) !runtime.AttrValue { + _ = allocator; + const v = val orelse return .{ .boolean = true }; // No value = boolean attribute + + // Handle boolean literals + if (std.mem.eql(u8, v, "true")) return .{ .boolean = true }; + if (std.mem.eql(u8, v, "false")) return .{ .boolean = false }; + if (std.mem.eql(u8, v, "null") or std.mem.eql(u8, v, "undefined")) return .none; + + // Quoted string - extract inner value + if (v.len >= 2 and (v[0] == '"' or v[0] == '\'')) { + return .{ .string = v[1 .. v.len - 1] }; + } + + // Expression - try to look up in data + if (getFieldValue(data, v)) |value| { + return .{ .string = value }; + } + + // Unknown expression - return as string literal + return .{ .string = v }; +} + +fn renderText(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: *Node, data: anytype) Allocator.Error!void { + if (text.val) |val| { + try processInterpolation(allocator, output, val, false, data); + } +} + +fn renderCode(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), code: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void { + if (code.buffer) { + if (code.val) |val| { + // Check if it's a string literal (quoted) + if (val.len >= 2 and (val[0] == '"' or val[0] == '\'')) { + const inner = val[1 .. val.len - 1]; + if (code.must_escape) { + try runtime.appendEscaped(allocator, output, inner); + } else { + try output.appendSlice(allocator, inner); + } + } else if (getFieldValue(data, val)) |value| { + if (code.must_escape) { + try runtime.appendEscaped(allocator, output, value); + } else { + try output.appendSlice(allocator, value); + } + } + } + } + + for (code.nodes.items) |child| { + try renderNode(allocator, output, child, data, ctx); + } +} + +fn renderEach(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), each: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void { + const collection_name = each.obj orelse return; + const item_name = each.val orelse "item"; + _ = item_name; + + const T = @TypeOf(data); + const info = @typeInfo(T); + + if (info != .@"struct") return; + + inline for (info.@"struct".fields) |field| { + if (std.mem.eql(u8, field.name, collection_name)) { + const collection = @field(data, field.name); + const CollType = @TypeOf(collection); + const coll_info = @typeInfo(CollType); + + if (coll_info == .pointer and coll_info.pointer.size == .slice) { + for (collection) |item| { + const ItemType = @TypeOf(item); + if (ItemType == []const u8) { + for (each.nodes.items) |child| { + try renderNodeWithItem(allocator, output, child, data, item, ctx); + } + } else { + for (each.nodes.items) |child| { + try renderNode(allocator, output, child, data, ctx); + } + } + } + return; + } + } + } + + if (each.alternate) |alt| { + try renderNode(allocator, output, alt, data, ctx); + } +} + +fn renderNodeWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), node: *Node, data: anytype, item: []const u8, ctx: *const RenderContext) Allocator.Error!void { + switch (node.type) { + .Block, .NamedBlock => { + for (node.nodes.items) |child| { + try renderNodeWithItem(allocator, output, child, data, item, ctx); + } + }, + .Tag, .InterpolatedTag => try renderTagWithItem(allocator, output, node, data, item, ctx), + .Text => try renderTextWithItem(allocator, output, node, item), + .Code => { + if (node.buffer) { + if (node.must_escape) { + try runtime.appendEscaped(allocator, output, item); + } else { + try output.appendSlice(allocator, item); + } + } + }, + else => { + for (node.nodes.items) |child| { + try renderNodeWithItem(allocator, output, child, data, item, ctx); + } + }, + } +} + +fn renderTagWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *Node, data: anytype, item: []const u8, ctx: *const RenderContext) Allocator.Error!void { + const name = tag.name orelse "div"; + + try output.appendSlice(allocator, "<"); + try output.appendSlice(allocator, name); + + // Render attributes using runtime.attr() + for (tag.attrs.items) |attr| { + const attr_val = try evaluateAttrValue(allocator, attr.val, data); + const attr_str = runtime.attr(allocator, attr.name, attr_val, true, ctx.terse) catch |err| switch (err) { + error.FormatError => return error.OutOfMemory, + error.OutOfMemory => return error.OutOfMemory, + }; + defer allocator.free(attr_str); + try output.appendSlice(allocator, attr_str); + } + + const is_void = isSelfClosing(name); + const is_self_closing = if (ctx.terse) + tag.self_closing or is_void + else + tag.self_closing; + + if (is_self_closing and tag.nodes.items.len == 0 and tag.val == null) { + if (ctx.terse and !tag.self_closing) { + try output.appendSlice(allocator, ">"); + } else { + try output.appendSlice(allocator, "/>"); + } + return; + } + + try output.appendSlice(allocator, ">"); + + if (tag.val) |val| { + try processInterpolationWithItem(allocator, output, val, true, data, item); + } + + for (tag.nodes.items) |child| { + try renderNodeWithItem(allocator, output, child, data, item, ctx); + } + + if (!is_self_closing) { + try output.appendSlice(allocator, ""); + } +} + +fn renderTextWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: *Node, item: []const u8) Allocator.Error!void { + if (text.val) |val| { + try runtime.appendEscaped(allocator, output, val); + _ = item; + } +} + +fn processInterpolationWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: []const u8, escape: bool, data: anytype, item: []const u8) Allocator.Error!void { + _ = data; + var i: usize = 0; + while (i < text.len) { + if (i + 1 < text.len and text[i] == '#' and text[i + 1] == '{') { + 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) { + if (escape) { + try runtime.appendEscaped(allocator, output, item); + } else { + try output.appendSlice(allocator, item); + } + i = j; + continue; + } + } + 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; + } +} + +fn renderComment(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), comment: *Node) Allocator.Error!void { + if (!comment.buffer) return; + try output.appendSlice(allocator, ""); +} + +fn renderBlockComment(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), comment: *Node, data: anytype, ctx: *const RenderContext) Allocator.Error!void { + if (!comment.buffer) return; + try output.appendSlice(allocator, ""); +} + +// Doctype mappings +const doctypes = std.StaticStringMap([]const u8).initComptime(.{ + .{ "html", "" }, + .{ "xml", "" }, + .{ "transitional", "" }, + .{ "strict", "" }, + .{ "frameset", "" }, + .{ "1.1", "" }, + .{ "basic", "" }, + .{ "mobile", "" }, + .{ "plist", "" }, +}); + +fn renderDoctype(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), doctype: *Node) Allocator.Error!void { + if (doctype.val) |val| { + if (doctypes.get(val)) |dt| { + try output.appendSlice(allocator, dt); + } else { + try output.appendSlice(allocator, ""); + } + } else { + try output.appendSlice(allocator, ""); + } +} + +/// Process interpolation #{expr} in text +/// escape_quotes: true for attribute values (escape "), false for text content +fn processInterpolation(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), text: []const u8, escape_quotes: bool, data: anytype) Allocator.Error!void { + var i: usize = 0; + while (i < text.len) { + if (i + 1 < text.len and text[i] == '#' and text[i + 1] == '{') { + 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) { + const expr = std.mem.trim(u8, text[i + 2 .. j - 1], " \t"); + if (getFieldValue(data, expr)) |value| { + if (escape_quotes) { + try runtime.appendEscaped(allocator, output, value); + } else { + // Text content: escape < > & but not quotes + try appendTextEscaped(allocator, output, value); + } + } + i = j; + continue; + } + } + // Regular character - use appropriate escaping + const c = text[i]; + if (escape_quotes) { + if (runtime.escapeChar(c)) |esc| { + try output.appendSlice(allocator, esc); + } else { + try output.append(allocator, c); + } + } else { + // Text content: escape < > & but not quotes, preserve HTML entities + switch (c) { + '<' => try output.appendSlice(allocator, "<"), + '>' => try output.appendSlice(allocator, ">"), + '&' => { + if (isHtmlEntity(text[i..])) { + try output.append(allocator, c); + } else { + try output.appendSlice(allocator, "&"); + } + }, + else => try output.append(allocator, c), + } + } + 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); + + if (ValueType == []const u8) { + return value; + } + + 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; +} + +/// Escape for text content - escapes < > & (NOT quotes) +/// Preserves existing HTML entities like ’ +fn appendTextEscaped(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), str: []const u8) Allocator.Error!void { + var i: usize = 0; + while (i < str.len) { + const c = str[i]; + switch (c) { + '<' => try output.appendSlice(allocator, "<"), + '>' => try output.appendSlice(allocator, ">"), + '&' => { + if (isHtmlEntity(str[i..])) { + try output.append(allocator, c); + } else { + try output.appendSlice(allocator, "&"); + } + }, + else => try output.append(allocator, c), + } + i += 1; + } +} + +/// Check if string starts with a valid HTML entity +fn isHtmlEntity(str: []const u8) bool { + if (str.len < 3 or str[0] != '&') return false; + + var i: usize = 1; + + // Numeric entity: &#digits; or &#xhex; + if (str[i] == '#') { + i += 1; + if (i >= str.len) return false; + + if (str[i] == 'x' or str[i] == 'X') { + i += 1; + if (i >= str.len) return false; + var has_hex = false; + while (i < str.len and i < 10) : (i += 1) { + const ch = str[i]; + if (ch == ';') return has_hex; + if ((ch >= '0' and ch <= '9') or + (ch >= 'a' and ch <= 'f') or + (ch >= 'A' and ch <= 'F')) + { + has_hex = true; + } else { + return false; + } + } + return false; + } + + var has_digit = false; + while (i < str.len and i < 10) : (i += 1) { + const ch = str[i]; + if (ch == ';') return has_digit; + if (ch >= '0' and ch <= '9') { + has_digit = true; + } else { + return false; + } + } + return false; + } + + // Named entity: &name; + var has_alpha = false; + while (i < str.len and i < 32) : (i += 1) { + const ch = str[i]; + if (ch == ';') return has_alpha; + if ((ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or (ch >= '0' and ch <= '9')) { + has_alpha = true; + } else { + return false; + } + } + return false; +} + +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/test-data/pug-attrs/index.test.js b/src/test-data/pug-attrs/index.test.js new file mode 100644 index 0000000..7bb8854 --- /dev/null +++ b/src/test-data/pug-attrs/index.test.js @@ -0,0 +1,301 @@ +'use strict'; + +var assert = require('assert'); +var utils = require('util'); +var attrs = require('../'); + +var options; +function test(input, expected, locals) { + var opts = options; + locals = locals || {}; + locals.pug = locals.pug || require('pug-runtime'); + it( + utils.inspect(input).replace(/\n/g, '') + ' => ' + utils.inspect(expected), + function() { + var src = attrs(input, opts); + var localKeys = Object.keys(locals).sort(); + var output = Function( + localKeys.join(', '), + 'return (' + src + ');' + ).apply( + null, + localKeys.map(function(key) { + return locals[key]; + }) + ); + if (opts.format === 'html') { + expect(output).toBe(expected); + } else { + expect(output).toEqual(expected); + } + } + ); +} +function withOptions(opts, fn) { + describe('options: ' + utils.inspect(opts), function() { + options = opts; + fn(); + }); +} + +withOptions( + { + terse: true, + format: 'html', + runtime: function(name) { + return 'pug.' + name; + }, + }, + function() { + test([], ''); + test([{name: 'foo', val: 'false', mustEscape: true}], ''); + test([{name: 'foo', val: 'true', mustEscape: true}], ' foo'); + test([{name: 'foo', val: false, mustEscape: true}], ''); + test([{name: 'foo', val: true, mustEscape: true}], ' foo'); + test([{name: 'foo', val: 'foo', mustEscape: true}], '', {foo: false}); + test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo', {foo: true}); + test([{name: 'foo', val: '"foo"', mustEscape: true}], ' foo="foo"'); + test( + [ + {name: 'foo', val: '"foo"', mustEscape: true}, + {name: 'bar', val: '"bar"', mustEscape: true}, + ], + ' foo="foo" bar="bar"' + ); + test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="fooo"', { + foo: 'fooo', + }); + test( + [ + {name: 'foo', val: 'foo', mustEscape: true}, + {name: 'bar', val: 'bar', mustEscape: true}, + ], + ' foo="fooo" bar="baro"', + {foo: 'fooo', bar: 'baro'} + ); + test( + [{name: 'style', val: '{color: "red"}', mustEscape: true}], + ' style="color:red;"' + ); + test( + [{name: 'style', val: '{color: color}', mustEscape: true}], + ' style="color:red;"', + {color: 'red'} + ); + test( + [ + {name: 'class', val: '"foo"', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + ' class="foo bar baz"' + ); + test( + [ + {name: 'class', val: '{foo: foo}', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + ' class="foo bar baz"', + {foo: true} + ); + test( + [ + {name: 'class', val: '{foo: foo}', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + ' class="bar baz"', + {foo: false} + ); + test( + [ + {name: 'class', val: 'foo', mustEscape: true}, + {name: 'class', val: '""', mustEscape: true}, + ], + ' class="<foo> <str>"', + {foo: ''} + ); + test( + [ + {name: 'foo', val: '"foo"', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + ' class="bar baz" foo="foo"' + ); + test( + [ + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + {name: 'foo', val: '"foo"', mustEscape: true}, + ], + ' class="bar baz" foo="foo"' + ); + test([{name: 'foo', val: '""', mustEscape: false}], ' foo=""'); + test( + [{name: 'foo', val: '""', mustEscape: true}], + ' foo="<foo>"' + ); + test([{name: 'foo', val: 'foo', mustEscape: false}], ' foo=""', { + foo: '', + }); + test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="<foo>"', { + foo: '', + }); + } +); +withOptions( + { + terse: false, + format: 'html', + runtime: function(name) { + return 'pug.' + name; + }, + }, + function() { + test([{name: 'foo', val: 'false', mustEscape: true}], ''); + test([{name: 'foo', val: 'true', mustEscape: true}], ' foo="foo"'); + test([{name: 'foo', val: false, mustEscape: true}], ''); + test([{name: 'foo', val: true, mustEscape: true}], ' foo="foo"'); + test([{name: 'foo', val: 'foo', mustEscape: true}], '', {foo: false}); + test([{name: 'foo', val: 'foo', mustEscape: true}], ' foo="foo"', { + foo: true, + }); + } +); + +withOptions( + { + terse: true, + format: 'object', + runtime: function(name) { + return 'pug.' + name; + }, + }, + function() { + test([], {}); + test([{name: 'foo', val: 'false', mustEscape: true}], {foo: false}); + test([{name: 'foo', val: 'true', mustEscape: true}], {foo: true}); + test([{name: 'foo', val: false, mustEscape: true}], {foo: false}); + test([{name: 'foo', val: true, mustEscape: true}], {foo: true}); + test( + [{name: 'foo', val: 'foo', mustEscape: true}], + {foo: false}, + {foo: false} + ); + test( + [{name: 'foo', val: 'foo', mustEscape: true}], + {foo: true}, + {foo: true} + ); + test([{name: 'foo', val: '"foo"', mustEscape: true}], {foo: 'foo'}); + test( + [ + {name: 'foo', val: '"foo"', mustEscape: true}, + {name: 'bar', val: '"bar"', mustEscape: true}, + ], + {foo: 'foo', bar: 'bar'} + ); + test( + [{name: 'foo', val: 'foo', mustEscape: true}], + {foo: 'fooo'}, + {foo: 'fooo'} + ); + test( + [ + {name: 'foo', val: 'foo', mustEscape: true}, + {name: 'bar', val: 'bar', mustEscape: true}, + ], + {foo: 'fooo', bar: 'baro'}, + {foo: 'fooo', bar: 'baro'} + ); + test([{name: 'style', val: '{color: "red"}', mustEscape: true}], { + style: 'color:red;', + }); + test( + [{name: 'style', val: '{color: color}', mustEscape: true}], + {style: 'color:red;'}, + {color: 'red'} + ); + test( + [ + {name: 'class', val: '"foo"', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + {class: 'foo bar baz'} + ); + test( + [ + {name: 'class', val: '{foo: foo}', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + {class: 'foo bar baz'}, + {foo: true} + ); + test( + [ + {name: 'class', val: '{foo: foo}', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + {class: 'bar baz'}, + {foo: false} + ); + test( + [ + {name: 'class', val: 'foo', mustEscape: true}, + {name: 'class', val: '""', mustEscape: true}, + ], + {class: '<foo> <str>'}, + {foo: ''} + ); + test( + [ + {name: 'foo', val: '"foo"', mustEscape: true}, + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + ], + {class: 'bar baz', foo: 'foo'} + ); + test( + [ + {name: 'class', val: '["bar", "baz"]', mustEscape: true}, + {name: 'foo', val: '"foo"', mustEscape: true}, + ], + {class: 'bar baz', foo: 'foo'} + ); + test([{name: 'foo', val: '""', mustEscape: false}], {foo: ''}); + test([{name: 'foo', val: '""', mustEscape: true}], { + foo: '<foo>', + }); + test( + [{name: 'foo', val: 'foo', mustEscape: false}], + {foo: ''}, + {foo: ''} + ); + test( + [{name: 'foo', val: 'foo', mustEscape: true}], + {foo: '<foo>'}, + {foo: ''} + ); + } +); +withOptions( + { + terse: false, + format: 'object', + runtime: function(name) { + return 'pug.' + name; + }, + }, + function() { + test([{name: 'foo', val: 'false', mustEscape: true}], {foo: false}); + test([{name: 'foo', val: 'true', mustEscape: true}], {foo: true}); + test([{name: 'foo', val: false, mustEscape: true}], {foo: false}); + test([{name: 'foo', val: true, mustEscape: true}], {foo: true}); + test( + [{name: 'foo', val: 'foo', mustEscape: true}], + {foo: false}, + {foo: false} + ); + test( + [{name: 'foo', val: 'foo', mustEscape: true}], + {foo: true}, + {foo: true} + ); + } +); diff --git a/src/test-data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap b/src/test-data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap new file mode 100644 index 0000000..a4c74bc --- /dev/null +++ b/src/test-data/pug-filters/test/__snapshots__/filter-aliases.test.js.snap @@ -0,0 +1,284 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`filters can be aliased 1`] = ` +Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 2, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "nodes": Array [ + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 4, + "type": "Text", + "val": "function myFunc(foo) {", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 5, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 5, + "type": "Text", + "val": " return foo;", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 6, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 6, + "type": "Text", + "val": "}", + }, + ], + "type": "Block", + }, + "column": 9, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "name": "minify", + "type": "Text", + "val": "function myFunc(n) { + return n; +} +", + }, + ], + "type": "Block", + }, + "column": 3, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "name": "cdata", + "type": "Text", + "val": "", + }, + ], + "type": "Block", + }, + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "isInline": false, + "line": 2, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`options are applied before aliases 1`] = ` +Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 2, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "nodes": Array [ + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 4, + "type": "Text", + "val": "function myFunc(foo) {", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 5, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 5, + "type": "Text", + "val": " return foo;", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 6, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 6, + "type": "Text", + "val": "}", + }, + ], + "type": "Block", + }, + "column": 9, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "name": "minify", + "type": "Text", + "val": "function myFunc(n) { + return n; +} +", + }, + ], + "type": "Block", + }, + "column": 3, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 3, + "name": "cdata", + "type": "Text", + "val": "", + }, + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 7, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 7, + "nodes": Array [ + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 8, + "type": "Text", + "val": "function myFunc(foo) {", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 9, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 9, + "type": "Text", + "val": " return foo;", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 10, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 10, + "type": "Text", + "val": "}", + }, + ], + "type": "Block", + }, + "column": 9, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 7, + "name": "uglify-js", + "type": "Text", + "val": "function myFunc(n) { + return n; +} +", + }, + ], + "type": "Block", + }, + "column": 3, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "line": 7, + "name": "cdata", + "type": "Text", + "val": "", + }, + ], + "type": "Block", + }, + "column": 1, + "filename": "/packages/pug-filters/test/filter-aliases.test.js", + "isInline": false, + "line": 2, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`we do not support chains of aliases 1`] = ` +Object { + "code": "PUG:FILTER_ALISE_CHAIN", + "message": "/packages/pug-filters/test/filter-aliases.test.js:3:9 + +The filter \\"minify-js\\" is an alias for \\"minify\\", which is an alias for \\"uglify-js\\". Pug does not support chains of filter aliases.", +} +`; diff --git a/src/test-data/pug-filters/test/__snapshots__/index.test.js.snap b/src/test-data/pug-filters/test/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..74a2f45 --- /dev/null +++ b/src/test-data/pug-filters/test/__snapshots__/index.test.js.snap @@ -0,0 +1,1074 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cases/filters.cdata.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Code\\", + \\"val\\": \\"users = [{ name: 'tobi', age: 2 }]\\", + \\"buffer\\": false, + \\"mustEscape\\": false, + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.cdata.tokens.json\\" + }, + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"fb:users\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Each\\", + \\"obj\\": \\"users\\", + \\"val\\": \\"user\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"fb:user\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"cdata\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"#{user.name}\\", + \\"line\\": 8 + } + ] + }, + \\"attrs\\": [], + \\"line\\": 7, + \\"filename\\": \\"filters.cdata.tokens.json\\", + \\"val\\": \\"\\" + } + ] + }, + \\"attrs\\": [ + { + \\"name\\": \\"age\\", + \\"val\\": \\"user.age\\", + \\"mustEscape\\": true + } + ], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 6, + \\"filename\\": \\"filters.cdata.tokens.json\\" + } + ], + \\"line\\": 6, + \\"filename\\": \\"filters.cdata.tokens.json\\" + }, + \\"line\\": 5, + \\"filename\\": \\"filters.cdata.tokens.json\\" + } + ] + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 4, + \\"filename\\": \\"filters.cdata.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.cdata.tokens.json\\" +}" +`; + +exports[`cases/filters.coffeescript.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"script\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"coffee-script\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"regexp = /\\\\\\\\n/\\", + \\"line\\": 3 + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.coffeescript.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 2, + \\"filename\\": \\"filters.coffeescript.tokens.json\\", + \\"val\\": \\"(function() {\\\\n var regexp;\\\\n\\\\n regexp = /\\\\\\\\n/;\\\\n\\\\n}).call(this);\\\\n\\" + }, + { + \\"type\\": \\"Text\\", + \\"name\\": \\"coffee-script\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"math =\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 6 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\" square: (value) -> value * value\\", + \\"line\\": 6 + } + ], + \\"line\\": 4, + \\"filename\\": \\"filters.coffeescript.tokens.json\\" + }, + \\"attrs\\": [ + { + \\"name\\": \\"minify\\", + \\"val\\": \\"true\\", + \\"mustEscape\\": true + } + ], + \\"line\\": 4, + \\"filename\\": \\"filters.coffeescript.tokens.json\\", + \\"val\\": \\"(function(){}).call(this);\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.coffeescript.tokens.json\\" + }, + \\"attrs\\": [ + { + \\"name\\": \\"type\\", + \\"val\\": \\"'text/javascript'\\", + \\"mustEscape\\": true + } + ], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.coffeescript.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.coffeescript.tokens.json\\" +}" +`; + +exports[`cases/filters.custom.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"html\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"body\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"custom\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"Line 1\\", + \\"line\\": 4 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"Line 2\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 6 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\", + \\"line\\": 6 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 7 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"Line 4\\", + \\"line\\": 7 + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters.custom.tokens.json\\" + }, + \\"attrs\\": [ + { + \\"name\\": \\"opt\\", + \\"val\\": \\"'val'\\", + \\"mustEscape\\": true + }, + { + \\"name\\": \\"num\\", + \\"val\\": \\"2\\", + \\"mustEscape\\": true + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters.custom.tokens.json\\", + \\"val\\": \\"BEGINLine 1\\\\nLine 2\\\\n\\\\nLine 4END\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.custom.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.custom.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.custom.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.custom.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.custom.tokens.json\\" +}" +`; + +exports[`cases/filters.include.custom.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"html\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"body\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"pre\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"line\\": 4, + \\"filename\\": \\"filters.include.custom.tokens.json\\", + \\"val\\": \\"BEGINhtml\\\\n body\\\\n pre\\\\n include:custom(opt='val' num=2) filters.include.custom.pug\\\\nEND\\" + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters.include.custom.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 3, + \\"filename\\": \\"filters.include.custom.tokens.json\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.include.custom.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.include.custom.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.include.custom.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.include.custom.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.include.custom.tokens.json\\" +}" +`; + +exports[`cases/filters.include.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"html\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"body\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"line\\": 3, + \\"filename\\": \\"filters.include.tokens.json\\", + \\"val\\": \\"

Just some markdown tests.

\\\\n

With new line.

\\\\n\\" + }, + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"script\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"line\\": 5, + \\"filename\\": \\"filters.include.tokens.json\\", + \\"val\\": \\"(function(){}).call(this);\\" + } + ], + \\"line\\": 4, + \\"filename\\": \\"filters.include.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 4, + \\"filename\\": \\"filters.include.tokens.json\\" + }, + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"script\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"line\\": 7, + \\"filename\\": \\"filters.include.tokens.json\\", + \\"val\\": \\"\\" + } + ], + \\"line\\": 6, + \\"filename\\": \\"filters.include.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 6, + \\"filename\\": \\"filters.include.tokens.json\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.include.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.include.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.include.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.include.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.include.tokens.json\\" +}" +`; + +exports[`cases/filters.inline.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"p\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"before \\", + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\" + }, + { + \\"type\\": \\"Text\\", + \\"name\\": \\"cdata\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"inside\\", + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\", + \\"val\\": \\"\\" + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\" after\\", + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.inline.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.inline.tokens.json\\" +}" +`; + +exports[`cases/filters.less.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"html\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"head\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"style\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"less\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"@pad: 15px;\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 6 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"body {\\", + \\"line\\": 6 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 7 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\" padding: @pad;\\", + \\"line\\": 7 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 8 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"}\\", + \\"line\\": 8 + } + ], + \\"line\\": 4, + \\"filename\\": \\"filters.less.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 4, + \\"filename\\": \\"filters.less.tokens.json\\", + \\"val\\": \\"body {\\\\n padding: 15px;\\\\n}\\\\n\\" + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters.less.tokens.json\\" + }, + \\"attrs\\": [ + { + \\"name\\": \\"type\\", + \\"val\\": \\"\\\\\\"text/css\\\\\\"\\", + \\"mustEscape\\": true + } + ], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 3, + \\"filename\\": \\"filters.less.tokens.json\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.less.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.less.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.less.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.less.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.less.tokens.json\\" +}" +`; + +exports[`cases/filters.markdown.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"html\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"body\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"markdown-it\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"This is _some_ awesome **markdown**\\", + \\"line\\": 4 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"whoop.\\", + \\"line\\": 5 + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters.markdown.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 3, + \\"filename\\": \\"filters.markdown.tokens.json\\", + \\"val\\": \\"

This is some awesome markdown\\\\nwhoop.

\\\\n\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.markdown.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.markdown.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.markdown.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.markdown.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.markdown.tokens.json\\" +}" +`; + +exports[`cases/filters.nested.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"script\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"cdata\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"uglify-js\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"(function() {\\", + \\"line\\": 3 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 4 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\" console.log('test')\\", + \\"line\\": 4 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"})()\\", + \\"line\\": 5 + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 2, + \\"filename\\": \\"filters.nested.tokens.json\\", + \\"val\\": \\"!function(){console.log(\\\\\\"test\\\\\\")}();\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 2, + \\"filename\\": \\"filters.nested.tokens.json\\", + \\"val\\": \\"\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"script\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"cdata\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"uglify-js\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"coffee-script\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"(->\\", + \\"line\\": 8 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 9 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\" console.log 'test'\\", + \\"line\\": 9 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 10 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\")()\\", + \\"line\\": 10 + } + ], + \\"line\\": 7, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 7, + \\"filename\\": \\"filters.nested.tokens.json\\", + \\"val\\": \\"(function() {\\\\n (function() {\\\\n return console.log('test');\\\\n })();\\\\n\\\\n}).call(this);\\\\n\\" + } + ], + \\"line\\": 7, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 7, + \\"filename\\": \\"filters.nested.tokens.json\\", + \\"val\\": \\"(function(){!function(){console.log(\\\\\\"test\\\\\\")}()}).call(this);\\" + } + ], + \\"line\\": 7, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 7, + \\"filename\\": \\"filters.nested.tokens.json\\", + \\"val\\": \\"\\" + } + ], + \\"line\\": 6, + \\"filename\\": \\"filters.nested.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 6, + \\"filename\\": \\"filters.nested.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.nested.tokens.json\\" +}" +`; + +exports[`cases/filters.stylus.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"html\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"head\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"style\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"stylus\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"val\\": \\"body\\", + \\"line\\": 5 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\"\\\\n\\", + \\"line\\": 6 + }, + { + \\"type\\": \\"Text\\", + \\"val\\": \\" padding: 50px\\", + \\"line\\": 6 + } + ], + \\"line\\": 4, + \\"filename\\": \\"filters.stylus.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 4, + \\"filename\\": \\"filters.stylus.tokens.json\\", + \\"val\\": \\"body {\\\\n padding: 50px;\\\\n}\\\\n\\" + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters.stylus.tokens.json\\" + }, + \\"attrs\\": [ + { + \\"name\\": \\"type\\", + \\"val\\": \\"\\\\\\"text/css\\\\\\"\\", + \\"mustEscape\\": true + } + ], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 3, + \\"filename\\": \\"filters.stylus.tokens.json\\" + } + ], + \\"line\\": 2, + \\"filename\\": \\"filters.stylus.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 2, + \\"filename\\": \\"filters.stylus.tokens.json\\" + }, + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"body\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [], + \\"line\\": 7, + \\"filename\\": \\"filters.stylus.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 7, + \\"filename\\": \\"filters.stylus.tokens.json\\" + } + ], + \\"line\\": 1, + \\"filename\\": \\"filters.stylus.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters.stylus.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters.stylus.tokens.json\\" +}" +`; + +exports[`cases/filters-empty.input.json 1`] = ` +"{ + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Code\\", + \\"val\\": \\"var users = [{ name: 'tobi', age: 2 }]\\", + \\"buffer\\": false, + \\"mustEscape\\": false, + \\"isInline\\": false, + \\"line\\": 1, + \\"filename\\": \\"filters-empty.tokens.json\\" + }, + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"fb:users\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Each\\", + \\"obj\\": \\"users\\", + \\"val\\": \\"user\\", + \\"key\\": null, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Tag\\", + \\"name\\": \\"fb:user\\", + \\"selfClosing\\": false, + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [ + { + \\"type\\": \\"Text\\", + \\"name\\": \\"cdata\\", + \\"block\\": { + \\"type\\": \\"Block\\", + \\"nodes\\": [], + \\"line\\": 6, + \\"filename\\": \\"filters-empty.tokens.json\\" + }, + \\"attrs\\": [], + \\"line\\": 6, + \\"filename\\": \\"filters-empty.tokens.json\\", + \\"val\\": \\"\\" + } + ], + \\"line\\": 5, + \\"filename\\": \\"filters-empty.tokens.json\\" + }, + \\"attrs\\": [ + { + \\"name\\": \\"age\\", + \\"val\\": \\"user.age\\", + \\"mustEscape\\": true + } + ], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 5, + \\"filename\\": \\"filters-empty.tokens.json\\" + } + ], + \\"line\\": 5, + \\"filename\\": \\"filters-empty.tokens.json\\" + }, + \\"line\\": 4, + \\"filename\\": \\"filters-empty.tokens.json\\" + } + ], + \\"line\\": 3, + \\"filename\\": \\"filters-empty.tokens.json\\" + }, + \\"attrs\\": [], + \\"attributeBlocks\\": [], + \\"isInline\\": false, + \\"line\\": 3, + \\"filename\\": \\"filters-empty.tokens.json\\" + } + ], + \\"line\\": 0, + \\"filename\\": \\"filters-empty.tokens.json\\" +}" +`; + +exports[`errors/dynamic-option.input.json 1`] = ` +Object { + "code": "PUG:FILTER_OPTION_NOT_CONSTANT", + "line": 2, + "msg": "\\"opt\\" is not constant. All filters are rendered compile-time so filter options must be constants.", +} +`; diff --git a/src/test-data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap b/src/test-data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap new file mode 100644 index 0000000..b69c7cb --- /dev/null +++ b/src/test-data/pug-filters/test/__snapshots__/per-filter-options-applied-to-nested-filters.test.js.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`per filter options are applied, even to nested filters 1`] = ` +Object { + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 2, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 3, + "nodes": Array [ + Object { + "attrs": Array [], + "block": Object { + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 3, + "nodes": Array [ + Object { + "column": 5, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 4, + "type": "Text", + "val": "function myFunc(foo) {", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 5, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 5, + "type": "Text", + "val": " return foo;", + }, + Object { + "column": 1, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 6, + "type": "Text", + "val": " +", + }, + Object { + "column": 5, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 6, + "type": "Text", + "val": "}", + }, + ], + "type": "Block", + }, + "column": 9, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 3, + "name": "uglify-js", + "type": "Text", + "val": "function myFunc(n) { + return n; +} +", + }, + ], + "type": "Block", + }, + "column": 3, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "line": 3, + "name": "cdata", + "type": "Text", + "val": "", + }, + ], + "type": "Block", + }, + "column": 1, + "filename": "/packages/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js", + "isInline": false, + "line": 2, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; diff --git a/src/test-data/pug-filters/test/cases/filters-empty.input.json b/src/test-data/pug-filters/test/cases/filters-empty.input.json new file mode 100644 index 0000000..e360d5d --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters-empty.input.json @@ -0,0 +1,84 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Code", + "val": "var users = [{ name: 'tobi', age: 2 }]", + "buffer": false, + "mustEscape": false, + "isInline": false, + "line": 1, + "filename": "filters-empty.tokens.json" + }, + { + "type": "Tag", + "name": "fb:users", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Each", + "obj": "users", + "val": "user", + "key": null, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "fb:user", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "cdata", + "block": { + "type": "Block", + "nodes": [], + "line": 6, + "filename": "filters-empty.tokens.json" + }, + "attrs": [], + "line": 6, + "filename": "filters-empty.tokens.json" + } + ], + "line": 5, + "filename": "filters-empty.tokens.json" + }, + "attrs": [ + { + "name": "age", + "val": "user.age", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "filters-empty.tokens.json" + } + ], + "line": 5, + "filename": "filters-empty.tokens.json" + }, + "line": 4, + "filename": "filters-empty.tokens.json" + } + ], + "line": 3, + "filename": "filters-empty.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "filters-empty.tokens.json" + } + ], + "line": 0, + "filename": "filters-empty.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.cdata.input.json b/src/test-data/pug-filters/test/cases/filters.cdata.input.json new file mode 100644 index 0000000..0b6034f --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.cdata.input.json @@ -0,0 +1,83 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Code", + "val": "users = [{ name: 'tobi', age: 2 }]", + "buffer": false, + "mustEscape": false, + "isInline": false, + "line": 2, + "filename": "filters.cdata.tokens.json" + }, + { + "type": "Tag", + "name": "fb:users", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Each", + "obj": "users", + "val": "user", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "fb:user", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "cdata", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "#{user.name}", + "line": 8 + } + ] + }, + "attrs": [], + "line": 7, + "filename": "filters.cdata.tokens.json" + } + ] + }, + "attrs": [ + { + "name": "age", + "val": "user.age", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "filters.cdata.tokens.json" + } + ], + "line": 6, + "filename": "filters.cdata.tokens.json" + }, + "line": 5, + "filename": "filters.cdata.tokens.json" + } + ] + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "filters.cdata.tokens.json" + } + ], + "line": 0, + "filename": "filters.cdata.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.coffeescript.input.json b/src/test-data/pug-filters/test/cases/filters.coffeescript.input.json new file mode 100644 index 0000000..106339c --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.coffeescript.input.json @@ -0,0 +1,84 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "coffee-script", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "regexp = /\\n/", + "line": 3 + } + ], + "line": 2, + "filename": "filters.coffeescript.tokens.json" + }, + "attrs": [], + "line": 2, + "filename": "filters.coffeescript.tokens.json" + }, + { + "type": "Filter", + "name": "coffee-script", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "math =", + "line": 5 + }, + { + "type": "Text", + "val": "\n", + "line": 6 + }, + { + "type": "Text", + "val": " square: (value) -> value * value", + "line": 6 + } + ], + "line": 4, + "filename": "filters.coffeescript.tokens.json" + }, + "attrs": [ + { + "name": "minify", + "val": "true", + "mustEscape": true + } + ], + "line": 4, + "filename": "filters.coffeescript.tokens.json" + } + ], + "line": 1, + "filename": "filters.coffeescript.tokens.json" + }, + "attrs": [ + { + "name": "type", + "val": "'text/javascript'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.coffeescript.tokens.json" + } + ], + "line": 0, + "filename": "filters.coffeescript.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.custom.input.json b/src/test-data/pug-filters/test/cases/filters.custom.input.json new file mode 100644 index 0000000..ae046d6 --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.custom.input.json @@ -0,0 +1,101 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "custom", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Line 1", + "line": 4 + }, + { + "type": "Text", + "val": "\n", + "line": 5 + }, + { + "type": "Text", + "val": "Line 2", + "line": 5 + }, + { + "type": "Text", + "val": "\n", + "line": 6 + }, + { + "type": "Text", + "val": "", + "line": 6 + }, + { + "type": "Text", + "val": "\n", + "line": 7 + }, + { + "type": "Text", + "val": "Line 4", + "line": 7 + } + ], + "line": 3, + "filename": "filters.custom.tokens.json" + }, + "attrs": [ + { + "name": "opt", + "val": "'val'", + "mustEscape": true + }, + { + "name": "num", + "val": "2", + "mustEscape": true + } + ], + "line": 3, + "filename": "filters.custom.tokens.json" + } + ], + "line": 2, + "filename": "filters.custom.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "filters.custom.tokens.json" + } + ], + "line": 1, + "filename": "filters.custom.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.custom.tokens.json" + } + ], + "line": 0, + "filename": "filters.custom.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.include.custom.input.json b/src/test-data/pug-filters/test/cases/filters.include.custom.input.json new file mode 100644 index 0000000..cc99b5a --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.include.custom.input.json @@ -0,0 +1,91 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "pre", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 4, + "filename": "filters.include.custom.tokens.json", + "path": "filters.include.custom.pug", + "fullPath": "test/cases/filters.include.custom.pug", + "str": "html\n body\n pre\n include:custom(opt='val' num=2) filters.include.custom.pug\n" + }, + "line": 4, + "filename": "filters.include.custom.tokens.json", + "filters": [ + { + "type": "IncludeFilter", + "name": "custom", + "attrs": [ + { + "name": "opt", + "val": "'val'", + "mustEscape": true + }, + { + "name": "num", + "val": "2", + "mustEscape": true + } + ], + "line": 4, + "filename": "filters.include.custom.tokens.json" + } + ] + } + ], + "line": 3, + "filename": "filters.include.custom.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "filters.include.custom.tokens.json" + } + ], + "line": 2, + "filename": "filters.include.custom.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "filters.include.custom.tokens.json" + } + ], + "line": 1, + "filename": "filters.include.custom.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.include.custom.tokens.json" + } + ], + "line": 0, + "filename": "filters.include.custom.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.include.custom.pug b/src/test-data/pug-filters/test/cases/filters.include.custom.pug new file mode 100644 index 0000000..5811147 --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.include.custom.pug @@ -0,0 +1,4 @@ +html + body + pre + include:custom(opt='val' num=2) filters.include.custom.pug diff --git a/src/test-data/pug-filters/test/cases/filters.include.input.json b/src/test-data/pug-filters/test/cases/filters.include.input.json new file mode 100644 index 0000000..c6f57b1 --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.include.input.json @@ -0,0 +1,160 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 3, + "filename": "filters.include.tokens.json", + "path": "some.md", + "fullPath": "test/cases/some.md", + "str": "Just _some_ markdown **tests**.\n\nWith new line.\n" + }, + "line": 3, + "filename": "filters.include.tokens.json", + "filters": [ + { + "type": "IncludeFilter", + "name": "markdown-it", + "attrs": [], + "line": 3, + "filename": "filters.include.tokens.json" + } + ] + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 5, + "filename": "filters.include.tokens.json", + "path": "include-filter-coffee.coffee", + "fullPath": "test/cases/include-filter-coffee.coffee", + "str": "math =\n square: (value) -> value * value\n" + }, + "line": 5, + "filename": "filters.include.tokens.json", + "filters": [ + { + "type": "IncludeFilter", + "name": "coffee-script", + "attrs": [ + { + "name": "minify", + "val": "true", + "mustEscape": true + } + ], + "line": 5, + "filename": "filters.include.tokens.json" + } + ] + } + ], + "line": 4, + "filename": "filters.include.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "filters.include.tokens.json" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 7, + "filename": "filters.include.tokens.json", + "path": "include-filter-coffee.coffee", + "fullPath": "test/cases/include-filter-coffee.coffee", + "str": "math =\n square: (value) -> value * value\n" + }, + "line": 7, + "filename": "filters.include.tokens.json", + "filters": [ + { + "type": "IncludeFilter", + "name": "cdata", + "attrs": [], + "line": 7, + "filename": "filters.include.tokens.json" + }, + { + "type": "IncludeFilter", + "name": "coffee-script", + "attrs": [ + { + "name": "minify", + "val": "false", + "mustEscape": true + } + ], + "line": 7, + "filename": "filters.include.tokens.json" + } + ] + } + ], + "line": 6, + "filename": "filters.include.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "filters.include.tokens.json" + } + ], + "line": 2, + "filename": "filters.include.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "filters.include.tokens.json" + } + ], + "line": 1, + "filename": "filters.include.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.include.tokens.json" + } + ], + "line": 0, + "filename": "filters.include.tokens.json" +} diff --git a/src/test-data/pug-filters/test/cases/filters.inline.input.json b/src/test-data/pug-filters/test/cases/filters.inline.input.json new file mode 100644 index 0000000..5899faa --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.inline.input.json @@ -0,0 +1,56 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "before ", + "line": 1, + "filename": "filters.inline.tokens.json" + }, + { + "type": "Filter", + "name": "cdata", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "inside", + "line": 1, + "filename": "filters.inline.tokens.json" + } + ], + "line": 1, + "filename": "filters.inline.tokens.json" + }, + "attrs": [], + "line": 1, + "filename": "filters.inline.tokens.json" + }, + { + "type": "Text", + "val": " after", + "line": 1, + "filename": "filters.inline.tokens.json" + } + ], + "line": 1, + "filename": "filters.inline.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.inline.tokens.json" + } + ], + "line": 0, + "filename": "filters.inline.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.less.input.json b/src/test-data/pug-filters/test/cases/filters.less.input.json new file mode 100644 index 0000000..f13a33a --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.less.input.json @@ -0,0 +1,113 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "style", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "less", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "@pad: 15px;", + "line": 5 + }, + { + "type": "Text", + "val": "\n", + "line": 6 + }, + { + "type": "Text", + "val": "body {", + "line": 6 + }, + { + "type": "Text", + "val": "\n", + "line": 7 + }, + { + "type": "Text", + "val": " padding: @pad;", + "line": 7 + }, + { + "type": "Text", + "val": "\n", + "line": 8 + }, + { + "type": "Text", + "val": "}", + "line": 8 + } + ], + "line": 4, + "filename": "filters.less.tokens.json" + }, + "attrs": [], + "line": 4, + "filename": "filters.less.tokens.json" + } + ], + "line": 3, + "filename": "filters.less.tokens.json" + }, + "attrs": [ + { + "name": "type", + "val": "\"text/css\"", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "filters.less.tokens.json" + } + ], + "line": 2, + "filename": "filters.less.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "filters.less.tokens.json" + } + ], + "line": 1, + "filename": "filters.less.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.less.tokens.json" + } + ], + "line": 0, + "filename": "filters.less.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.markdown.input.json b/src/test-data/pug-filters/test/cases/filters.markdown.input.json new file mode 100644 index 0000000..4dbf95e --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.markdown.input.json @@ -0,0 +1,70 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "markdown-it", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "This is _some_ awesome **markdown**", + "line": 4 + }, + { + "type": "Text", + "val": "\n", + "line": 5 + }, + { + "type": "Text", + "val": "whoop.", + "line": 5 + } + ], + "line": 3, + "filename": "filters.markdown.tokens.json" + }, + "attrs": [], + "line": 3, + "filename": "filters.markdown.tokens.json" + } + ], + "line": 2, + "filename": "filters.markdown.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "filters.markdown.tokens.json" + } + ], + "line": 1, + "filename": "filters.markdown.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.markdown.tokens.json" + } + ], + "line": 0, + "filename": "filters.markdown.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.nested.input.json b/src/test-data/pug-filters/test/cases/filters.nested.input.json new file mode 100644 index 0000000..8b0354a --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.nested.input.json @@ -0,0 +1,161 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "cdata", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "uglify-js", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "(function() {", + "line": 3 + }, + { + "type": "Text", + "val": "\n", + "line": 4 + }, + { + "type": "Text", + "val": " console.log('test')", + "line": 4 + }, + { + "type": "Text", + "val": "\n", + "line": 5 + }, + { + "type": "Text", + "val": "})()", + "line": 5 + } + ], + "line": 2, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "line": 2, + "filename": "filters.nested.tokens.json" + } + ], + "line": 2, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "line": 2, + "filename": "filters.nested.tokens.json" + } + ], + "line": 1, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.nested.tokens.json" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "cdata", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "uglify-js", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "coffee-script", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "(->", + "line": 8 + }, + { + "type": "Text", + "val": "\n", + "line": 9 + }, + { + "type": "Text", + "val": " console.log 'test'", + "line": 9 + }, + { + "type": "Text", + "val": "\n", + "line": 10 + }, + { + "type": "Text", + "val": ")()", + "line": 10 + } + ], + "line": 7, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "line": 7, + "filename": "filters.nested.tokens.json" + } + ], + "line": 7, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "line": 7, + "filename": "filters.nested.tokens.json" + } + ], + "line": 7, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "line": 7, + "filename": "filters.nested.tokens.json" + } + ], + "line": 6, + "filename": "filters.nested.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "filters.nested.tokens.json" + } + ], + "line": 0, + "filename": "filters.nested.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/filters.stylus.input.json b/src/test-data/pug-filters/test/cases/filters.stylus.input.json new file mode 100644 index 0000000..8fec328 --- /dev/null +++ b/src/test-data/pug-filters/test/cases/filters.stylus.input.json @@ -0,0 +1,109 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "style", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Filter", + "name": "stylus", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "body", + "line": 5 + }, + { + "type": "Text", + "val": "\n", + "line": 6 + }, + { + "type": "Text", + "val": " padding: 50px", + "line": 6 + } + ], + "line": 4, + "filename": "filters.stylus.tokens.json" + }, + "attrs": [], + "line": 4, + "filename": "filters.stylus.tokens.json" + } + ], + "line": 3, + "filename": "filters.stylus.tokens.json" + }, + "attrs": [ + { + "name": "type", + "val": "\"text/css\"", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "filters.stylus.tokens.json" + } + ], + "line": 2, + "filename": "filters.stylus.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "filters.stylus.tokens.json" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 7, + "filename": "filters.stylus.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "filters.stylus.tokens.json" + } + ], + "line": 1, + "filename": "filters.stylus.tokens.json" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "filters.stylus.tokens.json" + } + ], + "line": 0, + "filename": "filters.stylus.tokens.json" +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/cases/include-filter-coffee.coffee b/src/test-data/pug-filters/test/cases/include-filter-coffee.coffee new file mode 100644 index 0000000..9723cd7 --- /dev/null +++ b/src/test-data/pug-filters/test/cases/include-filter-coffee.coffee @@ -0,0 +1,2 @@ +math = + square: (value) -> value * value diff --git a/src/test-data/pug-filters/test/cases/some.md b/src/test-data/pug-filters/test/cases/some.md new file mode 100644 index 0000000..8ea3e54 --- /dev/null +++ b/src/test-data/pug-filters/test/cases/some.md @@ -0,0 +1,3 @@ +Just _some_ markdown **tests**. + +With new line. diff --git a/src/test-data/pug-filters/test/custom-filters.js b/src/test-data/pug-filters/test/custom-filters.js new file mode 100644 index 0000000..74755e5 --- /dev/null +++ b/src/test-data/pug-filters/test/custom-filters.js @@ -0,0 +1,9 @@ +var assert = require('assert'); + +module.exports = { + custom: function(str, options) { + expect(options.opt).toBe('val'); + expect(options.num).toBe(2); + return 'BEGIN' + str + 'END'; + }, +}; diff --git a/src/test-data/pug-filters/test/errors-src/dynamic-option.jade b/src/test-data/pug-filters/test/errors-src/dynamic-option.jade new file mode 100644 index 0000000..f79dd94 --- /dev/null +++ b/src/test-data/pug-filters/test/errors-src/dynamic-option.jade @@ -0,0 +1,3 @@ +- var opt = 'a' +:cdata(option=opt) + hey diff --git a/src/test-data/pug-filters/test/errors/dynamic-option.input.json b/src/test-data/pug-filters/test/errors/dynamic-option.input.json new file mode 100644 index 0000000..3728761 --- /dev/null +++ b/src/test-data/pug-filters/test/errors/dynamic-option.input.json @@ -0,0 +1,37 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Code", + "val": "var opt = 'a'", + "buffer": false, + "escape": false, + "isInline": false, + "line": 1 + }, + { + "type": "Filter", + "name": "cdata", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "hey", + "line": 3 + } + ], + "line": 2 + }, + "attrs": [ + { + "name": "option", + "val": "opt", + "escaped": true + } + ], + "line": 2 + } + ], + "line": 0 +} \ No newline at end of file diff --git a/src/test-data/pug-filters/test/filter-aliases.test.js b/src/test-data/pug-filters/test/filter-aliases.test.js new file mode 100644 index 0000000..3a4bf3b --- /dev/null +++ b/src/test-data/pug-filters/test/filter-aliases.test.js @@ -0,0 +1,88 @@ +const lex = require('pug-lexer'); +const parse = require('pug-parser'); +const handleFilters = require('../').handleFilters; + +const customFilters = {}; +test('filters can be aliased', () => { + const source = ` +script + :cdata:minify + function myFunc(foo) { + return foo; + } + `; + + const ast = parse(lex(source, {filename: __filename}), { + filename: __filename, + src: source, + }); + + const options = {}; + const aliases = { + minify: 'uglify-js', + }; + + const output = handleFilters(ast, customFilters, options, aliases); + expect(output).toMatchSnapshot(); +}); + +test('we do not support chains of aliases', () => { + const source = ` +script + :cdata:minify-js + function myFunc(foo) { + return foo; + } + `; + + const ast = parse(lex(source, {filename: __filename}), { + filename: __filename, + src: source, + }); + + const options = {}; + const aliases = { + 'minify-js': 'minify', + minify: 'uglify-js', + }; + + try { + const output = handleFilters(ast, customFilters, options, aliases); + } catch (ex) { + expect({ + code: ex.code, + message: ex.message, + }).toMatchSnapshot(); + return; + } + throw new Error('Expected an exception'); +}); + +test('options are applied before aliases', () => { + const source = ` +script + :cdata:minify + function myFunc(foo) { + return foo; + } + :cdata:uglify-js + function myFunc(foo) { + return foo; + } + `; + + const ast = parse(lex(source, {filename: __filename}), { + filename: __filename, + src: source, + }); + + const options = { + minify: {output: {beautify: true}}, + }; + const aliases = { + minify: 'uglify-js', + }; + + const output = handleFilters(ast, customFilters, options, aliases); + expect(output).toMatchSnapshot(); +}); diff --git a/src/test-data/pug-filters/test/index.test.js b/src/test-data/pug-filters/test/index.test.js new file mode 100644 index 0000000..d1369b5 --- /dev/null +++ b/src/test-data/pug-filters/test/index.test.js @@ -0,0 +1,55 @@ +'use strict'; + +var fs = require('fs'); +var assert = require('assert'); +var handleFilters = require('../').handleFilters; +var customFilters = require('./custom-filters.js'); + +process.chdir(__dirname + '/../'); + +var testCases; + +testCases = fs.readdirSync(__dirname + '/cases').filter(function(name) { + return /\.input\.json$/.test(name); +}); +// +testCases.forEach(function(filename) { + function read(path) { + return fs.readFileSync(__dirname + '/cases/' + path, 'utf8'); + } + + test('cases/' + filename, function() { + var actualAst = JSON.stringify( + handleFilters(JSON.parse(read(filename)), customFilters), + null, + ' ' + ); + expect(actualAst).toMatchSnapshot(); + }); +}); + +testCases = fs.readdirSync(__dirname + '/errors').filter(function(name) { + return /\.input\.json$/.test(name); +}); + +testCases.forEach(function(filename) { + function read(path) { + return fs.readFileSync(__dirname + '/errors/' + path, 'utf8'); + } + + test('errors/' + filename, function() { + var actual; + try { + handleFilters(JSON.parse(read(filename)), customFilters); + throw new Error('Expected ' + filename + ' to throw an exception.'); + } catch (ex) { + if (!ex || !ex.code || ex.code.indexOf('PUG:') !== 0) throw ex; + actual = { + msg: ex.msg, + code: ex.code, + line: ex.line, + }; + } + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/src/test-data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js b/src/test-data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js new file mode 100644 index 0000000..30593b5 --- /dev/null +++ b/src/test-data/pug-filters/test/per-filter-options-applied-to-nested-filters.test.js @@ -0,0 +1,28 @@ +const lex = require('pug-lexer'); +const parse = require('pug-parser'); +const handleFilters = require('../').handleFilters; + +const customFilters = {}; +test('per filter options are applied, even to nested filters', () => { + const source = ` +script + :cdata:uglify-js + function myFunc(foo) { + return foo; + } + `; + + const ast = parse(lex(source, {filename: __filename}), { + filename: __filename, + src: source, + }); + + const options = { + 'uglify-js': {output: {beautify: true}}, + }; + + const output = handleFilters(ast, customFilters, options); + expect(output).toMatchSnapshot(); + + // TODO: render with `options.filterOptions['uglify-js']` +}); diff --git a/src/test-data/pug-lexer/cases/attr-es2015.pug b/src/test-data/pug-lexer/cases/attr-es2015.pug new file mode 100644 index 0000000..d19080f --- /dev/null +++ b/src/test-data/pug-lexer/cases/attr-es2015.pug @@ -0,0 +1,3 @@ +- var avatar = '219b77f9d21de75e81851b6b886057c7' + +div.avatar-div(style=`background-image: url(https://www.gravatar.com/avatar/${avatar})`) diff --git a/src/test-data/pug-lexer/cases/attrs-data.pug b/src/test-data/pug-lexer/cases/attrs-data.pug new file mode 100644 index 0000000..9e5b4b6 --- /dev/null +++ b/src/test-data/pug-lexer/cases/attrs-data.pug @@ -0,0 +1,7 @@ +- var user = { name: 'tobi' } +foo(data-user=user) +foo(data-items=[1,2,3]) +foo(data-username='tobi') +foo(data-escaped={message: "Let's rock!"}) +foo(data-ampersand={message: "a quote: " this & that"}) +foo(data-epoc=new Date(0)) diff --git a/src/test-data/pug-lexer/cases/attrs.js.pug b/src/test-data/pug-lexer/cases/attrs.js.pug new file mode 100644 index 0000000..d989be8 --- /dev/null +++ b/src/test-data/pug-lexer/cases/attrs.js.pug @@ -0,0 +1,22 @@ +- var id = 5 +- function answer() { return 42; } +a(href='/user/' + id, class='button') +a(href = '/user/' + id, class = 'button') +meta(key='answer', value=answer()) +a(class = ['class1', 'class2']) +a.tag-class(class = ['class1', 'class2']) + +a(href='/user/' + id class='button') +a(href = '/user/' + id class = 'button') +meta(key='answer' value=answer()) +a(class = ['class1', 'class2']) +a.tag-class(class = ['class1', 'class2']) + +div(id=id)&attributes({foo: 'bar'}) +- var bar = null +div(foo=null bar=bar)&attributes({baz: 'baz'}) + +div(...object) +div(...object after="after") +div(before="before" ...object) +div(before="before" ...object after="after") diff --git a/src/test-data/pug-lexer/cases/attrs.pug b/src/test-data/pug-lexer/cases/attrs.pug new file mode 100644 index 0000000..d4420e3 --- /dev/null +++ b/src/test-data/pug-lexer/cases/attrs.pug @@ -0,0 +1,43 @@ +a(href='/contact') contact +a(href='/save').button save +a(foo, bar, baz) +a(foo='foo, bar, baz', bar=1) +a(foo='((foo))', bar= (1) ? 1 : 0 ) +select + option(value='foo', selected) Foo + option(selected, value='bar') Bar +a(foo="class:") +input(pattern='\\S+') + +a(href='/contact') contact +a(href='/save').button save +a(foo bar baz) +a(foo='foo, bar, baz' bar=1) +a(foo='((foo))' bar= (1) ? 1 : 0 ) +select + option(value='foo' selected) Foo + option(selected value='bar') Bar +a(foo="class:") +input(pattern='\\S+') +foo(terse="true") +foo(date=new Date(0)) + +foo(abc + ,def) +foo(abc, + def) +foo(abc, + def) +foo(abc + ,def) +foo(abc + def) +foo(abc + def) + +- var attrs = {foo: 'bar', bar: ''} + +div&attributes(attrs) + +a(foo='foo' "bar"="bar") +a(foo='foo' 'bar'='bar') diff --git a/src/test-data/pug-lexer/cases/attrs.unescaped.pug b/src/test-data/pug-lexer/cases/attrs.unescaped.pug new file mode 100644 index 0000000..36a4e10 --- /dev/null +++ b/src/test-data/pug-lexer/cases/attrs.unescaped.pug @@ -0,0 +1,3 @@ +script(type='text/x-template') + div(id!='user-<%= user.id %>') + h1 <%= user.title %> \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/basic.pug b/src/test-data/pug-lexer/cases/basic.pug new file mode 100644 index 0000000..77066d1 --- /dev/null +++ b/src/test-data/pug-lexer/cases/basic.pug @@ -0,0 +1,3 @@ +html + body + h1 Title \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/blanks.pug b/src/test-data/pug-lexer/cases/blanks.pug new file mode 100644 index 0000000..67b0697 --- /dev/null +++ b/src/test-data/pug-lexer/cases/blanks.pug @@ -0,0 +1,8 @@ + + +ul + li foo + + li bar + + li baz diff --git a/src/test-data/pug-lexer/cases/block-code.pug b/src/test-data/pug-lexer/cases/block-code.pug new file mode 100644 index 0000000..9ab6854 --- /dev/null +++ b/src/test-data/pug-lexer/cases/block-code.pug @@ -0,0 +1,12 @@ +- + list = ["uno", "dos", "tres", + "cuatro", "cinco", "seis"]; +//- Without a block, the element is accepted and no code is generated +- +each item in list + - + string = item.charAt(0) + + .toUpperCase() + + item.slice(1); + li= string diff --git a/src/test-data/pug-lexer/cases/block-expansion.pug b/src/test-data/pug-lexer/cases/block-expansion.pug new file mode 100644 index 0000000..fb40f9a --- /dev/null +++ b/src/test-data/pug-lexer/cases/block-expansion.pug @@ -0,0 +1,5 @@ +ul + li: a(href='#') foo + li: a(href='#') bar + +p baz \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/block-expansion.shorthands.pug b/src/test-data/pug-lexer/cases/block-expansion.shorthands.pug new file mode 100644 index 0000000..c52a126 --- /dev/null +++ b/src/test-data/pug-lexer/cases/block-expansion.shorthands.pug @@ -0,0 +1,2 @@ +ul + li.list-item: .foo: #bar baz \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/blockquote.pug b/src/test-data/pug-lexer/cases/blockquote.pug new file mode 100644 index 0000000..a23b70f --- /dev/null +++ b/src/test-data/pug-lexer/cases/blockquote.pug @@ -0,0 +1,4 @@ +figure + blockquote + | Try to define yourself by what you do, and you’ll burnout every time. You are. That is enough. I rest in that. + figcaption from @thefray at 1:43pm on May 10 \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/blocks-in-blocks.pug b/src/test-data/pug-lexer/cases/blocks-in-blocks.pug new file mode 100644 index 0000000..13077d9 --- /dev/null +++ b/src/test-data/pug-lexer/cases/blocks-in-blocks.pug @@ -0,0 +1,4 @@ +extends ./auxiliary/blocks-in-blocks-layout.pug + +block body + h1 Page 2 diff --git a/src/test-data/pug-lexer/cases/blocks-in-if.pug b/src/test-data/pug-lexer/cases/blocks-in-if.pug new file mode 100644 index 0000000..e0c6361 --- /dev/null +++ b/src/test-data/pug-lexer/cases/blocks-in-if.pug @@ -0,0 +1,19 @@ +//- see https://github.com/pugjs/pug/issues/1589 + +-var ajax = true + +-if( ajax ) + //- return only contents if ajax requests + block contents + p ajax contents + +-else + //- return all html + doctype html + html + head + meta( charset='utf8' ) + title sample + body + block contents + p all contetns diff --git a/src/test-data/pug-lexer/cases/case-blocks.pug b/src/test-data/pug-lexer/cases/case-blocks.pug new file mode 100644 index 0000000..345cd41 --- /dev/null +++ b/src/test-data/pug-lexer/cases/case-blocks.pug @@ -0,0 +1,10 @@ +html + body + - var friends = 1 + case friends + when 0 + p you have no friends + when 1 + p you have a friend + default + p you have #{friends} friends \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/case.pug b/src/test-data/pug-lexer/cases/case.pug new file mode 100644 index 0000000..0fbe2ef --- /dev/null +++ b/src/test-data/pug-lexer/cases/case.pug @@ -0,0 +1,19 @@ +html + body + - var friends = 1 + case friends + when 0: p you have no friends + when 1: p you have a friend + default: p you have #{friends} friends + - var friends = 0 + case friends + when 0 + when 1 + p you have very few friends + default + p you have #{friends} friends + + - var friend = 'Tim:G' + case friend + when 'Tim:G': p Friend is a string + when {tim: 'g'}: p Friend is an object diff --git a/src/test-data/pug-lexer/cases/classes-empty.pug b/src/test-data/pug-lexer/cases/classes-empty.pug new file mode 100644 index 0000000..5e66d84 --- /dev/null +++ b/src/test-data/pug-lexer/cases/classes-empty.pug @@ -0,0 +1,3 @@ +a(class='') +a(class=null) +a(class=undefined) \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/classes.pug b/src/test-data/pug-lexer/cases/classes.pug new file mode 100644 index 0000000..699a075 --- /dev/null +++ b/src/test-data/pug-lexer/cases/classes.pug @@ -0,0 +1,14 @@ +a(class=['foo', 'bar', 'baz']) + + + +a.foo(class='bar').baz + + + +a.foo-bar_baz + +a(class={foo: true, bar: false, baz: true}) + +a.-foo +a.3foo diff --git a/src/test-data/pug-lexer/cases/code.conditionals.pug b/src/test-data/pug-lexer/cases/code.conditionals.pug new file mode 100644 index 0000000..aa4c715 --- /dev/null +++ b/src/test-data/pug-lexer/cases/code.conditionals.pug @@ -0,0 +1,43 @@ + +- if (true) + p foo +- else + p bar + +- if (true) { + p foo +- } else { + p bar +- } + +if true + p foo + p bar + p baz +else + p bar + +unless true + p foo +else + p bar + +if 'nested' + if 'works' + p yay + +//- allow empty blocks +if false +else + .bar +if true + .bar +else +.bing + +if false + .bing +else if false + .bar +else + .foo \ No newline at end of file diff --git a/src/test-data/pug-lexer/cases/code.escape.pug b/src/test-data/pug-lexer/cases/code.escape.pug new file mode 100644 index 0000000..762c089 --- /dev/null +++ b/src/test-data/pug-lexer/cases/code.escape.pug @@ -0,0 +1,2 @@ +p= ' +", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "type", + "val": "\\"text/javascript\\"", + }, + ], + "block": Object { + "filename": "includes.pug", + "line": 9, + "nodes": Array [ + Object { + "type": "Text", + "val": "var STRING_SUBSTITUTIONS = { // table of character substitutions + '\\\\t': '\\\\\\\\t', + '\\\\r': '\\\\\\\\r', + '\\\\n': '\\\\\\\\n', + '\\"' : '\\\\\\\\\\"', + '\\\\\\\\': '\\\\\\\\\\\\\\\\' +};", + }, + ], + "type": "Block", + }, + "filename": "includes.pug", + "isInline": false, + "line": 9, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "includes.pug", + "isInline": false, + "line": 6, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`cases from pug includes-with-ext-js.input.json 1`] = ` +Object { + "declaredBlocks": Object {}, + "filename": "includes-with-ext-js.pug", + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "includes-with-ext-js.pug", + "line": 1, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "includes-with-ext-js.pug", + "line": 2, + "nodes": Array [ + Object { + "type": "Text", + "val": "var x = \\"\\\\n here is some \\\\n new lined text\\"; +", + }, + ], + "type": "Block", + }, + "filename": "includes-with-ext-js.pug", + "isInline": true, + "line": 2, + "name": "code", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "includes-with-ext-js.pug", + "isInline": false, + "line": 1, + "name": "pre", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`cases from pug layout.append.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "body": Array [ + Object { + "filename": "../fixtures/append/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/append/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.append.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.append.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/append/layout.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/append/layout.pug", + "line": 2, + "nodes": Array [ + Object { + "filename": "../fixtures/append/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.append.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.append.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/append/layout.pug", + "line": 6, + "nodes": Array [ + Object { + "filename": "../fixtures/append/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/append/layout.pug", + "isInline": false, + "line": 6, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/append/layout.pug", + "isInline": false, + "line": 2, + "name": "html", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`cases from pug layout.append.without-block.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "body": Array [ + Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append-without-block/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.append.without-block.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.without-block.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.append.without-block.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.without-block.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/append-without-block/layout.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 2, + "nodes": Array [ + Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/append-without-block/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.append.without-block.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.without-block.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.append.without-block.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.append.without-block.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 6, + "nodes": Array [ + Object { + "filename": "../fixtures/append-without-block/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/layout.pug", + "isInline": false, + "line": 6, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/append-without-block/layout.pug", + "isInline": false, + "line": 2, + "name": "html", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`cases from pug layout.multi.append.prepend.block.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "content": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 3, + "mode": "replace", + "name": "content", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'last'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'prepend'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 13, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 13, + "type": "Text", + "val": "Last prepend must appear at top", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 13, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'first'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'prepend'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 7, + "type": "Text", + "val": "Something prepended to content", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 7, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'content'", + }, + ], + "block": Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 5, + "type": "Text", + "val": "Defined content", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "isInline": false, + "line": 4, + "name": "div", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'first'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'append'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 4, + "type": "Text", + "val": "Something appended to content", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 4, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'last'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'append'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 10, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 10, + "type": "Text", + "val": "Last append must be most last", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 10, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "parents": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "line": 1, + "mode": "replace", + "name": "content", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'last'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'prepend'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 13, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 13, + "type": "Text", + "val": "Last prepend must appear at top", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 13, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'first'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'prepend'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 7, + "type": "Text", + "val": "Something prepended to content", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 7, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'content'", + }, + ], + "block": Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 5, + "type": "Text", + "val": "Defined content", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "isInline": false, + "line": 4, + "name": "div", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'first'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'append'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 4, + "type": "Text", + "val": "Something appended to content", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 4, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'last'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'append'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 10, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 10, + "type": "Text", + "val": "Last append must be most last", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 10, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "line": 4, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 19, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 19, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'/app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'jquery.js'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 16, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 16, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "line": 1, + "mode": "replace", + "name": "content", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'last'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'prepend'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 13, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 13, + "type": "Text", + "val": "Last prepend must appear at top", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 13, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'first'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'prepend'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 7, + "type": "Text", + "val": "Something prepended to content", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 7, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'content'", + }, + ], + "block": Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 5, + "type": "Text", + "val": "Defined content", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "isInline": false, + "line": 4, + "name": "div", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'first'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'append'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 4, + "type": "Text", + "val": "Something appended to content", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 4, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'last'", + }, + Object { + "mustEscape": false, + "name": "class", + "val": "'append'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 10, + "nodes": Array [ + Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 10, + "type": "Text", + "val": "Last append must be most last", + }, + ], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 10, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + Object { + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "line": 4, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 19, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 19, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'/app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'jquery.js'", + }, + ], + "block": Object { + "filename": "layout.multi.append.prepend.block.pug", + "line": 16, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.multi.append.prepend.block.pug", + "isInline": false, + "line": 16, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "Block", +} +`; + +exports[`cases from pug layout.prepend.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "body": Array [ + Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/prepend/layout.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 2, + "nodes": Array [ + Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 6, + "nodes": Array [ + Object { + "filename": "../fixtures/prepend/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/prepend/layout.pug", + "isInline": false, + "line": 6, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/prepend/layout.pug", + "isInline": false, + "line": 2, + "name": "html", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`cases from pug layout.prepend.without-block.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "body": Array [ + Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.without-block.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.without-block.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.without-block.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.without-block.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend-without-block/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 2, + "nodes": Array [ + Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 3, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'foo.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.without-block.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.without-block.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'bar.js'", + }, + ], + "block": Object { + "filename": "layout.prepend.without-block.pug", + "line": 6, + "nodes": Array [], + "type": "Block", + }, + "filename": "layout.prepend.without-block.pug", + "isInline": false, + "line": 6, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'app.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend-without-block/app-layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/app-layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/jquery.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "isInline": false, + "line": 4, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "src", + "val": "'vendor/caustic.js'", + }, + ], + "block": Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 5, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 6, + "nodes": Array [ + Object { + "filename": "../fixtures/prepend-without-block/layout.pug", + "line": 7, + "mode": "replace", + "name": "body", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "isInline": false, + "line": 6, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/prepend-without-block/layout.pug", + "isInline": false, + "line": 2, + "name": "html", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`error handling child-with-tags.input.json 1`] = ` +Object { + "code": "PUG:UNEXPECTED_NODES_IN_EXTENDING_ROOT", + "line": 6, + "msg": "Only named blocks and mixins can appear at the top level of an extending template", +} +`; + +exports[`error handling extends-not-first.input.json 1`] = ` +Object { + "code": "PUG:EXTENDS_NOT_FIRST", + "line": 4, + "msg": "Declaration of template inheritance (\\"extends\\") should be the first thing in the file. There can only be one extends statement per file.", +} +`; + +exports[`error handling unexpected-block.input.json 1`] = ` +Object { + "code": "PUG:UNEXPECTED_BLOCK", + "line": 3, + "msg": "Unexpected block foo", +} +`; + +exports[`special cases extending-empty.input.json 1`] = ` +Object { + "declaredBlocks": Object {}, + "filename": "../fixtures/empty.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [], + "type": "Block", +} +`; + +exports[`special cases extending-include.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "body": Array [ + Object { + "filename": "extending-include.pug", + "line": 4, + "mode": "replace", + "name": "body", + "nodes": Array [ + Object { + "args": "'myimg.png'", + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'with-border'", + }, + Object { + "mustEscape": true, + "name": "alt", + "val": "\\"My image\\"", + }, + ], + "block": null, + "call": true, + "filename": "extending-include.pug", + "line": 5, + "name": "image", + "type": "Mixin", + }, + ], + "parents": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 8, + "mode": "replace", + "name": "body", + "nodes": Array [ + Object { + "args": "'myimg.png'", + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'with-border'", + }, + Object { + "mustEscape": true, + "name": "alt", + "val": "\\"My image\\"", + }, + ], + "block": null, + "call": true, + "filename": "extending-include.pug", + "line": 5, + "name": "image", + "type": "Mixin", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 5, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "isHtml": true, + "line": 6, + "type": "Text", + "val": "Hello world!", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/layout.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "args": "src", + "block": Object { + "filename": "../fixtures/mixins.pug", + "line": 2, + "nodes": Array [ + Object { + "attributeBlocks": Array [ + "attributes", + ], + "attrs": Array [ + Object { + "mustEscape": true, + "name": "cl-src", + "val": "src", + }, + ], + "block": Object { + "filename": "../fixtures/mixins.pug", + "line": 2, + "nodes": Array [], + "type": "Block", + }, + "filename": "../fixtures/mixins.pug", + "isInline": true, + "line": 2, + "name": "img", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "call": false, + "filename": "../fixtures/mixins.pug", + "line": 1, + "name": "image", + "type": "Mixin", + }, + Object { + "filename": "../fixtures/layout.pug", + "line": 1, + "type": "Doctype", + "val": "", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/layout.pug", + "line": 3, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/layout.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 5, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "isHtml": true, + "line": 6, + "type": "Text", + "val": "Hello world!", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/layout.pug", + "isInline": false, + "line": 4, + "name": "head", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/layout.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 8, + "mode": "replace", + "name": "body", + "nodes": Array [ + Object { + "args": "'myimg.png'", + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "mustEscape": false, + "name": "class", + "val": "'with-border'", + }, + Object { + "mustEscape": true, + "name": "alt", + "val": "\\"My image\\"", + }, + ], + "block": null, + "call": true, + "filename": "extending-include.pug", + "line": 5, + "name": "image", + "type": "Mixin", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/layout.pug", + "isInline": false, + "line": 7, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/layout.pug", + "isInline": false, + "line": 3, + "name": "html", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; + +exports[`special cases root-mixin.input.json 1`] = ` +Object { + "declaredBlocks": Object { + "body": Array [ + Object { + "filename": "root-mixin.pug", + "line": 6, + "mode": "replace", + "name": "body", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 7, + "type": "Text", + "val": "Before", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 7, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "args": null, + "attributeBlocks": Array [], + "attrs": Array [], + "block": null, + "call": true, + "filename": "root-mixin.pug", + "line": 8, + "name": "myMixin", + "type": "Mixin", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 9, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 9, + "type": "Text", + "val": "After", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 9, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "parents": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 8, + "mode": "replace", + "name": "body", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 7, + "type": "Text", + "val": "Before", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 7, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "args": null, + "attributeBlocks": Array [], + "attrs": Array [], + "block": null, + "call": true, + "filename": "root-mixin.pug", + "line": 8, + "name": "myMixin", + "type": "Mixin", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 9, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 9, + "type": "Text", + "val": "After", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 9, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "NamedBlock", + }, + ], + "head": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 5, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "isHtml": true, + "line": 6, + "type": "Text", + "val": "Hello world!", + }, + ], + "type": "NamedBlock", + }, + ], + }, + "filename": "../fixtures/layout.pug", + "hasExtends": true, + "line": 0, + "nodes": Array [ + Object { + "args": null, + "block": Object { + "filename": "root-mixin.pug", + "line": 4, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 4, + "type": "Text", + "val": "Hello world", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 4, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "call": false, + "filename": "root-mixin.pug", + "line": 3, + "name": "myMixin", + "type": "Mixin", + }, + Object { + "filename": "../fixtures/layout.pug", + "line": 1, + "type": "Doctype", + "val": "", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/layout.pug", + "line": 3, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/layout.pug", + "line": 4, + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 5, + "mode": "replace", + "name": "head", + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "isHtml": true, + "line": 6, + "type": "Text", + "val": "Hello world!", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/layout.pug", + "isInline": false, + "line": 4, + "name": "head", + "selfClosing": false, + "type": "Tag", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "../fixtures/layout.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "../fixtures/layout.pug", + "line": 8, + "mode": "replace", + "name": "body", + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 7, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 7, + "type": "Text", + "val": "Before", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 7, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + Object { + "args": null, + "attributeBlocks": Array [], + "attrs": Array [], + "block": null, + "call": true, + "filename": "root-mixin.pug", + "line": 8, + "name": "myMixin", + "type": "Mixin", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "root-mixin.pug", + "line": 9, + "nodes": Array [ + Object { + "filename": "root-mixin.pug", + "line": 9, + "type": "Text", + "val": "After", + }, + ], + "type": "Block", + }, + "filename": "root-mixin.pug", + "isInline": false, + "line": 9, + "name": "p", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/layout.pug", + "isInline": false, + "line": 7, + "name": "body", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "filename": "../fixtures/layout.pug", + "isInline": false, + "line": 3, + "name": "html", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", +} +`; diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/1794-extends.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/1794-extends.pug new file mode 100644 index 0000000..99649d6 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/1794-extends.pug @@ -0,0 +1 @@ +block content \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/1794-include.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/1794-include.pug new file mode 100644 index 0000000..b9c03b4 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/1794-include.pug @@ -0,0 +1,4 @@ +mixin test() + .test&attributes(attributes) + ++test() \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug new file mode 100644 index 0000000..17ca8a0 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/blocks-in-blocks-layout.pug @@ -0,0 +1,8 @@ +doctype html +html + head + title Default title + body + block body + .container + block content diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/dialog.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/dialog.pug new file mode 100644 index 0000000..607bdec --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/dialog.pug @@ -0,0 +1,6 @@ + +extends window.pug + +block window-content + .dialog + block content diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/empty-block.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/empty-block.pug new file mode 100644 index 0000000..776e5fe --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/empty-block.pug @@ -0,0 +1,2 @@ +block test + diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/escapes.html b/src/test-data/pug-linker/test/cases-src/auxiliary/escapes.html new file mode 100644 index 0000000..69e3701 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/escapes.html @@ -0,0 +1,3 @@ + diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug new file mode 100644 index 0000000..2729803 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-1.pug @@ -0,0 +1,5 @@ +extends empty-block.pug + +block test + div test1 + diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug new file mode 100644 index 0000000..beb2e83 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-empty-block-2.pug @@ -0,0 +1,5 @@ +extends empty-block.pug + +block test + div test2 + diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug new file mode 100644 index 0000000..da52beb --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-from-root.pug @@ -0,0 +1,4 @@ +extends /auxiliary/layout.pug + +block content + include /auxiliary/include-from-root.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/extends-relative.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-relative.pug new file mode 100644 index 0000000..7a2ecc4 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/extends-relative.pug @@ -0,0 +1,4 @@ +extends ../../cases-src/auxiliary/layout + +block content + include ../../cases-src/auxiliary/include-from-root diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug new file mode 100644 index 0000000..a3df945 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/filter-in-include.pug @@ -0,0 +1,8 @@ +html + head + style(type="text/css") + :less + @pad: 15px; + body { + padding: @pad; + } diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/includable.js b/src/test-data/pug-linker/test/cases-src/auxiliary/includable.js new file mode 100644 index 0000000..38c071e --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/includable.js @@ -0,0 +1,8 @@ +var STRING_SUBSTITUTIONS = { + // table of character substitutions + '\t': '\\t', + '\r': '\\r', + '\n': '\\n', + '"': '\\"', + '\\': '\\\\', +}; diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/include-from-root.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/include-from-root.pug new file mode 100644 index 0000000..93c364b --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/include-from-root.pug @@ -0,0 +1 @@ +h1 hello \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug new file mode 100644 index 0000000..890febc --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.mixin.block.pug @@ -0,0 +1,11 @@ +mixin article() + article + block + +html + head + title My Application + block head + body + +article + block content diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug new file mode 100644 index 0000000..61033fa --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grand-grandparent.pug @@ -0,0 +1,2 @@ +h1 grand-grandparent +block grand-grandparent \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug new file mode 100644 index 0000000..f8ad4b8 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-grandparent.pug @@ -0,0 +1,6 @@ +extends inheritance.extend.recursive-grand-grandparent.pug + +block grand-grandparent + h2 grandparent + block grandparent + diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug new file mode 100644 index 0000000..72d7230 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/inheritance.extend.recursive-parent.pug @@ -0,0 +1,5 @@ +extends inheritance.extend.recursive-grandparent.pug + +block grandparent + h3 parent + block parent \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/layout.include.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/layout.include.pug new file mode 100644 index 0000000..96734bf --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/layout.include.pug @@ -0,0 +1,7 @@ +html + head + title My Application + block head + body + block content + include window.pug diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/layout.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/layout.pug new file mode 100644 index 0000000..7d183b3 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/layout.pug @@ -0,0 +1,6 @@ +html + head + title My Application + block head + body + block content \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug new file mode 100644 index 0000000..e51eb01 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/mixin-at-end-of-file.pug @@ -0,0 +1,3 @@ +mixin slide + section.slide + block \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/mixins.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/mixins.pug new file mode 100644 index 0000000..0c14c1d --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/mixins.pug @@ -0,0 +1,3 @@ + +mixin foo() + p bar \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/pet.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/pet.pug new file mode 100644 index 0000000..ebee3a8 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/pet.pug @@ -0,0 +1,3 @@ +.pet + h1 {{name}} + p {{name}} is a {{species}} that is {{age}} old \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/smile.html b/src/test-data/pug-linker/test/cases-src/auxiliary/smile.html new file mode 100644 index 0000000..3eadc80 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/smile.html @@ -0,0 +1 @@ +

:)

diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/window.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/window.pug new file mode 100644 index 0000000..7ab7132 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/window.pug @@ -0,0 +1,4 @@ + +.window + a(href='#').close Close + block window-content \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/auxiliary/yield-nested.pug b/src/test-data/pug-linker/test/cases-src/auxiliary/yield-nested.pug new file mode 100644 index 0000000..0771c0a --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/auxiliary/yield-nested.pug @@ -0,0 +1,10 @@ +html + head + title + body + h1 Page + #content + #content-wrapper + yield + #footer + stuff \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases-src/include-extends-from-root.pug b/src/test-data/pug-linker/test/cases-src/include-extends-from-root.pug new file mode 100644 index 0000000..a79a57d --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-extends-from-root.pug @@ -0,0 +1 @@ +include /auxiliary/extends-from-root.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-extends-of-common-template.pug b/src/test-data/pug-linker/test/cases-src/include-extends-of-common-template.pug new file mode 100644 index 0000000..2511f52 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-extends-of-common-template.pug @@ -0,0 +1,2 @@ +include auxiliary/extends-empty-block-1.pug +include auxiliary/extends-empty-block-2.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-extends-relative.pug b/src/test-data/pug-linker/test/cases-src/include-extends-relative.pug new file mode 100644 index 0000000..f1648ff --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-extends-relative.pug @@ -0,0 +1 @@ +include ../cases-src/auxiliary/extends-relative.pug diff --git a/src/test-data/pug-linker/test/cases-src/include-filter-coffee.coffee b/src/test-data/pug-linker/test/cases-src/include-filter-coffee.coffee new file mode 100644 index 0000000..9723cd7 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-filter-coffee.coffee @@ -0,0 +1,2 @@ +math = + square: (value) -> value * value diff --git a/src/test-data/pug-linker/test/cases-src/include-filter-stylus.pug b/src/test-data/pug-linker/test/cases-src/include-filter-stylus.pug new file mode 100644 index 0000000..eefd3c1 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-filter-stylus.pug @@ -0,0 +1,2 @@ +style(type="text/css") + include:stylus some.styl diff --git a/src/test-data/pug-linker/test/cases-src/include-filter.pug b/src/test-data/pug-linker/test/cases-src/include-filter.pug new file mode 100644 index 0000000..e7ea3db --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-filter.pug @@ -0,0 +1,7 @@ +html + body + include:markdown-it some.md + script + include:coffee-script(minify=true) include-filter-coffee.coffee + script + include:coffee-script(minify=false) include-filter-coffee.coffee diff --git a/src/test-data/pug-linker/test/cases-src/include-only-text-body.pug b/src/test-data/pug-linker/test/cases-src/include-only-text-body.pug new file mode 100644 index 0000000..fdb080c --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-only-text-body.pug @@ -0,0 +1,3 @@ +| The message is " +yield +| " diff --git a/src/test-data/pug-linker/test/cases-src/include-only-text.pug b/src/test-data/pug-linker/test/cases-src/include-only-text.pug new file mode 100644 index 0000000..ede4f0f --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-only-text.pug @@ -0,0 +1,5 @@ +html + body + p + include include-only-text-body.pug + em hello world diff --git a/src/test-data/pug-linker/test/cases-src/include-with-text-head.pug b/src/test-data/pug-linker/test/cases-src/include-with-text-head.pug new file mode 100644 index 0000000..4e670c0 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-with-text-head.pug @@ -0,0 +1,3 @@ +head + script(type='text/javascript'). + alert('hello world'); diff --git a/src/test-data/pug-linker/test/cases-src/include-with-text.pug b/src/test-data/pug-linker/test/cases-src/include-with-text.pug new file mode 100644 index 0000000..bc83ea5 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include-with-text.pug @@ -0,0 +1,4 @@ +html + include include-with-text-head.pug + script(src='/caustic.js') + script(src='/app.js') diff --git a/src/test-data/pug-linker/test/cases-src/include.script.pug b/src/test-data/pug-linker/test/cases-src/include.script.pug new file mode 100644 index 0000000..f449144 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include.script.pug @@ -0,0 +1,2 @@ +script#pet-template(type='text/x-template') + include auxiliary/pet.pug diff --git a/src/test-data/pug-linker/test/cases-src/include.yield.nested.pug b/src/test-data/pug-linker/test/cases-src/include.yield.nested.pug new file mode 100644 index 0000000..f4a7d69 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/include.yield.nested.pug @@ -0,0 +1,4 @@ + +include auxiliary/yield-nested.pug + p some content + p and some more diff --git a/src/test-data/pug-linker/test/cases-src/includes-with-ext-js.pug b/src/test-data/pug-linker/test/cases-src/includes-with-ext-js.pug new file mode 100644 index 0000000..65bfa8a --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/includes-with-ext-js.pug @@ -0,0 +1,3 @@ +pre + code + include javascript-new-lines.js diff --git a/src/test-data/pug-linker/test/cases-src/includes.pug b/src/test-data/pug-linker/test/cases-src/includes.pug new file mode 100644 index 0000000..7761ce2 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/includes.pug @@ -0,0 +1,10 @@ + +include auxiliary/mixins.pug + ++foo + +body + include auxiliary/smile.html + include auxiliary/escapes.html + script(type="text/javascript") + include:verbatim auxiliary/includable.js diff --git a/src/test-data/pug-linker/test/cases-src/javascript-new-lines.js b/src/test-data/pug-linker/test/cases-src/javascript-new-lines.js new file mode 100644 index 0000000..bb0c26f --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/javascript-new-lines.js @@ -0,0 +1 @@ +var x = '\n here is some \n new lined text'; diff --git a/src/test-data/pug-linker/test/cases-src/layout.append.pug b/src/test-data/pug-linker/test/cases-src/layout.append.pug new file mode 100644 index 0000000..d771bc9 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/layout.append.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/append/app-layout.pug + +block append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/cases-src/layout.append.without-block.pug b/src/test-data/pug-linker/test/cases-src/layout.append.without-block.pug new file mode 100644 index 0000000..19842fc --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/layout.append.without-block.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/append-without-block/app-layout.pug + +append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug b/src/test-data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug new file mode 100644 index 0000000..79d15b1 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/layout.multi.append.prepend.block.pug @@ -0,0 +1,19 @@ +extends ../fixtures/multi-append-prepend-block/redefine.pug + +append content + p.first.append Something appended to content + +prepend content + p.first.prepend Something prepended to content + +append content + p.last.append Last append must be most last + +prepend content + p.last.prepend Last prepend must appear at top + +append head + script(src='jquery.js') + +prepend head + script(src='foo.js') diff --git a/src/test-data/pug-linker/test/cases-src/layout.prepend.pug b/src/test-data/pug-linker/test/cases-src/layout.prepend.pug new file mode 100644 index 0000000..4659a11 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/layout.prepend.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/prepend/app-layout.pug + +block prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/cases-src/layout.prepend.without-block.pug b/src/test-data/pug-linker/test/cases-src/layout.prepend.without-block.pug new file mode 100644 index 0000000..516d01b --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/layout.prepend.without-block.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/prepend-without-block/app-layout.pug + +prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/cases-src/some-included.styl b/src/test-data/pug-linker/test/cases-src/some-included.styl new file mode 100644 index 0000000..7458543 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/some-included.styl @@ -0,0 +1,2 @@ +body + padding 10px diff --git a/src/test-data/pug-linker/test/cases-src/some.md b/src/test-data/pug-linker/test/cases-src/some.md new file mode 100644 index 0000000..8ea3e54 --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/some.md @@ -0,0 +1,3 @@ +Just _some_ markdown **tests**. + +With new line. diff --git a/src/test-data/pug-linker/test/cases-src/some.styl b/src/test-data/pug-linker/test/cases-src/some.styl new file mode 100644 index 0000000..f77222d --- /dev/null +++ b/src/test-data/pug-linker/test/cases-src/some.styl @@ -0,0 +1 @@ +@import "some-included" diff --git a/src/test-data/pug-linker/test/cases/include-extends-from-root.input.json b/src/test-data/pug-linker/test/cases/include-extends-from-root.input.json new file mode 100644 index 0000000..dc58773 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-extends-from-root.input.json @@ -0,0 +1,201 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 1, + "filename": "include-extends-from-root.pug", + "path": "/auxiliary/extends-from-root.pug", + "fullPath": "auxiliary/extends-from-root.pug", + "str": "extends /auxiliary/layout.pug\n\nblock content\n include /auxiliary/include-from-root.pug\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "/auxiliary/layout.pug", + "line": 1, + "filename": "auxiliary/extends-from-root.pug", + "fullPath": "auxiliary/layout.pug", + "str": "html\n head\n title My Application\n block head\n body\n block content", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "title", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "My Application", + "line": 3, + "filename": "auxiliary/layout.pug" + } + ], + "line": 3, + "filename": "auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "auxiliary/layout.pug" + }, + { + "type": "NamedBlock", + "nodes": [], + "line": 4, + "filename": "auxiliary/layout.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 2, + "filename": "auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "auxiliary/layout.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 6, + "filename": "auxiliary/layout.pug", + "name": "content", + "mode": "replace" + } + ], + "line": 5, + "filename": "auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "auxiliary/layout.pug" + } + ], + "line": 1, + "filename": "auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "auxiliary/layout.pug" + } + ], + "line": 0, + "filename": "auxiliary/layout.pug" + } + }, + "line": 1, + "filename": "auxiliary/extends-from-root.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 4, + "filename": "auxiliary/extends-from-root.pug", + "path": "/auxiliary/include-from-root.pug", + "fullPath": "auxiliary/include-from-root.pug", + "str": "h1 hello", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "h1", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "hello", + "line": 1, + "filename": "auxiliary/include-from-root.pug" + } + ], + "line": 1, + "filename": "auxiliary/include-from-root.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "auxiliary/include-from-root.pug" + } + ], + "line": 0, + "filename": "auxiliary/include-from-root.pug" + } + }, + "line": 4, + "filename": "auxiliary/extends-from-root.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 4, + "filename": "auxiliary/extends-from-root.pug" + } + } + ], + "line": 3, + "filename": "auxiliary/extends-from-root.pug", + "name": "content", + "mode": "replace" + } + ], + "line": 0, + "filename": "auxiliary/extends-from-root.pug" + } + }, + "line": 1, + "filename": "include-extends-from-root.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 1, + "filename": "include-extends-from-root.pug" + } + } + ], + "line": 0, + "filename": "include-extends-from-root.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-extends-of-common-template.input.json b/src/test-data/pug-linker/test/cases/include-extends-of-common-template.input.json new file mode 100644 index 0000000..a20f7f2 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-extends-of-common-template.input.json @@ -0,0 +1,179 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 1, + "filename": "include-extends-of-common-template.pug", + "path": "auxiliary/extends-empty-block-1.pug", + "fullPath": "auxiliary/extends-empty-block-1.pug", + "str": "extends empty-block.pug\n\nblock test\n div test1\n\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "empty-block.pug", + "line": 1, + "filename": "auxiliary/extends-empty-block-1.pug", + "fullPath": "auxiliary/empty-block.pug", + "str": "block test\n\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 1, + "filename": "auxiliary/empty-block.pug", + "name": "test", + "mode": "replace" + } + ], + "line": 0, + "filename": "auxiliary/empty-block.pug" + } + }, + "line": 1, + "filename": "auxiliary/extends-empty-block-1.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "test1", + "line": 4, + "filename": "auxiliary/extends-empty-block-1.pug" + } + ], + "line": 4, + "filename": "auxiliary/extends-empty-block-1.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "auxiliary/extends-empty-block-1.pug" + } + ], + "line": 3, + "filename": "auxiliary/extends-empty-block-1.pug", + "name": "test", + "mode": "replace" + } + ], + "line": 0, + "filename": "auxiliary/extends-empty-block-1.pug" + } + }, + "line": 1, + "filename": "include-extends-of-common-template.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 1, + "filename": "include-extends-of-common-template.pug" + } + }, + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 2, + "filename": "include-extends-of-common-template.pug", + "path": "auxiliary/extends-empty-block-2.pug", + "fullPath": "auxiliary/extends-empty-block-2.pug", + "str": "extends empty-block.pug\n\nblock test\n div test2\n\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "empty-block.pug", + "line": 1, + "filename": "auxiliary/extends-empty-block-2.pug", + "fullPath": "auxiliary/empty-block.pug", + "str": "block test\n\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 1, + "filename": "auxiliary/empty-block.pug", + "name": "test", + "mode": "replace" + } + ], + "line": 0, + "filename": "auxiliary/empty-block.pug" + } + }, + "line": 1, + "filename": "auxiliary/extends-empty-block-2.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "test2", + "line": 4, + "filename": "auxiliary/extends-empty-block-2.pug" + } + ], + "line": 4, + "filename": "auxiliary/extends-empty-block-2.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "auxiliary/extends-empty-block-2.pug" + } + ], + "line": 3, + "filename": "auxiliary/extends-empty-block-2.pug", + "name": "test", + "mode": "replace" + } + ], + "line": 0, + "filename": "auxiliary/extends-empty-block-2.pug" + } + }, + "line": 2, + "filename": "include-extends-of-common-template.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 2, + "filename": "include-extends-of-common-template.pug" + } + } + ], + "line": 0, + "filename": "include-extends-of-common-template.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-extends-relative.input.json b/src/test-data/pug-linker/test/cases/include-extends-relative.input.json new file mode 100644 index 0000000..7f3d560 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-extends-relative.input.json @@ -0,0 +1,166 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 1, + "filename": "include-extends-relative.pug", + "path": "../cases-src/auxiliary/extends-relative.pug", + "fullPath": "../cases-src/auxiliary/extends-relative.pug", + "str": "extends ../../cases-src/auxiliary/layout\n\nblock content\n include ../../cases-src/auxiliary/include-from-root\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../../cases-src/auxiliary/layout", + "line": 1, + "filename": "../cases-src/auxiliary/extends-relative.pug", + "fullPath": "../cases-src/auxiliary/layout.pug", + "str": "html\n head\n title My Application\n block head\n body\n block content", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "title", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "My Application", + "line": 3, + "filename": "../cases-src/auxiliary/layout.pug" + } + ], + "line": 3, + "filename": "../cases-src/auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "../cases-src/auxiliary/layout.pug" + }, + { + "type": "NamedBlock", + "nodes": [], + "line": 4, + "filename": "../cases-src/auxiliary/layout.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 2, + "filename": "../cases-src/auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "../cases-src/auxiliary/layout.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 6, + "filename": "../cases-src/auxiliary/layout.pug", + "name": "content", + "mode": "replace" + } + ], + "line": 5, + "filename": "../cases-src/auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../cases-src/auxiliary/layout.pug" + } + ], + "line": 1, + "filename": "../cases-src/auxiliary/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "../cases-src/auxiliary/layout.pug" + } + ], + "line": 0, + "filename": "../cases-src/auxiliary/layout.pug" + } + }, + "line": 1, + "filename": "../cases-src/auxiliary/extends-relative.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 4, + "filename": "../cases-src/auxiliary/extends-relative.pug", + "path": "../../cases-src/auxiliary/include-from-root", + "fullPath": "../cases-src/auxiliary/include-from-root.pug", + "str": "h1 hello" + }, + "line": 4, + "filename": "../cases-src/auxiliary/extends-relative.pug", + "filters": [] + } + ], + "line": 3, + "filename": "../cases-src/auxiliary/extends-relative.pug", + "name": "content", + "mode": "replace" + } + ], + "line": 0, + "filename": "../cases-src/auxiliary/extends-relative.pug" + } + }, + "line": 1, + "filename": "include-extends-relative.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 1, + "filename": "include-extends-relative.pug" + } + } + ], + "line": 0, + "filename": "include-extends-relative.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-filter-stylus.input.json b/src/test-data/pug-linker/test/cases/include-filter-stylus.input.json new file mode 100644 index 0000000..3145a07 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-filter-stylus.input.json @@ -0,0 +1,52 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "style", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 2, + "filename": "include-filter-stylus.pug", + "path": "some.styl", + "fullPath": "some.styl", + "str": "@import \"some-included\"\n" + }, + "line": 2, + "filename": "include-filter-stylus.pug", + "filters": [ + { + "type": "IncludeFilter", + "name": "stylus", + "attrs": [], + "line": 2, + "filename": "include-filter-stylus.pug" + } + ] + } + ], + "line": 1, + "filename": "include-filter-stylus.pug" + }, + "attrs": [ + { + "name": "type", + "val": "\"text/css\"", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include-filter-stylus.pug" + } + ], + "line": 0, + "filename": "include-filter-stylus.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-filter.input.json b/src/test-data/pug-linker/test/cases/include-filter.input.json new file mode 100644 index 0000000..1f5d0c4 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-filter.input.json @@ -0,0 +1,153 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 3, + "filename": "include-filter.pug", + "path": "some.md", + "fullPath": "some.md", + "str": "Just _some_ markdown **tests**.\n\nWith new line.\n" + }, + "line": 3, + "filename": "include-filter.pug", + "filters": [ + { + "type": "IncludeFilter", + "name": "markdown-it", + "attrs": [], + "line": 3, + "filename": "include-filter.pug" + } + ] + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 5, + "filename": "include-filter.pug", + "path": "include-filter-coffee.coffee", + "fullPath": "include-filter-coffee.coffee", + "str": "math =\n square: (value) -> value * value\n" + }, + "line": 5, + "filename": "include-filter.pug", + "filters": [ + { + "type": "IncludeFilter", + "name": "coffee-script", + "attrs": [ + { + "name": "minify", + "val": "true", + "mustEscape": true + } + ], + "line": 5, + "filename": "include-filter.pug" + } + ] + } + ], + "line": 4, + "filename": "include-filter.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "include-filter.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 7, + "filename": "include-filter.pug", + "path": "include-filter-coffee.coffee", + "fullPath": "include-filter-coffee.coffee", + "str": "math =\n square: (value) -> value * value\n" + }, + "line": 7, + "filename": "include-filter.pug", + "filters": [ + { + "type": "IncludeFilter", + "name": "coffee-script", + "attrs": [ + { + "name": "minify", + "val": "false", + "mustEscape": true + } + ], + "line": 7, + "filename": "include-filter.pug" + } + ] + } + ], + "line": 6, + "filename": "include-filter.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "include-filter.pug" + } + ], + "line": 2, + "filename": "include-filter.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "include-filter.pug" + } + ], + "line": 1, + "filename": "include-filter.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include-filter.pug" + } + ], + "line": 0, + "filename": "include-filter.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-only-text-body.input.json b/src/test-data/pug-linker/test/cases/include-only-text-body.input.json new file mode 100644 index 0000000..82d7656 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-only-text-body.input.json @@ -0,0 +1,24 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "The message is \"", + "line": 1, + "filename": "include-only-text-body.pug" + }, + { + "type": "YieldBlock", + "line": 2, + "filename": "include-only-text-body.pug" + }, + { + "type": "Text", + "val": "\"", + "line": 3, + "filename": "include-only-text-body.pug" + } + ], + "line": 0, + "filename": "include-only-text-body.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-only-text.input.json b/src/test-data/pug-linker/test/cases/include-only-text.input.json new file mode 100644 index 0000000..84d010a --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-only-text.input.json @@ -0,0 +1,125 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 4, + "filename": "include-only-text.pug", + "path": "include-only-text-body.pug", + "fullPath": "include-only-text-body.pug", + "str": "| The message is \"\nyield\n| \"\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "The message is \"", + "line": 1, + "filename": "include-only-text-body.pug" + }, + { + "type": "YieldBlock", + "line": 2, + "filename": "include-only-text-body.pug" + }, + { + "type": "Text", + "val": "\"", + "line": 3, + "filename": "include-only-text-body.pug" + } + ], + "line": 0, + "filename": "include-only-text-body.pug" + } + }, + "line": 4, + "filename": "include-only-text.pug", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "em", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "hello world", + "line": 5, + "filename": "include-only-text.pug" + } + ], + "line": 5, + "filename": "include-only-text.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": true, + "line": 5, + "filename": "include-only-text.pug" + } + ], + "line": 5, + "filename": "include-only-text.pug" + } + } + ], + "line": 3, + "filename": "include-only-text.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "include-only-text.pug" + } + ], + "line": 2, + "filename": "include-only-text.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "include-only-text.pug" + } + ], + "line": 1, + "filename": "include-only-text.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include-only-text.pug" + } + ], + "line": 0, + "filename": "include-only-text.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-with-text-head.input.json b/src/test-data/pug-linker/test/cases/include-with-text-head.input.json new file mode 100644 index 0000000..03d7151 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-with-text-head.input.json @@ -0,0 +1,53 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "alert('hello world');", + "line": 3 + } + ], + "line": 2, + "filename": "include-with-text-head.pug" + }, + "attrs": [ + { + "name": "type", + "val": "'text/javascript'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "include-with-text-head.pug", + "textOnly": true + } + ], + "line": 1, + "filename": "include-with-text-head.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include-with-text-head.pug" + } + ], + "line": 0, + "filename": "include-with-text-head.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include-with-text.input.json b/src/test-data/pug-linker/test/cases/include-with-text.input.json new file mode 100644 index 0000000..128c078 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include-with-text.input.json @@ -0,0 +1,141 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 2, + "filename": "include-with-text.pug", + "path": "include-with-text-head.pug", + "fullPath": "include-with-text-head.pug", + "str": "head\n script(type='text/javascript').\n alert('hello world');\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "alert('hello world');", + "line": 3 + } + ], + "line": 2, + "filename": "include-with-text-head.pug" + }, + "attrs": [ + { + "name": "type", + "val": "'text/javascript'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "include-with-text-head.pug", + "textOnly": true + } + ], + "line": 1, + "filename": "include-with-text-head.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include-with-text-head.pug" + } + ], + "line": 0, + "filename": "include-with-text-head.pug" + } + }, + "line": 2, + "filename": "include-with-text.pug", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 3, + "filename": "include-with-text.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'/caustic.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "include-with-text.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 4, + "filename": "include-with-text.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'/app.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "include-with-text.pug" + } + ], + "line": 3, + "filename": "include-with-text.pug" + } + } + ], + "line": 1, + "filename": "include-with-text.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include-with-text.pug" + } + ], + "line": 0, + "filename": "include-with-text.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include.script.input.json b/src/test-data/pug-linker/test/cases/include.script.input.json new file mode 100644 index 0000000..315b120 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include.script.input.json @@ -0,0 +1,130 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 2, + "filename": "include.script.pug", + "path": "auxiliary/pet.pug", + "fullPath": "auxiliary/pet.pug", + "str": ".pet\n h1 {{name}}\n p {{name}} is a {{species}} that is {{age}} old", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "h1", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "{{name}}", + "line": 2, + "filename": "auxiliary/pet.pug" + } + ], + "line": 2, + "filename": "auxiliary/pet.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "auxiliary/pet.pug" + }, + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "{{name}} is a {{species}} that is {{age}} old", + "line": 3, + "filename": "auxiliary/pet.pug" + } + ], + "line": 3, + "filename": "auxiliary/pet.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "auxiliary/pet.pug" + } + ], + "line": 1, + "filename": "auxiliary/pet.pug" + }, + "attrs": [ + { + "name": "class", + "val": "'pet'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "auxiliary/pet.pug" + } + ], + "line": 0, + "filename": "auxiliary/pet.pug" + } + }, + "line": 2, + "filename": "include.script.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 2, + "filename": "include.script.pug" + } + } + ], + "line": 1, + "filename": "include.script.pug" + }, + "attrs": [ + { + "name": "id", + "val": "'pet-template'", + "mustEscape": false + }, + { + "name": "type", + "val": "'text/x-template'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "include.script.pug" + } + ], + "line": 0, + "filename": "include.script.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/include.yield.nested.input.json b/src/test-data/pug-linker/test/cases/include.yield.nested.input.json new file mode 100644 index 0000000..3c69e4c --- /dev/null +++ b/src/test-data/pug-linker/test/cases/include.yield.nested.input.json @@ -0,0 +1,260 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 2, + "filename": "include.yield.nested.pug", + "path": "auxiliary/yield-nested.pug", + "fullPath": "auxiliary/yield-nested.pug", + "str": "html\n head\n title\n body\n h1 Page\n #content\n #content-wrapper\n yield\n #footer\n stuff", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "title", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 3, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 2, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "auxiliary/yield-nested.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "h1", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Page", + "line": 5, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 5, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "auxiliary/yield-nested.pug" + }, + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "YieldBlock", + "line": 8, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 7, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [ + { + "name": "id", + "val": "'content-wrapper'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 6, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [ + { + "name": "id", + "val": "'content'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "auxiliary/yield-nested.pug" + }, + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "stuff", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 10, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 10, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 9, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [ + { + "name": "id", + "val": "'footer'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 9, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 4, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 1, + "filename": "auxiliary/yield-nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "auxiliary/yield-nested.pug" + } + ], + "line": 0, + "filename": "auxiliary/yield-nested.pug" + } + }, + "line": 2, + "filename": "include.yield.nested.pug", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "some content", + "line": 3, + "filename": "include.yield.nested.pug" + } + ], + "line": 3, + "filename": "include.yield.nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "include.yield.nested.pug" + }, + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "and some more", + "line": 4, + "filename": "include.yield.nested.pug" + } + ], + "line": 4, + "filename": "include.yield.nested.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "include.yield.nested.pug" + } + ], + "line": 3, + "filename": "include.yield.nested.pug" + } + } + ], + "line": 0, + "filename": "include.yield.nested.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/includes-with-ext-js.input.json b/src/test-data/pug-linker/test/cases/includes-with-ext-js.input.json new file mode 100644 index 0000000..cd88d12 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/includes-with-ext-js.input.json @@ -0,0 +1,55 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "pre", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "code", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 3, + "filename": "includes-with-ext-js.pug", + "path": "javascript-new-lines.js", + "fullPath": "javascript-new-lines.js", + "str": "var x = \"\\n here is some \\n new lined text\";\n" + }, + "line": 3, + "filename": "includes-with-ext-js.pug", + "filters": [] + } + ], + "line": 2, + "filename": "includes-with-ext-js.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": true, + "line": 2, + "filename": "includes-with-ext-js.pug" + } + ], + "line": 1, + "filename": "includes-with-ext-js.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 1, + "filename": "includes-with-ext-js.pug" + } + ], + "line": 0, + "filename": "includes-with-ext-js.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/includes.input.json b/src/test-data/pug-linker/test/cases/includes.input.json new file mode 100644 index 0000000..79d5f89 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/includes.input.json @@ -0,0 +1,172 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 2, + "filename": "includes.pug", + "path": "auxiliary/mixins.pug", + "fullPath": "auxiliary/mixins.pug", + "str": "\nmixin foo()\n p bar", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Mixin", + "name": "foo", + "args": null, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "bar", + "line": 3, + "filename": "auxiliary/mixins.pug" + } + ], + "line": 3, + "filename": "auxiliary/mixins.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "auxiliary/mixins.pug" + } + ], + "line": 3, + "filename": "auxiliary/mixins.pug" + }, + "call": false, + "line": 2, + "filename": "auxiliary/mixins.pug" + } + ], + "line": 0, + "filename": "auxiliary/mixins.pug" + } + }, + "line": 2, + "filename": "includes.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 2, + "filename": "includes.pug" + } + }, + { + "type": "Mixin", + "name": "foo", + "args": null, + "block": null, + "call": true, + "attrs": [], + "attributeBlocks": [], + "line": 4, + "filename": "includes.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 7, + "filename": "includes.pug", + "path": "auxiliary/smile.html", + "fullPath": "auxiliary/smile.html", + "str": "

:)

\n" + }, + "line": 7, + "filename": "includes.pug", + "filters": [] + }, + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 8, + "filename": "includes.pug", + "path": "auxiliary/escapes.html", + "fullPath": "auxiliary/escapes.html", + "str": "\n" + }, + "line": 8, + "filename": "includes.pug", + "filters": [] + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "RawInclude", + "file": { + "type": "FileReference", + "line": 10, + "filename": "includes.pug", + "path": "auxiliary/includable.js", + "fullPath": "auxiliary/includable.js", + "str": "var STRING_SUBSTITUTIONS = { // table of character substitutions\n '\\t': '\\\\t',\n '\\r': '\\\\r',\n '\\n': '\\\\n',\n '\"' : '\\\\\"',\n '\\\\': '\\\\\\\\'\n};" + }, + "line": 10, + "filename": "includes.pug", + "filters": [ + { + "type": "IncludeFilter", + "name": "verbatim", + "attrs": [], + "line": 10, + "filename": "includes.pug" + } + ] + } + ], + "line": 9, + "filename": "includes.pug" + }, + "attrs": [ + { + "name": "type", + "val": "\"text/javascript\"", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 9, + "filename": "includes.pug" + } + ], + "line": 6, + "filename": "includes.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "includes.pug" + } + ], + "line": 0, + "filename": "includes.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/layout.append.input.json b/src/test-data/pug-linker/test/cases/layout.append.input.json new file mode 100644 index 0000000..2b8879a --- /dev/null +++ b/src/test-data/pug-linker/test/cases/layout.append.input.json @@ -0,0 +1,226 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/append/app-layout.pug", + "line": 2, + "filename": "layout.append.pug", + "fullPath": "../fixtures/append/app-layout.pug", + "str": "\nextends layout\n\nblock append head\n script(src='app.js')", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "layout", + "line": 2, + "filename": "../fixtures/append/app-layout.pug", + "fullPath": "../fixtures/append/layout.pug", + "str": "\nhtml\n block head\n script(src='vendor/jquery.js')\n script(src='vendor/caustic.js')\n body\n block body", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 4, + "filename": "../fixtures/append/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/jquery.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/append/layout.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/append/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/caustic.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/append/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/append/layout.pug", + "name": "head", + "mode": "replace" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 7, + "filename": "../fixtures/append/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 6, + "filename": "../fixtures/append/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "../fixtures/append/layout.pug" + } + ], + "line": 2, + "filename": "../fixtures/append/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "../fixtures/append/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/append/layout.pug" + } + }, + "line": 2, + "filename": "../fixtures/append/app-layout.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/append/app-layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'app.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/append/app-layout.pug" + } + ], + "line": 4, + "filename": "../fixtures/append/app-layout.pug", + "name": "head", + "mode": "append" + } + ], + "line": 0, + "filename": "../fixtures/append/app-layout.pug" + } + }, + "line": 2, + "filename": "layout.append.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "layout.append.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'foo.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "layout.append.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 6, + "filename": "layout.append.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'bar.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "layout.append.pug" + } + ], + "line": 4, + "filename": "layout.append.pug", + "name": "head", + "mode": "append" + } + ], + "line": 0, + "filename": "layout.append.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/layout.append.without-block.input.json b/src/test-data/pug-linker/test/cases/layout.append.without-block.input.json new file mode 100644 index 0000000..4fc3d87 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/layout.append.without-block.input.json @@ -0,0 +1,226 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/append-without-block/app-layout.pug", + "line": 2, + "filename": "layout.append.without-block.pug", + "fullPath": "../fixtures/append-without-block/app-layout.pug", + "str": "\nextends layout.pug\n\nappend head\n script(src='app.js')\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "layout.pug", + "line": 2, + "filename": "../fixtures/append-without-block/app-layout.pug", + "fullPath": "../fixtures/append-without-block/layout.pug", + "str": "\nhtml\n block head\n script(src='vendor/jquery.js')\n script(src='vendor/caustic.js')\n body\n block body", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 4, + "filename": "../fixtures/append-without-block/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/jquery.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/append-without-block/layout.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/append-without-block/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/caustic.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/append-without-block/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/append-without-block/layout.pug", + "name": "head", + "mode": "replace" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 7, + "filename": "../fixtures/append-without-block/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 6, + "filename": "../fixtures/append-without-block/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "../fixtures/append-without-block/layout.pug" + } + ], + "line": 2, + "filename": "../fixtures/append-without-block/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "../fixtures/append-without-block/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/append-without-block/layout.pug" + } + }, + "line": 2, + "filename": "../fixtures/append-without-block/app-layout.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/append-without-block/app-layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'app.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/append-without-block/app-layout.pug" + } + ], + "line": 4, + "filename": "../fixtures/append-without-block/app-layout.pug", + "name": "head", + "mode": "append" + } + ], + "line": 0, + "filename": "../fixtures/append-without-block/app-layout.pug" + } + }, + "line": 2, + "filename": "layout.append.without-block.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "layout.append.without-block.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'foo.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "layout.append.without-block.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 6, + "filename": "layout.append.without-block.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'bar.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "layout.append.without-block.pug" + } + ], + "line": 4, + "filename": "layout.append.without-block.pug", + "name": "head", + "mode": "append" + } + ], + "line": 0, + "filename": "layout.append.without-block.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json b/src/test-data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json new file mode 100644 index 0000000..cdd8d01 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/layout.multi.append.prepend.block.input.json @@ -0,0 +1,365 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/multi-append-prepend-block/redefine.pug", + "line": 1, + "filename": "layout.multi.append.prepend.block.pug", + "fullPath": "../fixtures/multi-append-prepend-block/redefine.pug", + "str": "extends root.pug\n\nblock content\n\t.content\n\t\t| Defined content\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "root.pug", + "line": 1, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "fullPath": "../fixtures/multi-append-prepend-block/root.pug", + "str": "block content\n\t| default content\n\nblock head\n\tscript(src='/app.js')", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Text", + "val": "default content", + "line": 2, + "filename": "../fixtures/multi-append-prepend-block/root.pug" + } + ], + "line": 1, + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "name": "content", + "mode": "replace" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/multi-append-prepend-block/root.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'/app.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/multi-append-prepend-block/root.pug" + } + ], + "line": 4, + "filename": "../fixtures/multi-append-prepend-block/root.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 0, + "filename": "../fixtures/multi-append-prepend-block/root.pug" + } + }, + "line": 1, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Defined content", + "line": 5, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug" + } + ], + "line": 4, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug" + }, + "attrs": [ + { + "name": "class", + "val": "'content'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug" + } + ], + "line": 3, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug", + "name": "content", + "mode": "replace" + } + ], + "line": 0, + "filename": "../fixtures/multi-append-prepend-block/redefine.pug" + } + }, + "line": 1, + "filename": "layout.multi.append.prepend.block.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Something appended to content", + "line": 4, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 4, + "filename": "layout.multi.append.prepend.block.pug" + }, + "attrs": [ + { + "name": "class", + "val": "'first'", + "mustEscape": false + }, + { + "name": "class", + "val": "'append'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 3, + "filename": "layout.multi.append.prepend.block.pug", + "name": "content", + "mode": "append" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Something prepended to content", + "line": 7, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 7, + "filename": "layout.multi.append.prepend.block.pug" + }, + "attrs": [ + { + "name": "class", + "val": "'first'", + "mustEscape": false + }, + { + "name": "class", + "val": "'prepend'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 6, + "filename": "layout.multi.append.prepend.block.pug", + "name": "content", + "mode": "prepend" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Last append must be most last", + "line": 10, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 10, + "filename": "layout.multi.append.prepend.block.pug" + }, + "attrs": [ + { + "name": "class", + "val": "'last'", + "mustEscape": false + }, + { + "name": "class", + "val": "'append'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 10, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 9, + "filename": "layout.multi.append.prepend.block.pug", + "name": "content", + "mode": "append" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Last prepend must appear at top", + "line": 13, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 13, + "filename": "layout.multi.append.prepend.block.pug" + }, + "attrs": [ + { + "name": "class", + "val": "'last'", + "mustEscape": false + }, + { + "name": "class", + "val": "'prepend'", + "mustEscape": false + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 13, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 12, + "filename": "layout.multi.append.prepend.block.pug", + "name": "content", + "mode": "prepend" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 16, + "filename": "layout.multi.append.prepend.block.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'jquery.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 16, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 15, + "filename": "layout.multi.append.prepend.block.pug", + "name": "head", + "mode": "append" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 19, + "filename": "layout.multi.append.prepend.block.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'foo.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 19, + "filename": "layout.multi.append.prepend.block.pug" + } + ], + "line": 18, + "filename": "layout.multi.append.prepend.block.pug", + "name": "head", + "mode": "prepend" + } + ], + "line": 0, + "filename": "layout.multi.append.prepend.block.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/layout.prepend.input.json b/src/test-data/pug-linker/test/cases/layout.prepend.input.json new file mode 100644 index 0000000..44a5cc0 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/layout.prepend.input.json @@ -0,0 +1,226 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/prepend/app-layout.pug", + "line": 2, + "filename": "layout.prepend.pug", + "fullPath": "../fixtures/prepend/app-layout.pug", + "str": "\nextends layout.pug\n\nblock prepend head\n script(src='app.js')\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "layout.pug", + "line": 2, + "filename": "../fixtures/prepend/app-layout.pug", + "fullPath": "../fixtures/prepend/layout.pug", + "str": "\nhtml\n block head\n script(src='vendor/jquery.js')\n script(src='vendor/caustic.js')\n body\n block body", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 4, + "filename": "../fixtures/prepend/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/jquery.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/prepend/layout.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/prepend/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/caustic.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/prepend/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/prepend/layout.pug", + "name": "head", + "mode": "replace" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 7, + "filename": "../fixtures/prepend/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 6, + "filename": "../fixtures/prepend/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "../fixtures/prepend/layout.pug" + } + ], + "line": 2, + "filename": "../fixtures/prepend/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "../fixtures/prepend/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/prepend/layout.pug" + } + }, + "line": 2, + "filename": "../fixtures/prepend/app-layout.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/prepend/app-layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'app.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/prepend/app-layout.pug" + } + ], + "line": 4, + "filename": "../fixtures/prepend/app-layout.pug", + "name": "head", + "mode": "prepend" + } + ], + "line": 0, + "filename": "../fixtures/prepend/app-layout.pug" + } + }, + "line": 2, + "filename": "layout.prepend.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "layout.prepend.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'foo.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "layout.prepend.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 6, + "filename": "layout.prepend.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'bar.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "layout.prepend.pug" + } + ], + "line": 4, + "filename": "layout.prepend.pug", + "name": "head", + "mode": "prepend" + } + ], + "line": 0, + "filename": "layout.prepend.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/cases/layout.prepend.without-block.input.json b/src/test-data/pug-linker/test/cases/layout.prepend.without-block.input.json new file mode 100644 index 0000000..d94e193 --- /dev/null +++ b/src/test-data/pug-linker/test/cases/layout.prepend.without-block.input.json @@ -0,0 +1,226 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/prepend-without-block/app-layout.pug", + "line": 2, + "filename": "layout.prepend.without-block.pug", + "fullPath": "../fixtures/prepend-without-block/app-layout.pug", + "str": "\nextends layout.pug\n\nprepend head\n script(src='app.js')\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "layout.pug", + "line": 2, + "filename": "../fixtures/prepend-without-block/app-layout.pug", + "fullPath": "../fixtures/prepend-without-block/layout.pug", + "str": "\nhtml\n block head\n script(src='vendor/jquery.js')\n script(src='vendor/caustic.js')\n body\n block body", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 4, + "filename": "../fixtures/prepend-without-block/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/jquery.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/prepend-without-block/layout.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/prepend-without-block/layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'vendor/caustic.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/prepend-without-block/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/prepend-without-block/layout.pug", + "name": "head", + "mode": "replace" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 7, + "filename": "../fixtures/prepend-without-block/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 6, + "filename": "../fixtures/prepend-without-block/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "../fixtures/prepend-without-block/layout.pug" + } + ], + "line": 2, + "filename": "../fixtures/prepend-without-block/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "../fixtures/prepend-without-block/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/prepend-without-block/layout.pug" + } + }, + "line": 2, + "filename": "../fixtures/prepend-without-block/app-layout.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "../fixtures/prepend-without-block/app-layout.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'app.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "../fixtures/prepend-without-block/app-layout.pug" + } + ], + "line": 4, + "filename": "../fixtures/prepend-without-block/app-layout.pug", + "name": "head", + "mode": "prepend" + } + ], + "line": 0, + "filename": "../fixtures/prepend-without-block/app-layout.pug" + } + }, + "line": 2, + "filename": "layout.prepend.without-block.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 5, + "filename": "layout.prepend.without-block.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'foo.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 5, + "filename": "layout.prepend.without-block.pug" + }, + { + "type": "Tag", + "name": "script", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 6, + "filename": "layout.prepend.without-block.pug" + }, + "attrs": [ + { + "name": "src", + "val": "'bar.js'", + "mustEscape": true + } + ], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "layout.prepend.without-block.pug" + } + ], + "line": 4, + "filename": "layout.prepend.without-block.pug", + "name": "head", + "mode": "prepend" + } + ], + "line": 0, + "filename": "layout.prepend.without-block.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/errors-src/child-with-tags.pug b/src/test-data/pug-linker/test/errors-src/child-with-tags.pug new file mode 100644 index 0000000..fb439dd --- /dev/null +++ b/src/test-data/pug-linker/test/errors-src/child-with-tags.pug @@ -0,0 +1,6 @@ +extend ../fixtures/layout + +block body + p Hello world! + +p BAD!!! diff --git a/src/test-data/pug-linker/test/errors-src/extends-not-first.pug b/src/test-data/pug-linker/test/errors-src/extends-not-first.pug new file mode 100644 index 0000000..47249bc --- /dev/null +++ b/src/test-data/pug-linker/test/errors-src/extends-not-first.pug @@ -0,0 +1,4 @@ +block body + p Hey + +extends ../fixtures/layout diff --git a/src/test-data/pug-linker/test/errors-src/unexpected-block.pug b/src/test-data/pug-linker/test/errors-src/unexpected-block.pug new file mode 100644 index 0000000..5d56192 --- /dev/null +++ b/src/test-data/pug-linker/test/errors-src/unexpected-block.pug @@ -0,0 +1,4 @@ +extends ../fixtures/empty.pug + +block foo + div Hello World diff --git a/src/test-data/pug-linker/test/errors/child-with-tags.input.json b/src/test-data/pug-linker/test/errors/child-with-tags.input.json new file mode 100644 index 0000000..8875e2e --- /dev/null +++ b/src/test-data/pug-linker/test/errors/child-with-tags.input.json @@ -0,0 +1,163 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/layout", + "line": 1, + "filename": "child-with-tags.pug", + "fullPath": "../fixtures/layout.pug", + "str": "doctype\n\nhtml\n head\n block head\n Hello world!\n body\n block body\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Doctype", + "val": "", + "line": 1, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Text", + "val": "Hello world!", + "filename": "../fixtures/layout.pug", + "line": 6, + "isHtml": true + } + ], + "line": 5, + "filename": "../fixtures/layout.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 4, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 8, + "filename": "../fixtures/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 7, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "../fixtures/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "../fixtures/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/layout.pug" + } + }, + "line": 1, + "filename": "child-with-tags.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Hello world!", + "line": 4, + "filename": "child-with-tags.pug" + } + ], + "line": 4, + "filename": "child-with-tags.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "child-with-tags.pug" + } + ], + "line": 3, + "filename": "child-with-tags.pug", + "name": "body", + "mode": "replace" + }, + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "BAD!!!", + "line": 6, + "filename": "child-with-tags.pug" + } + ], + "line": 6, + "filename": "child-with-tags.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 6, + "filename": "child-with-tags.pug" + } + ], + "line": 0, + "filename": "child-with-tags.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/errors/extends-not-first.input.json b/src/test-data/pug-linker/test/errors/extends-not-first.input.json new file mode 100644 index 0000000..44f8b47 --- /dev/null +++ b/src/test-data/pug-linker/test/errors/extends-not-first.input.json @@ -0,0 +1,140 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Hey", + "line": 2, + "filename": "extends-not-first.pug" + } + ], + "line": 2, + "filename": "extends-not-first.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 2, + "filename": "extends-not-first.pug" + } + ], + "line": 1, + "filename": "extends-not-first.pug", + "name": "body", + "mode": "replace" + }, + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/layout", + "line": 4, + "filename": "extends-not-first.pug", + "fullPath": "../fixtures/layout.pug", + "str": "doctype\n\nhtml\n head\n block head\n Hello world!\n body\n block body\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Doctype", + "val": "", + "line": 1, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Text", + "val": "Hello world!", + "filename": "../fixtures/layout.pug", + "line": 6, + "isHtml": true + } + ], + "line": 5, + "filename": "../fixtures/layout.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 4, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 8, + "filename": "../fixtures/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 7, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "../fixtures/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "../fixtures/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/layout.pug" + } + }, + "line": 4, + "filename": "extends-not-first.pug" + } + ], + "line": 0, + "filename": "extends-not-first.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/errors/unexpected-block.input.json b/src/test-data/pug-linker/test/errors/unexpected-block.input.json new file mode 100644 index 0000000..4433046 --- /dev/null +++ b/src/test-data/pug-linker/test/errors/unexpected-block.input.json @@ -0,0 +1,58 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/empty.pug", + "line": 1, + "filename": "unexpected-block.pug", + "fullPath": "../fixtures/empty.pug", + "str": "", + "ast": { + "type": "Block", + "nodes": [], + "line": 0, + "filename": "../fixtures/empty.pug" + } + }, + "line": 1, + "filename": "unexpected-block.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "div", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Hello World", + "line": 4, + "filename": "unexpected-block.pug" + } + ], + "line": 4, + "filename": "unexpected-block.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "unexpected-block.pug" + } + ], + "line": 3, + "filename": "unexpected-block.pug", + "name": "foo", + "mode": "replace" + } + ], + "line": 0, + "filename": "unexpected-block.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/append-without-block/app-layout.pug b/src/test-data/pug-linker/test/fixtures/append-without-block/app-layout.pug new file mode 100644 index 0000000..1b55872 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append-without-block/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout.pug + +append head + script(src='app.js') diff --git a/src/test-data/pug-linker/test/fixtures/append-without-block/layout.pug b/src/test-data/pug-linker/test/fixtures/append-without-block/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append-without-block/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/append-without-block/page.pug b/src/test-data/pug-linker/test/fixtures/append-without-block/page.pug new file mode 100644 index 0000000..e607ae7 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append-without-block/page.pug @@ -0,0 +1,6 @@ + +extends app-layout.pug + +append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/fixtures/append/app-layout.pug b/src/test-data/pug-linker/test/fixtures/append/app-layout.pug new file mode 100644 index 0000000..48bf886 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout + +block append head + script(src='app.js') \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/append/layout.pug b/src/test-data/pug-linker/test/fixtures/append/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/append/page.html b/src/test-data/pug-linker/test/fixtures/append/page.html new file mode 100644 index 0000000..bc5e126 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append/page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug-linker/test/fixtures/append/page.pug b/src/test-data/pug-linker/test/fixtures/append/page.pug new file mode 100644 index 0000000..1ae9909 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/append/page.pug @@ -0,0 +1,6 @@ + +extends app-layout + +block append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/fixtures/empty.pug b/src/test-data/pug-linker/test/fixtures/empty.pug new file mode 100644 index 0000000..e69de29 diff --git a/src/test-data/pug-linker/test/fixtures/layout.pug b/src/test-data/pug-linker/test/fixtures/layout.pug new file mode 100644 index 0000000..aaa3c63 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/layout.pug @@ -0,0 +1,8 @@ +doctype + +html + head + block head + Hello world! + body + block body diff --git a/src/test-data/pug-linker/test/fixtures/mixins.pug b/src/test-data/pug-linker/test/fixtures/mixins.pug new file mode 100644 index 0000000..e20550a --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/mixins.pug @@ -0,0 +1,2 @@ +mixin image(src) + img(cl-src=src)&attributes(attributes) diff --git a/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug b/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug new file mode 100644 index 0000000..abc178e --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/redefine.pug @@ -0,0 +1,5 @@ +extends root.pug + +block content + .content + | Defined content diff --git a/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug b/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug new file mode 100644 index 0000000..8e3334a --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/multi-append-prepend-block/root.pug @@ -0,0 +1,5 @@ +block content + | default content + +block head + script(src='/app.js') \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug b/src/test-data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug new file mode 100644 index 0000000..53f89ba --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend-without-block/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout.pug + +prepend head + script(src='app.js') diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/layout.pug b/src/test-data/pug-linker/test/fixtures/prepend-without-block/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend-without-block/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.html b/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.html new file mode 100644 index 0000000..8753a42 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.pug b/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.pug new file mode 100644 index 0000000..6b9bb01 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend-without-block/page.pug @@ -0,0 +1,6 @@ + +extends app-layout.pug + +prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/fixtures/prepend/app-layout.pug b/src/test-data/pug-linker/test/fixtures/prepend/app-layout.pug new file mode 100644 index 0000000..7040eec --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout.pug + +block prepend head + script(src='app.js') diff --git a/src/test-data/pug-linker/test/fixtures/prepend/layout.pug b/src/test-data/pug-linker/test/fixtures/prepend/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug-linker/test/fixtures/prepend/page.html b/src/test-data/pug-linker/test/fixtures/prepend/page.html new file mode 100644 index 0000000..8753a42 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend/page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug-linker/test/fixtures/prepend/page.pug b/src/test-data/pug-linker/test/fixtures/prepend/page.pug new file mode 100644 index 0000000..c2a91c9 --- /dev/null +++ b/src/test-data/pug-linker/test/fixtures/prepend/page.pug @@ -0,0 +1,6 @@ + +extends app-layout.pug + +block prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug-linker/test/index.test.js b/src/test-data/pug-linker/test/index.test.js new file mode 100644 index 0000000..862629d --- /dev/null +++ b/src/test-data/pug-linker/test/index.test.js @@ -0,0 +1,46 @@ +var assert = require('assert'); +var fs = require('fs'); +var link = require('../'); + +function testDir(dir) { + fs.readdirSync(dir).forEach(function(name) { + if (!/\.input\.json$/.test(name)) return; + test(name, function() { + var actual = link(JSON.parse(fs.readFileSync(dir + '/' + name, 'utf8'))); + expect(actual).toMatchSnapshot(); + }); + }); +} + +function testDirError(dir) { + fs.readdirSync(dir).forEach(function(name) { + if (!/\.input\.json$/.test(name)) return; + test(name, function() { + var input = JSON.parse(fs.readFileSync(dir + '/' + name, 'utf8')); + var err; + try { + link(input); + } catch (ex) { + err = { + msg: ex.msg, + code: ex.code, + line: ex.line, + }; + } + if (!err) throw new Error('Expected error'); + expect(err).toMatchSnapshot(); + }); + }); +} + +describe('cases from pug', function() { + testDir(__dirname + '/cases'); +}); + +describe('special cases', function() { + testDir(__dirname + '/special-cases'); +}); + +describe('error handling', function() { + testDirError(__dirname + '/errors'); +}); diff --git a/src/test-data/pug-linker/test/special-cases-src/extending-empty.pug b/src/test-data/pug-linker/test/special-cases-src/extending-empty.pug new file mode 100644 index 0000000..0b87566 --- /dev/null +++ b/src/test-data/pug-linker/test/special-cases-src/extending-empty.pug @@ -0,0 +1 @@ +extend ../fixtures/empty.pug diff --git a/src/test-data/pug-linker/test/special-cases-src/extending-include.pug b/src/test-data/pug-linker/test/special-cases-src/extending-include.pug new file mode 100644 index 0000000..5dd10ec --- /dev/null +++ b/src/test-data/pug-linker/test/special-cases-src/extending-include.pug @@ -0,0 +1,5 @@ +extend ../fixtures/layout.pug +include ../fixtures/mixins.pug + +block body + +image('myimg.png').with-border(alt="My image") diff --git a/src/test-data/pug-linker/test/special-cases-src/root-mixin.pug b/src/test-data/pug-linker/test/special-cases-src/root-mixin.pug new file mode 100644 index 0000000..c81b2b5 --- /dev/null +++ b/src/test-data/pug-linker/test/special-cases-src/root-mixin.pug @@ -0,0 +1,9 @@ +extend ../fixtures/layout.pug + +mixin myMixin + p Hello world + +block body + p Before + +myMixin + p After diff --git a/src/test-data/pug-linker/test/special-cases/extending-empty.input.json b/src/test-data/pug-linker/test/special-cases/extending-empty.input.json new file mode 100644 index 0000000..532adb5 --- /dev/null +++ b/src/test-data/pug-linker/test/special-cases/extending-empty.input.json @@ -0,0 +1,26 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/empty.pug", + "line": 1, + "filename": "extending-empty.pug", + "fullPath": "../fixtures/empty.pug", + "str": "", + "ast": { + "type": "Block", + "nodes": [], + "line": 0, + "filename": "../fixtures/empty.pug" + } + }, + "line": 1, + "filename": "extending-empty.pug" + } + ], + "line": 0, + "filename": "extending-empty.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/special-cases/extending-include.input.json b/src/test-data/pug-linker/test/special-cases/extending-include.input.json new file mode 100644 index 0000000..495311e --- /dev/null +++ b/src/test-data/pug-linker/test/special-cases/extending-include.input.json @@ -0,0 +1,204 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/layout.pug", + "line": 1, + "filename": "extending-include.pug", + "fullPath": "../fixtures/layout.pug", + "str": "doctype\n\nhtml\n head\n block head\n Hello world!\n body\n block body\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Doctype", + "val": "", + "line": 1, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Text", + "val": "Hello world!", + "filename": "../fixtures/layout.pug", + "line": 6, + "isHtml": true + } + ], + "line": 5, + "filename": "../fixtures/layout.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 4, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 8, + "filename": "../fixtures/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 7, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "../fixtures/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "../fixtures/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/layout.pug" + } + }, + "line": 1, + "filename": "extending-include.pug" + }, + { + "type": "Include", + "file": { + "type": "FileReference", + "line": 2, + "filename": "extending-include.pug", + "path": "../fixtures/mixins.pug", + "fullPath": "../fixtures/mixins.pug", + "str": "mixin image(src)\n img(cl-src=src)&attributes(attributes)\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Mixin", + "name": "image", + "args": "src", + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "img", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [], + "line": 2, + "filename": "../fixtures/mixins.pug" + }, + "attrs": [ + { + "name": "cl-src", + "val": "src", + "mustEscape": true + } + ], + "attributeBlocks": [ + "attributes" + ], + "isInline": true, + "line": 2, + "filename": "../fixtures/mixins.pug" + } + ], + "line": 2, + "filename": "../fixtures/mixins.pug" + }, + "call": false, + "line": 1, + "filename": "../fixtures/mixins.pug" + } + ], + "line": 0, + "filename": "../fixtures/mixins.pug" + } + }, + "line": 2, + "filename": "extending-include.pug", + "block": { + "type": "Block", + "nodes": [], + "line": 2, + "filename": "extending-include.pug" + } + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Mixin", + "name": "image", + "args": "'myimg.png'", + "block": null, + "call": true, + "attrs": [ + { + "name": "class", + "val": "'with-border'", + "mustEscape": false + }, + { + "name": "alt", + "val": "\"My image\"", + "mustEscape": true + } + ], + "attributeBlocks": [], + "line": 5, + "filename": "extending-include.pug" + } + ], + "line": 4, + "filename": "extending-include.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 0, + "filename": "extending-include.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-linker/test/special-cases/root-mixin.input.json b/src/test-data/pug-linker/test/special-cases/root-mixin.input.json new file mode 100644 index 0000000..743250e --- /dev/null +++ b/src/test-data/pug-linker/test/special-cases/root-mixin.input.json @@ -0,0 +1,212 @@ +{ + "type": "Block", + "nodes": [ + { + "type": "Extends", + "file": { + "type": "FileReference", + "path": "../fixtures/layout.pug", + "line": 1, + "filename": "root-mixin.pug", + "fullPath": "../fixtures/layout.pug", + "str": "doctype\n\nhtml\n head\n block head\n Hello world!\n body\n block body\n", + "ast": { + "type": "Block", + "nodes": [ + { + "type": "Doctype", + "val": "", + "line": 1, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "html", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "head", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Text", + "val": "Hello world!", + "filename": "../fixtures/layout.pug", + "line": 6, + "isHtml": true + } + ], + "line": 5, + "filename": "../fixtures/layout.pug", + "name": "head", + "mode": "replace" + } + ], + "line": 4, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "../fixtures/layout.pug" + }, + { + "type": "Tag", + "name": "body", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "NamedBlock", + "nodes": [], + "line": 8, + "filename": "../fixtures/layout.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 7, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "../fixtures/layout.pug" + } + ], + "line": 3, + "filename": "../fixtures/layout.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 3, + "filename": "../fixtures/layout.pug" + } + ], + "line": 0, + "filename": "../fixtures/layout.pug" + } + }, + "line": 1, + "filename": "root-mixin.pug" + }, + { + "type": "Mixin", + "name": "myMixin", + "args": null, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Hello world", + "line": 4, + "filename": "root-mixin.pug" + } + ], + "line": 4, + "filename": "root-mixin.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 4, + "filename": "root-mixin.pug" + } + ], + "line": 4, + "filename": "root-mixin.pug" + }, + "call": false, + "line": 3, + "filename": "root-mixin.pug" + }, + { + "type": "NamedBlock", + "nodes": [ + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "Before", + "line": 7, + "filename": "root-mixin.pug" + } + ], + "line": 7, + "filename": "root-mixin.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 7, + "filename": "root-mixin.pug" + }, + { + "type": "Mixin", + "name": "myMixin", + "args": null, + "block": null, + "call": true, + "attrs": [], + "attributeBlocks": [], + "line": 8, + "filename": "root-mixin.pug" + }, + { + "type": "Tag", + "name": "p", + "selfClosing": false, + "block": { + "type": "Block", + "nodes": [ + { + "type": "Text", + "val": "After", + "line": 9, + "filename": "root-mixin.pug" + } + ], + "line": 9, + "filename": "root-mixin.pug" + }, + "attrs": [], + "attributeBlocks": [], + "isInline": false, + "line": 9, + "filename": "root-mixin.pug" + } + ], + "line": 6, + "filename": "root-mixin.pug", + "name": "body", + "mode": "replace" + } + ], + "line": 0, + "filename": "root-mixin.pug" +} \ No newline at end of file diff --git a/src/test-data/pug-load/test/__snapshots__/index.test.js.snap b/src/test-data/pug-load/test/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..6ad59e8 --- /dev/null +++ b/src/test-data/pug-load/test/__snapshots__/index.test.js.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pug-load 1`] = ` +Object { + "filename": "/foo.pug", + "line": 0, + "nodes": Array [ + Object { + "column": 1, + "file": Object { + "ast": Object { + "filename": "/bar.pug", + "line": 0, + "nodes": Array [ + Object { + "column": 1, + "filename": "/bar.pug", + "line": 1, + "mode": "replace", + "name": "bing", + "nodes": Array [], + "type": "NamedBlock", + }, + ], + "type": "Block", + }, + "column": 9, + "filename": "/foo.pug", + "fullPath": "/bar.pug", + "line": 1, + "path": "bar.pug", + "raw": Object { + "hash": "538bf7d4b81ef364b1f2e9d42c11f156", + "size": 11, + "type": "Buffer", + }, + "str": "block bing +", + "type": "FileReference", + }, + "filename": "/foo.pug", + "line": 1, + "type": "Extends", + }, + Object { + "column": 1, + "filename": "/foo.pug", + "line": 3, + "mode": "replace", + "name": "bing", + "nodes": Array [ + Object { + "block": Object { + "filename": "/foo.pug", + "line": 4, + "nodes": Array [], + "type": "Block", + }, + "column": 3, + "file": Object { + "ast": Object { + "filename": "/bing.pug", + "line": 0, + "nodes": Array [ + Object { + "attributeBlocks": Array [], + "attrs": Array [ + Object { + "column": 1, + "filename": "/packages/pug-load/test/bing.pug", + "line": 1, + "mustEscape": false, + "name": "class", + "val": "'bing'", + }, + ], + "block": Object { + "filename": "/bing.pug", + "line": 1, + "nodes": Array [ + Object { + "column": 7, + "filename": "/bing.pug", + "line": 1, + "type": "Text", + "val": "bong", + }, + ], + "type": "Block", + }, + "column": 1, + "filename": "/bing.pug", + "isInline": false, + "line": 1, + "name": "div", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "Block", + }, + "column": 11, + "filename": "/foo.pug", + "fullPath": "/bing.pug", + "line": 4, + "path": "bing.pug", + "raw": Object { + "hash": "58ecbe086e7a045084cbddac849a2563", + "size": 11, + "type": "Buffer", + }, + "str": ".bing bong +", + "type": "FileReference", + }, + "filename": "/foo.pug", + "line": 4, + "type": "Include", + }, + Object { + "attributeBlocks": Array [], + "attrs": Array [], + "block": Object { + "filename": "/foo.pug", + "line": 5, + "nodes": Array [ + Object { + "column": 5, + "file": Object { + "column": 13, + "filename": "/foo.pug", + "fullPath": "/script.js", + "line": 6, + "path": "script.js", + "raw": Object { + "hash": "86d4f8e34165faeb09f10255121078f8", + "size": 32, + "type": "Buffer", + }, + "str": "document.write('hello world!'); +", + "type": "FileReference", + }, + "filename": "/foo.pug", + "filters": Array [], + "line": 6, + "type": "RawInclude", + }, + ], + "type": "Block", + }, + "column": 3, + "filename": "/foo.pug", + "isInline": false, + "line": 5, + "name": "script", + "selfClosing": false, + "type": "Tag", + }, + ], + "type": "NamedBlock", + }, + ], + "type": "Block", +} +`; diff --git a/src/test-data/pug-load/test/bar.pug b/src/test-data/pug-load/test/bar.pug new file mode 100644 index 0000000..24e3cee --- /dev/null +++ b/src/test-data/pug-load/test/bar.pug @@ -0,0 +1 @@ +block bing diff --git a/src/test-data/pug-load/test/bing.pug b/src/test-data/pug-load/test/bing.pug new file mode 100644 index 0000000..9013b36 --- /dev/null +++ b/src/test-data/pug-load/test/bing.pug @@ -0,0 +1 @@ +.bing bong diff --git a/src/test-data/pug-load/test/foo.pug b/src/test-data/pug-load/test/foo.pug new file mode 100644 index 0000000..d30a98f --- /dev/null +++ b/src/test-data/pug-load/test/foo.pug @@ -0,0 +1,6 @@ +extends bar.pug + +block bing + include bing.pug + script + include script.js diff --git a/src/test-data/pug-load/test/index.test.js b/src/test-data/pug-load/test/index.test.js new file mode 100644 index 0000000..f8b21fb --- /dev/null +++ b/src/test-data/pug-load/test/index.test.js @@ -0,0 +1,30 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var assert = require('assert'); +var walk = require('pug-walk'); +var lex = require('pug-lexer'); +var parse = require('pug-parser'); +var load = require('../'); + +test('pug-load', () => { + var filename = __dirname + '/foo.pug'; + var ast = load.file(filename, { + lex: lex, + parse: parse, + }); + + ast = walk( + ast, + function(node) { + if (node.filename) + node.filename = '/' + path.basename(node.filename); + if (node.fullPath) + node.fullPath = '/' + path.basename(node.fullPath); + }, + {includeDependencies: true} + ); + + expect(ast).toMatchSnapshot(); +}); diff --git a/src/test-data/pug-load/test/script.js b/src/test-data/pug-load/test/script.js new file mode 100644 index 0000000..1e07719 --- /dev/null +++ b/src/test-data/pug-load/test/script.js @@ -0,0 +1 @@ +document.write('hello world!'); diff --git a/src/test-data/pug-parser/cases/attr-es2015.tokens.json b/src/test-data/pug-parser/cases/attr-es2015.tokens.json new file mode 100644 index 0000000..8616bc6 --- /dev/null +++ b/src/test-data/pug-parser/cases/attr-es2015.tokens.json @@ -0,0 +1,9 @@ +{"type":"code","loc":{"start":{"line":1,"column":1},"filename":"/cases/attr-es2015.pug","end":{"line":1,"column":50}},"val":"var avatar = '219b77f9d21de75e81851b6b886057c7'","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/attr-es2015.pug","end":{"line":3,"column":1}}} +{"type":"tag","loc":{"start":{"line":3,"column":1},"filename":"/cases/attr-es2015.pug","end":{"line":3,"column":4}},"val":"div"} +{"type":"class","loc":{"start":{"line":3,"column":4},"filename":"/cases/attr-es2015.pug","end":{"line":3,"column":15}},"val":"avatar-div"} +{"type":"start-attributes","loc":{"start":{"line":3,"column":15},"filename":"/cases/attr-es2015.pug","end":{"line":3,"column":16}}} +{"type":"attribute","loc":{"start":{"line":3,"column":16},"filename":"/cases/attr-es2015.pug","end":{"line":3,"column":88}},"name":"style","mustEscape":true,"val":"`background-image: url(https://www.gravatar.com/avatar/${avatar})`"} +{"type":"end-attributes","loc":{"start":{"line":3,"column":88},"filename":"/cases/attr-es2015.pug","end":{"line":3,"column":89}}} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/attr-es2015.pug","end":{"line":4,"column":1}}} +{"type":"eos","loc":{"start":{"line":4,"column":1},"filename":"/cases/attr-es2015.pug","end":{"line":4,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/attrs-data.tokens.json b/src/test-data/pug-parser/cases/attrs-data.tokens.json new file mode 100644 index 0000000..8f4ee02 --- /dev/null +++ b/src/test-data/pug-parser/cases/attrs-data.tokens.json @@ -0,0 +1,33 @@ +{"type":"code","loc":{"start":{"line":1,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":1,"column":30}},"val":"var user = { name: 'tobi' }","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":2,"column":1}}} +{"type":"tag","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":2,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":2,"column":4},"filename":"/cases/attrs-data.pug","end":{"line":2,"column":5}}} +{"type":"attribute","loc":{"start":{"line":2,"column":5},"filename":"/cases/attrs-data.pug","end":{"line":2,"column":19}},"name":"data-user","mustEscape":true,"val":"user"} +{"type":"end-attributes","loc":{"start":{"line":2,"column":19},"filename":"/cases/attrs-data.pug","end":{"line":2,"column":20}}} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":3,"column":1}}} +{"type":"tag","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":3,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":3,"column":4},"filename":"/cases/attrs-data.pug","end":{"line":3,"column":5}}} +{"type":"attribute","loc":{"start":{"line":3,"column":5},"filename":"/cases/attrs-data.pug","end":{"line":3,"column":23}},"name":"data-items","mustEscape":true,"val":"[1,2,3]"} +{"type":"end-attributes","loc":{"start":{"line":3,"column":23},"filename":"/cases/attrs-data.pug","end":{"line":3,"column":24}}} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":4,"column":1}}} +{"type":"tag","loc":{"start":{"line":4,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":4,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":4,"column":4},"filename":"/cases/attrs-data.pug","end":{"line":4,"column":5}}} +{"type":"attribute","loc":{"start":{"line":4,"column":5},"filename":"/cases/attrs-data.pug","end":{"line":4,"column":25}},"name":"data-username","mustEscape":true,"val":"'tobi'"} +{"type":"end-attributes","loc":{"start":{"line":4,"column":25},"filename":"/cases/attrs-data.pug","end":{"line":4,"column":26}}} +{"type":"newline","loc":{"start":{"line":5,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":5,"column":1}}} +{"type":"tag","loc":{"start":{"line":5,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":5,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":5,"column":4},"filename":"/cases/attrs-data.pug","end":{"line":5,"column":5}}} +{"type":"attribute","loc":{"start":{"line":5,"column":5},"filename":"/cases/attrs-data.pug","end":{"line":5,"column":42}},"name":"data-escaped","mustEscape":true,"val":"{message: \"Let's rock!\"}"} +{"type":"end-attributes","loc":{"start":{"line":5,"column":42},"filename":"/cases/attrs-data.pug","end":{"line":5,"column":43}}} +{"type":"newline","loc":{"start":{"line":6,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":6,"column":1}}} +{"type":"tag","loc":{"start":{"line":6,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":6,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":6,"column":4},"filename":"/cases/attrs-data.pug","end":{"line":6,"column":5}}} +{"type":"attribute","loc":{"start":{"line":6,"column":5},"filename":"/cases/attrs-data.pug","end":{"line":6,"column":60}},"name":"data-ampersand","mustEscape":true,"val":"{message: \"a quote: " this & that\"}"} +{"type":"end-attributes","loc":{"start":{"line":6,"column":60},"filename":"/cases/attrs-data.pug","end":{"line":6,"column":61}}} +{"type":"newline","loc":{"start":{"line":7,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":7,"column":1}}} +{"type":"tag","loc":{"start":{"line":7,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":7,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":7,"column":4},"filename":"/cases/attrs-data.pug","end":{"line":7,"column":5}}} +{"type":"attribute","loc":{"start":{"line":7,"column":5},"filename":"/cases/attrs-data.pug","end":{"line":7,"column":26}},"name":"data-epoc","mustEscape":true,"val":"new Date(0)"} +{"type":"end-attributes","loc":{"start":{"line":7,"column":26},"filename":"/cases/attrs-data.pug","end":{"line":7,"column":27}}} +{"type":"newline","loc":{"start":{"line":8,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":8,"column":1}}} +{"type":"eos","loc":{"start":{"line":8,"column":1},"filename":"/cases/attrs-data.pug","end":{"line":8,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/attrs.js.tokens.json b/src/test-data/pug-parser/cases/attrs.js.tokens.json new file mode 100644 index 0000000..7da64d9 --- /dev/null +++ b/src/test-data/pug-parser/cases/attrs.js.tokens.json @@ -0,0 +1,78 @@ +{"type":"code","loc":{"start":{"line":1,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":1,"column":13}},"val":"var id = 5","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":2,"column":1}}} +{"type":"code","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":2,"column":35}},"val":"function answer() { return 42; }","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":3,"column":1}}} +{"type":"tag","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":3,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":3,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":3,"column":3}}} +{"type":"attribute","loc":{"start":{"line":3,"column":3},"filename":"/cases/attrs.js.pug","end":{"line":3,"column":21}},"name":"href","mustEscape":true,"val":"'/user/' + id"} +{"type":"attribute","loc":{"start":{"line":3,"column":23},"filename":"/cases/attrs.js.pug","end":{"line":3,"column":37}},"name":"class","mustEscape":true,"val":"'button'"} +{"type":"end-attributes","loc":{"start":{"line":3,"column":37},"filename":"/cases/attrs.js.pug","end":{"line":3,"column":38}}} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":4,"column":1}}} +{"type":"tag","loc":{"start":{"line":4,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":4,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":4,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":4,"column":3}}} +{"type":"attribute","loc":{"start":{"line":4,"column":3},"filename":"/cases/attrs.js.pug","end":{"line":4,"column":25}},"name":"href","mustEscape":true,"val":"'/user/' + id"} +{"type":"attribute","loc":{"start":{"line":4,"column":27},"filename":"/cases/attrs.js.pug","end":{"line":4,"column":45}},"name":"class","mustEscape":true,"val":"'button'"} +{"type":"end-attributes","loc":{"start":{"line":4,"column":45},"filename":"/cases/attrs.js.pug","end":{"line":4,"column":46}}} +{"type":"newline","loc":{"start":{"line":5,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":5,"column":1}}} +{"type":"tag","loc":{"start":{"line":5,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":5,"column":5}},"val":"meta"} +{"type":"start-attributes","loc":{"start":{"line":5,"column":5},"filename":"/cases/attrs.js.pug","end":{"line":5,"column":6}}} +{"type":"attribute","loc":{"start":{"line":5,"column":6},"filename":"/cases/attrs.js.pug","end":{"line":5,"column":18}},"name":"key","mustEscape":true,"val":"'answer'"} +{"type":"attribute","loc":{"start":{"line":5,"column":20},"filename":"/cases/attrs.js.pug","end":{"line":5,"column":34}},"name":"value","mustEscape":true,"val":"answer()"} +{"type":"end-attributes","loc":{"start":{"line":5,"column":34},"filename":"/cases/attrs.js.pug","end":{"line":5,"column":35}}} +{"type":"newline","loc":{"start":{"line":6,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":6,"column":1}}} +{"type":"tag","loc":{"start":{"line":6,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":6,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":6,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":6,"column":3}}} +{"type":"attribute","loc":{"start":{"line":6,"column":3},"filename":"/cases/attrs.js.pug","end":{"line":6,"column":31}},"name":"class","mustEscape":true,"val":"['class1', 'class2']"} +{"type":"end-attributes","loc":{"start":{"line":6,"column":31},"filename":"/cases/attrs.js.pug","end":{"line":6,"column":32}}} +{"type":"newline","loc":{"start":{"line":7,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":7,"column":1}}} +{"type":"tag","loc":{"start":{"line":7,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":7,"column":2}},"val":"a"} +{"type":"class","loc":{"start":{"line":7,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":7,"column":12}},"val":"tag-class"} +{"type":"start-attributes","loc":{"start":{"line":7,"column":12},"filename":"/cases/attrs.js.pug","end":{"line":7,"column":13}}} +{"type":"attribute","loc":{"start":{"line":7,"column":13},"filename":"/cases/attrs.js.pug","end":{"line":7,"column":41}},"name":"class","mustEscape":true,"val":"['class1', 'class2']"} +{"type":"end-attributes","loc":{"start":{"line":7,"column":41},"filename":"/cases/attrs.js.pug","end":{"line":7,"column":42}}} +{"type":"newline","loc":{"start":{"line":9,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":9,"column":1}}} +{"type":"tag","loc":{"start":{"line":9,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":9,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":9,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":9,"column":3}}} +{"type":"attribute","loc":{"start":{"line":9,"column":3},"filename":"/cases/attrs.js.pug","end":{"line":9,"column":21}},"name":"href","mustEscape":true,"val":"'/user/' + id"} +{"type":"attribute","loc":{"start":{"line":9,"column":22},"filename":"/cases/attrs.js.pug","end":{"line":9,"column":36}},"name":"class","mustEscape":true,"val":"'button'"} +{"type":"end-attributes","loc":{"start":{"line":9,"column":36},"filename":"/cases/attrs.js.pug","end":{"line":9,"column":37}}} +{"type":"newline","loc":{"start":{"line":10,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":10,"column":1}}} +{"type":"tag","loc":{"start":{"line":10,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":10,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":10,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":10,"column":3}}} +{"type":"attribute","loc":{"start":{"line":10,"column":3},"filename":"/cases/attrs.js.pug","end":{"line":10,"column":25}},"name":"href","mustEscape":true,"val":"'/user/' + id"} +{"type":"attribute","loc":{"start":{"line":10,"column":26},"filename":"/cases/attrs.js.pug","end":{"line":10,"column":44}},"name":"class","mustEscape":true,"val":"'button'"} +{"type":"end-attributes","loc":{"start":{"line":10,"column":44},"filename":"/cases/attrs.js.pug","end":{"line":10,"column":45}}} +{"type":"newline","loc":{"start":{"line":11,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":11,"column":1}}} +{"type":"tag","loc":{"start":{"line":11,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":11,"column":5}},"val":"meta"} +{"type":"start-attributes","loc":{"start":{"line":11,"column":5},"filename":"/cases/attrs.js.pug","end":{"line":11,"column":6}}} +{"type":"attribute","loc":{"start":{"line":11,"column":6},"filename":"/cases/attrs.js.pug","end":{"line":11,"column":18}},"name":"key","mustEscape":true,"val":"'answer'"} +{"type":"attribute","loc":{"start":{"line":11,"column":19},"filename":"/cases/attrs.js.pug","end":{"line":11,"column":33}},"name":"value","mustEscape":true,"val":"answer()"} +{"type":"end-attributes","loc":{"start":{"line":11,"column":33},"filename":"/cases/attrs.js.pug","end":{"line":11,"column":34}}} +{"type":"newline","loc":{"start":{"line":12,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":12,"column":1}}} +{"type":"tag","loc":{"start":{"line":12,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":12,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":12,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":12,"column":3}}} +{"type":"attribute","loc":{"start":{"line":12,"column":3},"filename":"/cases/attrs.js.pug","end":{"line":12,"column":31}},"name":"class","mustEscape":true,"val":"['class1', 'class2']"} +{"type":"end-attributes","loc":{"start":{"line":12,"column":31},"filename":"/cases/attrs.js.pug","end":{"line":12,"column":32}}} +{"type":"newline","loc":{"start":{"line":13,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":13,"column":1}}} +{"type":"tag","loc":{"start":{"line":13,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":13,"column":2}},"val":"a"} +{"type":"class","loc":{"start":{"line":13,"column":2},"filename":"/cases/attrs.js.pug","end":{"line":13,"column":12}},"val":"tag-class"} +{"type":"start-attributes","loc":{"start":{"line":13,"column":12},"filename":"/cases/attrs.js.pug","end":{"line":13,"column":13}}} +{"type":"attribute","loc":{"start":{"line":13,"column":13},"filename":"/cases/attrs.js.pug","end":{"line":13,"column":41}},"name":"class","mustEscape":true,"val":"['class1', 'class2']"} +{"type":"end-attributes","loc":{"start":{"line":13,"column":41},"filename":"/cases/attrs.js.pug","end":{"line":13,"column":42}}} +{"type":"newline","loc":{"start":{"line":15,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":15,"column":1}}} +{"type":"tag","loc":{"start":{"line":15,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":15,"column":4}},"val":"div"} +{"type":"start-attributes","loc":{"start":{"line":15,"column":4},"filename":"/cases/attrs.js.pug","end":{"line":15,"column":5}}} +{"type":"attribute","loc":{"start":{"line":15,"column":5},"filename":"/cases/attrs.js.pug","end":{"line":15,"column":10}},"name":"id","mustEscape":true,"val":"id"} +{"type":"end-attributes","loc":{"start":{"line":15,"column":10},"filename":"/cases/attrs.js.pug","end":{"line":15,"column":11}}} +{"type":"&attributes","loc":{"start":{"line":15,"column":11},"filename":"/cases/attrs.js.pug","end":{"line":15,"column":36}},"val":"{foo: 'bar'}"} +{"type":"newline","loc":{"start":{"line":16,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":16,"column":1}}} +{"type":"code","loc":{"start":{"line":16,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":16,"column":17}},"val":"var bar = null","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":17,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":1}}} +{"type":"tag","loc":{"start":{"line":17,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":4}},"val":"div"} +{"type":"start-attributes","loc":{"start":{"line":17,"column":4},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":5}}} +{"type":"attribute","loc":{"start":{"line":17,"column":5},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":13}},"name":"foo","mustEscape":true,"val":"null"} +{"type":"attribute","loc":{"start":{"line":17,"column":14},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":21}},"name":"bar","mustEscape":true,"val":"bar"} +{"type":"end-attributes","loc":{"start":{"line":17,"column":21},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":22}}} +{"type":"&attributes","loc":{"start":{"line":17,"column":22},"filename":"/cases/attrs.js.pug","end":{"line":17,"column":47}},"val":"{baz: 'baz'}"} +{"type":"newline","loc":{"start":{"line":18,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":18,"column":1}}} +{"type":"eos","loc":{"start":{"line":18,"column":1},"filename":"/cases/attrs.js.pug","end":{"line":18,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/attrs.tokens.json b/src/test-data/pug-parser/cases/attrs.tokens.json new file mode 100644 index 0000000..d15ce77 --- /dev/null +++ b/src/test-data/pug-parser/cases/attrs.tokens.json @@ -0,0 +1,180 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/attrs.pug","end":{"line":1,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":1,"column":2},"filename":"/cases/attrs.pug","end":{"line":1,"column":3}}} +{"type":"attribute","loc":{"start":{"line":1,"column":3},"filename":"/cases/attrs.pug","end":{"line":1,"column":18}},"name":"href","mustEscape":true,"val":"'/contact'"} +{"type":"end-attributes","loc":{"start":{"line":1,"column":18},"filename":"/cases/attrs.pug","end":{"line":1,"column":19}}} +{"type":"text","loc":{"start":{"line":1,"column":20},"filename":"/cases/attrs.pug","end":{"line":1,"column":27}},"val":"contact"} +{"type":"newline","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs.pug","end":{"line":2,"column":1}}} +{"type":"tag","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs.pug","end":{"line":2,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":2,"column":2},"filename":"/cases/attrs.pug","end":{"line":2,"column":3}}} +{"type":"attribute","loc":{"start":{"line":2,"column":3},"filename":"/cases/attrs.pug","end":{"line":2,"column":15}},"name":"href","mustEscape":true,"val":"'/save'"} +{"type":"end-attributes","loc":{"start":{"line":2,"column":15},"filename":"/cases/attrs.pug","end":{"line":2,"column":16}}} +{"type":"class","loc":{"start":{"line":2,"column":16},"filename":"/cases/attrs.pug","end":{"line":2,"column":23}},"val":"button"} +{"type":"text","loc":{"start":{"line":2,"column":24},"filename":"/cases/attrs.pug","end":{"line":2,"column":28}},"val":"save"} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs.pug","end":{"line":3,"column":1}}} +{"type":"tag","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs.pug","end":{"line":3,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":3,"column":2},"filename":"/cases/attrs.pug","end":{"line":3,"column":3}}} +{"type":"attribute","loc":{"start":{"line":3,"column":3},"filename":"/cases/attrs.pug","end":{"line":3,"column":6}},"name":"foo","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":3,"column":8},"filename":"/cases/attrs.pug","end":{"line":3,"column":11}},"name":"bar","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":3,"column":13},"filename":"/cases/attrs.pug","end":{"line":3,"column":16}},"name":"baz","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":3,"column":16},"filename":"/cases/attrs.pug","end":{"line":3,"column":17}}} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/attrs.pug","end":{"line":4,"column":1}}} +{"type":"tag","loc":{"start":{"line":4,"column":1},"filename":"/cases/attrs.pug","end":{"line":4,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":4,"column":2},"filename":"/cases/attrs.pug","end":{"line":4,"column":3}}} +{"type":"attribute","loc":{"start":{"line":4,"column":3},"filename":"/cases/attrs.pug","end":{"line":4,"column":22}},"name":"foo","mustEscape":true,"val":"'foo, bar, baz'"} +{"type":"attribute","loc":{"start":{"line":4,"column":24},"filename":"/cases/attrs.pug","end":{"line":4,"column":29}},"name":"bar","mustEscape":true,"val":"1"} +{"type":"end-attributes","loc":{"start":{"line":4,"column":29},"filename":"/cases/attrs.pug","end":{"line":4,"column":30}}} +{"type":"newline","loc":{"start":{"line":5,"column":1},"filename":"/cases/attrs.pug","end":{"line":5,"column":1}}} +{"type":"tag","loc":{"start":{"line":5,"column":1},"filename":"/cases/attrs.pug","end":{"line":5,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":5,"column":2},"filename":"/cases/attrs.pug","end":{"line":5,"column":3}}} +{"type":"attribute","loc":{"start":{"line":5,"column":3},"filename":"/cases/attrs.pug","end":{"line":5,"column":16}},"name":"foo","mustEscape":true,"val":"'((foo))'"} +{"type":"attribute","loc":{"start":{"line":5,"column":18},"filename":"/cases/attrs.pug","end":{"line":5,"column":34}},"name":"bar","mustEscape":true,"val":"(1) ? 1 : 0"} +{"type":"end-attributes","loc":{"start":{"line":5,"column":35},"filename":"/cases/attrs.pug","end":{"line":5,"column":36}}} +{"type":"newline","loc":{"start":{"line":6,"column":1},"filename":"/cases/attrs.pug","end":{"line":6,"column":1}}} +{"type":"tag","loc":{"start":{"line":6,"column":1},"filename":"/cases/attrs.pug","end":{"line":6,"column":7}},"val":"select"} +{"type":"indent","loc":{"start":{"line":7,"column":1},"filename":"/cases/attrs.pug","end":{"line":7,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":7,"column":3},"filename":"/cases/attrs.pug","end":{"line":7,"column":9}},"val":"option"} +{"type":"start-attributes","loc":{"start":{"line":7,"column":9},"filename":"/cases/attrs.pug","end":{"line":7,"column":10}}} +{"type":"attribute","loc":{"start":{"line":7,"column":10},"filename":"/cases/attrs.pug","end":{"line":7,"column":21}},"name":"value","mustEscape":true,"val":"'foo'"} +{"type":"attribute","loc":{"start":{"line":7,"column":23},"filename":"/cases/attrs.pug","end":{"line":7,"column":31}},"name":"selected","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":7,"column":31},"filename":"/cases/attrs.pug","end":{"line":7,"column":32}}} +{"type":"text","loc":{"start":{"line":7,"column":33},"filename":"/cases/attrs.pug","end":{"line":7,"column":36}},"val":"Foo"} +{"type":"newline","loc":{"start":{"line":8,"column":1},"filename":"/cases/attrs.pug","end":{"line":8,"column":3}}} +{"type":"tag","loc":{"start":{"line":8,"column":3},"filename":"/cases/attrs.pug","end":{"line":8,"column":9}},"val":"option"} +{"type":"start-attributes","loc":{"start":{"line":8,"column":9},"filename":"/cases/attrs.pug","end":{"line":8,"column":10}}} +{"type":"attribute","loc":{"start":{"line":8,"column":10},"filename":"/cases/attrs.pug","end":{"line":8,"column":18}},"name":"selected","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":8,"column":20},"filename":"/cases/attrs.pug","end":{"line":8,"column":31}},"name":"value","mustEscape":true,"val":"'bar'"} +{"type":"end-attributes","loc":{"start":{"line":8,"column":31},"filename":"/cases/attrs.pug","end":{"line":8,"column":32}}} +{"type":"text","loc":{"start":{"line":8,"column":33},"filename":"/cases/attrs.pug","end":{"line":8,"column":36}},"val":"Bar"} +{"type":"outdent","loc":{"start":{"line":9,"column":1},"filename":"/cases/attrs.pug","end":{"line":9,"column":1}}} +{"type":"tag","loc":{"start":{"line":9,"column":1},"filename":"/cases/attrs.pug","end":{"line":9,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":9,"column":2},"filename":"/cases/attrs.pug","end":{"line":9,"column":3}}} +{"type":"attribute","loc":{"start":{"line":9,"column":3},"filename":"/cases/attrs.pug","end":{"line":9,"column":15}},"name":"foo","mustEscape":true,"val":"\"class:\""} +{"type":"end-attributes","loc":{"start":{"line":9,"column":15},"filename":"/cases/attrs.pug","end":{"line":9,"column":16}}} +{"type":"newline","loc":{"start":{"line":10,"column":1},"filename":"/cases/attrs.pug","end":{"line":10,"column":1}}} +{"type":"tag","loc":{"start":{"line":10,"column":1},"filename":"/cases/attrs.pug","end":{"line":10,"column":6}},"val":"input"} +{"type":"start-attributes","loc":{"start":{"line":10,"column":6},"filename":"/cases/attrs.pug","end":{"line":10,"column":7}}} +{"type":"attribute","loc":{"start":{"line":10,"column":7},"filename":"/cases/attrs.pug","end":{"line":10,"column":21}},"name":"pattern","mustEscape":true,"val":"'\\\\S+'"} +{"type":"end-attributes","loc":{"start":{"line":10,"column":21},"filename":"/cases/attrs.pug","end":{"line":10,"column":22}}} +{"type":"newline","loc":{"start":{"line":12,"column":1},"filename":"/cases/attrs.pug","end":{"line":12,"column":1}}} +{"type":"tag","loc":{"start":{"line":12,"column":1},"filename":"/cases/attrs.pug","end":{"line":12,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":12,"column":2},"filename":"/cases/attrs.pug","end":{"line":12,"column":3}}} +{"type":"attribute","loc":{"start":{"line":12,"column":3},"filename":"/cases/attrs.pug","end":{"line":12,"column":18}},"name":"href","mustEscape":true,"val":"'/contact'"} +{"type":"end-attributes","loc":{"start":{"line":12,"column":18},"filename":"/cases/attrs.pug","end":{"line":12,"column":19}}} +{"type":"text","loc":{"start":{"line":12,"column":20},"filename":"/cases/attrs.pug","end":{"line":12,"column":27}},"val":"contact"} +{"type":"newline","loc":{"start":{"line":13,"column":1},"filename":"/cases/attrs.pug","end":{"line":13,"column":1}}} +{"type":"tag","loc":{"start":{"line":13,"column":1},"filename":"/cases/attrs.pug","end":{"line":13,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":13,"column":2},"filename":"/cases/attrs.pug","end":{"line":13,"column":3}}} +{"type":"attribute","loc":{"start":{"line":13,"column":3},"filename":"/cases/attrs.pug","end":{"line":13,"column":15}},"name":"href","mustEscape":true,"val":"'/save'"} +{"type":"end-attributes","loc":{"start":{"line":13,"column":15},"filename":"/cases/attrs.pug","end":{"line":13,"column":16}}} +{"type":"class","loc":{"start":{"line":13,"column":16},"filename":"/cases/attrs.pug","end":{"line":13,"column":23}},"val":"button"} +{"type":"text","loc":{"start":{"line":13,"column":24},"filename":"/cases/attrs.pug","end":{"line":13,"column":28}},"val":"save"} +{"type":"newline","loc":{"start":{"line":14,"column":1},"filename":"/cases/attrs.pug","end":{"line":14,"column":1}}} +{"type":"tag","loc":{"start":{"line":14,"column":1},"filename":"/cases/attrs.pug","end":{"line":14,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":14,"column":2},"filename":"/cases/attrs.pug","end":{"line":14,"column":3}}} +{"type":"attribute","loc":{"start":{"line":14,"column":3},"filename":"/cases/attrs.pug","end":{"line":14,"column":6}},"name":"foo","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":14,"column":7},"filename":"/cases/attrs.pug","end":{"line":14,"column":10}},"name":"bar","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":14,"column":11},"filename":"/cases/attrs.pug","end":{"line":14,"column":14}},"name":"baz","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":14,"column":14},"filename":"/cases/attrs.pug","end":{"line":14,"column":15}}} +{"type":"newline","loc":{"start":{"line":15,"column":1},"filename":"/cases/attrs.pug","end":{"line":15,"column":1}}} +{"type":"tag","loc":{"start":{"line":15,"column":1},"filename":"/cases/attrs.pug","end":{"line":15,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":15,"column":2},"filename":"/cases/attrs.pug","end":{"line":15,"column":3}}} +{"type":"attribute","loc":{"start":{"line":15,"column":3},"filename":"/cases/attrs.pug","end":{"line":15,"column":22}},"name":"foo","mustEscape":true,"val":"'foo, bar, baz'"} +{"type":"attribute","loc":{"start":{"line":15,"column":23},"filename":"/cases/attrs.pug","end":{"line":15,"column":28}},"name":"bar","mustEscape":true,"val":"1"} +{"type":"end-attributes","loc":{"start":{"line":15,"column":28},"filename":"/cases/attrs.pug","end":{"line":15,"column":29}}} +{"type":"newline","loc":{"start":{"line":16,"column":1},"filename":"/cases/attrs.pug","end":{"line":16,"column":1}}} +{"type":"tag","loc":{"start":{"line":16,"column":1},"filename":"/cases/attrs.pug","end":{"line":16,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":16,"column":2},"filename":"/cases/attrs.pug","end":{"line":16,"column":3}}} +{"type":"attribute","loc":{"start":{"line":16,"column":3},"filename":"/cases/attrs.pug","end":{"line":16,"column":16}},"name":"foo","mustEscape":true,"val":"'((foo))'"} +{"type":"attribute","loc":{"start":{"line":16,"column":17},"filename":"/cases/attrs.pug","end":{"line":16,"column":33}},"name":"bar","mustEscape":true,"val":"(1) ? 1 : 0"} +{"type":"end-attributes","loc":{"start":{"line":16,"column":34},"filename":"/cases/attrs.pug","end":{"line":16,"column":35}}} +{"type":"newline","loc":{"start":{"line":17,"column":1},"filename":"/cases/attrs.pug","end":{"line":17,"column":1}}} +{"type":"tag","loc":{"start":{"line":17,"column":1},"filename":"/cases/attrs.pug","end":{"line":17,"column":7}},"val":"select"} +{"type":"indent","loc":{"start":{"line":18,"column":1},"filename":"/cases/attrs.pug","end":{"line":18,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":18,"column":3},"filename":"/cases/attrs.pug","end":{"line":18,"column":9}},"val":"option"} +{"type":"start-attributes","loc":{"start":{"line":18,"column":9},"filename":"/cases/attrs.pug","end":{"line":18,"column":10}}} +{"type":"attribute","loc":{"start":{"line":18,"column":10},"filename":"/cases/attrs.pug","end":{"line":18,"column":21}},"name":"value","mustEscape":true,"val":"'foo'"} +{"type":"attribute","loc":{"start":{"line":18,"column":22},"filename":"/cases/attrs.pug","end":{"line":18,"column":30}},"name":"selected","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":18,"column":30},"filename":"/cases/attrs.pug","end":{"line":18,"column":31}}} +{"type":"text","loc":{"start":{"line":18,"column":32},"filename":"/cases/attrs.pug","end":{"line":18,"column":35}},"val":"Foo"} +{"type":"newline","loc":{"start":{"line":19,"column":1},"filename":"/cases/attrs.pug","end":{"line":19,"column":3}}} +{"type":"tag","loc":{"start":{"line":19,"column":3},"filename":"/cases/attrs.pug","end":{"line":19,"column":9}},"val":"option"} +{"type":"start-attributes","loc":{"start":{"line":19,"column":9},"filename":"/cases/attrs.pug","end":{"line":19,"column":10}}} +{"type":"attribute","loc":{"start":{"line":19,"column":10},"filename":"/cases/attrs.pug","end":{"line":19,"column":18}},"name":"selected","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":19,"column":19},"filename":"/cases/attrs.pug","end":{"line":19,"column":30}},"name":"value","mustEscape":true,"val":"'bar'"} +{"type":"end-attributes","loc":{"start":{"line":19,"column":30},"filename":"/cases/attrs.pug","end":{"line":19,"column":31}}} +{"type":"text","loc":{"start":{"line":19,"column":32},"filename":"/cases/attrs.pug","end":{"line":19,"column":35}},"val":"Bar"} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/attrs.pug","end":{"line":20,"column":1}}} +{"type":"tag","loc":{"start":{"line":20,"column":1},"filename":"/cases/attrs.pug","end":{"line":20,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":20,"column":2},"filename":"/cases/attrs.pug","end":{"line":20,"column":3}}} +{"type":"attribute","loc":{"start":{"line":20,"column":3},"filename":"/cases/attrs.pug","end":{"line":20,"column":15}},"name":"foo","mustEscape":true,"val":"\"class:\""} +{"type":"end-attributes","loc":{"start":{"line":20,"column":15},"filename":"/cases/attrs.pug","end":{"line":20,"column":16}}} +{"type":"newline","loc":{"start":{"line":21,"column":1},"filename":"/cases/attrs.pug","end":{"line":21,"column":1}}} +{"type":"tag","loc":{"start":{"line":21,"column":1},"filename":"/cases/attrs.pug","end":{"line":21,"column":6}},"val":"input"} +{"type":"start-attributes","loc":{"start":{"line":21,"column":6},"filename":"/cases/attrs.pug","end":{"line":21,"column":7}}} +{"type":"attribute","loc":{"start":{"line":21,"column":7},"filename":"/cases/attrs.pug","end":{"line":21,"column":21}},"name":"pattern","mustEscape":true,"val":"'\\\\S+'"} +{"type":"end-attributes","loc":{"start":{"line":21,"column":21},"filename":"/cases/attrs.pug","end":{"line":21,"column":22}}} +{"type":"newline","loc":{"start":{"line":22,"column":1},"filename":"/cases/attrs.pug","end":{"line":22,"column":1}}} +{"type":"tag","loc":{"start":{"line":22,"column":1},"filename":"/cases/attrs.pug","end":{"line":22,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":22,"column":4},"filename":"/cases/attrs.pug","end":{"line":22,"column":5}}} +{"type":"attribute","loc":{"start":{"line":22,"column":5},"filename":"/cases/attrs.pug","end":{"line":22,"column":17}},"name":"terse","mustEscape":true,"val":"\"true\""} +{"type":"end-attributes","loc":{"start":{"line":22,"column":17},"filename":"/cases/attrs.pug","end":{"line":22,"column":18}}} +{"type":"newline","loc":{"start":{"line":23,"column":1},"filename":"/cases/attrs.pug","end":{"line":23,"column":1}}} +{"type":"tag","loc":{"start":{"line":23,"column":1},"filename":"/cases/attrs.pug","end":{"line":23,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":23,"column":4},"filename":"/cases/attrs.pug","end":{"line":23,"column":5}}} +{"type":"attribute","loc":{"start":{"line":23,"column":5},"filename":"/cases/attrs.pug","end":{"line":23,"column":21}},"name":"date","mustEscape":true,"val":"new Date(0)"} +{"type":"end-attributes","loc":{"start":{"line":23,"column":21},"filename":"/cases/attrs.pug","end":{"line":23,"column":22}}} +{"type":"newline","loc":{"start":{"line":25,"column":1},"filename":"/cases/attrs.pug","end":{"line":25,"column":1}}} +{"type":"tag","loc":{"start":{"line":25,"column":1},"filename":"/cases/attrs.pug","end":{"line":25,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":25,"column":4},"filename":"/cases/attrs.pug","end":{"line":25,"column":5}}} +{"type":"attribute","loc":{"start":{"line":25,"column":5},"filename":"/cases/attrs.pug","end":{"line":25,"column":8}},"name":"abc","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":26,"column":5},"filename":"/cases/attrs.pug","end":{"line":26,"column":8}},"name":"def","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":26,"column":8},"filename":"/cases/attrs.pug","end":{"line":26,"column":9}}} +{"type":"newline","loc":{"start":{"line":27,"column":1},"filename":"/cases/attrs.pug","end":{"line":27,"column":1}}} +{"type":"tag","loc":{"start":{"line":27,"column":1},"filename":"/cases/attrs.pug","end":{"line":27,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":27,"column":4},"filename":"/cases/attrs.pug","end":{"line":27,"column":5}}} +{"type":"attribute","loc":{"start":{"line":27,"column":5},"filename":"/cases/attrs.pug","end":{"line":27,"column":8}},"name":"abc","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":28,"column":5},"filename":"/cases/attrs.pug","end":{"line":28,"column":8}},"name":"def","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":28,"column":8},"filename":"/cases/attrs.pug","end":{"line":28,"column":9}}} +{"type":"newline","loc":{"start":{"line":29,"column":1},"filename":"/cases/attrs.pug","end":{"line":29,"column":1}}} +{"type":"tag","loc":{"start":{"line":29,"column":1},"filename":"/cases/attrs.pug","end":{"line":29,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":29,"column":4},"filename":"/cases/attrs.pug","end":{"line":29,"column":5}}} +{"type":"attribute","loc":{"start":{"line":29,"column":5},"filename":"/cases/attrs.pug","end":{"line":29,"column":8}},"name":"abc","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":30,"column":3},"filename":"/cases/attrs.pug","end":{"line":30,"column":6}},"name":"def","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":30,"column":6},"filename":"/cases/attrs.pug","end":{"line":30,"column":7}}} +{"type":"newline","loc":{"start":{"line":31,"column":1},"filename":"/cases/attrs.pug","end":{"line":31,"column":1}}} +{"type":"tag","loc":{"start":{"line":31,"column":1},"filename":"/cases/attrs.pug","end":{"line":31,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":31,"column":4},"filename":"/cases/attrs.pug","end":{"line":31,"column":5}}} +{"type":"attribute","loc":{"start":{"line":31,"column":5},"filename":"/cases/attrs.pug","end":{"line":31,"column":8}},"name":"abc","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":32,"column":4},"filename":"/cases/attrs.pug","end":{"line":32,"column":7}},"name":"def","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":32,"column":7},"filename":"/cases/attrs.pug","end":{"line":32,"column":8}}} +{"type":"newline","loc":{"start":{"line":33,"column":1},"filename":"/cases/attrs.pug","end":{"line":33,"column":1}}} +{"type":"tag","loc":{"start":{"line":33,"column":1},"filename":"/cases/attrs.pug","end":{"line":33,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":33,"column":4},"filename":"/cases/attrs.pug","end":{"line":33,"column":5}}} +{"type":"attribute","loc":{"start":{"line":33,"column":5},"filename":"/cases/attrs.pug","end":{"line":33,"column":8}},"name":"abc","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":34,"column":3},"filename":"/cases/attrs.pug","end":{"line":34,"column":6}},"name":"def","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":34,"column":6},"filename":"/cases/attrs.pug","end":{"line":34,"column":7}}} +{"type":"newline","loc":{"start":{"line":35,"column":1},"filename":"/cases/attrs.pug","end":{"line":35,"column":1}}} +{"type":"tag","loc":{"start":{"line":35,"column":1},"filename":"/cases/attrs.pug","end":{"line":35,"column":4}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":35,"column":4},"filename":"/cases/attrs.pug","end":{"line":35,"column":5}}} +{"type":"attribute","loc":{"start":{"line":35,"column":5},"filename":"/cases/attrs.pug","end":{"line":35,"column":8}},"name":"abc","mustEscape":true,"val":true} +{"type":"attribute","loc":{"start":{"line":36,"column":5},"filename":"/cases/attrs.pug","end":{"line":36,"column":8}},"name":"def","mustEscape":true,"val":true} +{"type":"end-attributes","loc":{"start":{"line":36,"column":8},"filename":"/cases/attrs.pug","end":{"line":36,"column":9}}} +{"type":"newline","loc":{"start":{"line":38,"column":1},"filename":"/cases/attrs.pug","end":{"line":38,"column":1}}} +{"type":"code","loc":{"start":{"line":38,"column":1},"filename":"/cases/attrs.pug","end":{"line":38,"column":41}},"val":"var attrs = {foo: 'bar', bar: ''}","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":40,"column":1},"filename":"/cases/attrs.pug","end":{"line":40,"column":1}}} +{"type":"tag","loc":{"start":{"line":40,"column":1},"filename":"/cases/attrs.pug","end":{"line":40,"column":4}},"val":"div"} +{"type":"&attributes","loc":{"start":{"line":40,"column":4},"filename":"/cases/attrs.pug","end":{"line":40,"column":22}},"val":"attrs"} +{"type":"newline","loc":{"start":{"line":42,"column":1},"filename":"/cases/attrs.pug","end":{"line":42,"column":1}}} +{"type":"tag","loc":{"start":{"line":42,"column":1},"filename":"/cases/attrs.pug","end":{"line":42,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":42,"column":2},"filename":"/cases/attrs.pug","end":{"line":42,"column":3}}} +{"type":"attribute","loc":{"start":{"line":42,"column":3},"filename":"/cases/attrs.pug","end":{"line":42,"column":12}},"name":"foo","mustEscape":true,"val":"'foo'"} +{"type":"attribute","loc":{"start":{"line":42,"column":13},"filename":"/cases/attrs.pug","end":{"line":42,"column":24}},"name":"bar","mustEscape":true,"val":"\"bar\""} +{"type":"end-attributes","loc":{"start":{"line":42,"column":24},"filename":"/cases/attrs.pug","end":{"line":42,"column":25}}} +{"type":"newline","loc":{"start":{"line":43,"column":1},"filename":"/cases/attrs.pug","end":{"line":43,"column":1}}} +{"type":"tag","loc":{"start":{"line":43,"column":1},"filename":"/cases/attrs.pug","end":{"line":43,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":43,"column":2},"filename":"/cases/attrs.pug","end":{"line":43,"column":3}}} +{"type":"attribute","loc":{"start":{"line":43,"column":3},"filename":"/cases/attrs.pug","end":{"line":43,"column":12}},"name":"foo","mustEscape":true,"val":"'foo'"} +{"type":"attribute","loc":{"start":{"line":43,"column":13},"filename":"/cases/attrs.pug","end":{"line":43,"column":24}},"name":"bar","mustEscape":true,"val":"'bar'"} +{"type":"end-attributes","loc":{"start":{"line":43,"column":24},"filename":"/cases/attrs.pug","end":{"line":43,"column":25}}} +{"type":"newline","loc":{"start":{"line":44,"column":1},"filename":"/cases/attrs.pug","end":{"line":44,"column":1}}} +{"type":"eos","loc":{"start":{"line":44,"column":1},"filename":"/cases/attrs.pug","end":{"line":44,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/attrs.unescaped.tokens.json b/src/test-data/pug-parser/cases/attrs.unescaped.tokens.json new file mode 100644 index 0000000..9cce521 --- /dev/null +++ b/src/test-data/pug-parser/cases/attrs.unescaped.tokens.json @@ -0,0 +1,15 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/attrs.unescaped.pug","end":{"line":1,"column":7}},"val":"script"} +{"type":"start-attributes","loc":{"start":{"line":1,"column":7},"filename":"/cases/attrs.unescaped.pug","end":{"line":1,"column":8}}} +{"type":"attribute","loc":{"start":{"line":1,"column":8},"filename":"/cases/attrs.unescaped.pug","end":{"line":1,"column":30}},"name":"type","mustEscape":true,"val":"'text/x-template'"} +{"type":"end-attributes","loc":{"start":{"line":1,"column":30},"filename":"/cases/attrs.unescaped.pug","end":{"line":1,"column":31}}} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/attrs.unescaped.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/attrs.unescaped.pug","end":{"line":2,"column":6}},"val":"div"} +{"type":"start-attributes","loc":{"start":{"line":2,"column":6},"filename":"/cases/attrs.unescaped.pug","end":{"line":2,"column":7}}} +{"type":"attribute","loc":{"start":{"line":2,"column":7},"filename":"/cases/attrs.unescaped.pug","end":{"line":2,"column":32}},"name":"id","mustEscape":false,"val":"'user-<%= user.id %>'"} +{"type":"end-attributes","loc":{"start":{"line":2,"column":32},"filename":"/cases/attrs.unescaped.pug","end":{"line":2,"column":33}}} +{"type":"indent","loc":{"start":{"line":3,"column":1},"filename":"/cases/attrs.unescaped.pug","end":{"line":3,"column":5}},"val":4} +{"type":"tag","loc":{"start":{"line":3,"column":5},"filename":"/cases/attrs.unescaped.pug","end":{"line":3,"column":7}},"val":"h1"} +{"type":"text","loc":{"start":{"line":3,"column":8},"filename":"/cases/attrs.unescaped.pug","end":{"line":3,"column":25}},"val":"<%= user.title %>"} +{"type":"outdent","loc":{"start":{"line":3,"column":25},"filename":"/cases/attrs.unescaped.pug","end":{"line":3,"column":25}}} +{"type":"outdent","loc":{"start":{"line":3,"column":25},"filename":"/cases/attrs.unescaped.pug","end":{"line":3,"column":25}}} +{"type":"eos","loc":{"start":{"line":3,"column":25},"filename":"/cases/attrs.unescaped.pug","end":{"line":3,"column":25}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/basic.tokens.json b/src/test-data/pug-parser/cases/basic.tokens.json new file mode 100644 index 0000000..0d38aae --- /dev/null +++ b/src/test-data/pug-parser/cases/basic.tokens.json @@ -0,0 +1,9 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/basic.pug","end":{"line":1,"column":5}},"val":"html"} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/basic.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/basic.pug","end":{"line":2,"column":7}},"val":"body"} +{"type":"indent","loc":{"start":{"line":3,"column":1},"filename":"/cases/basic.pug","end":{"line":3,"column":5}},"val":4} +{"type":"tag","loc":{"start":{"line":3,"column":5},"filename":"/cases/basic.pug","end":{"line":3,"column":7}},"val":"h1"} +{"type":"text","loc":{"start":{"line":3,"column":8},"filename":"/cases/basic.pug","end":{"line":3,"column":13}},"val":"Title"} +{"type":"outdent","loc":{"start":{"line":3,"column":13},"filename":"/cases/basic.pug","end":{"line":3,"column":13}}} +{"type":"outdent","loc":{"start":{"line":3,"column":13},"filename":"/cases/basic.pug","end":{"line":3,"column":13}}} +{"type":"eos","loc":{"start":{"line":3,"column":13},"filename":"/cases/basic.pug","end":{"line":3,"column":13}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/blanks.tokens.json b/src/test-data/pug-parser/cases/blanks.tokens.json new file mode 100644 index 0000000..1529f5c --- /dev/null +++ b/src/test-data/pug-parser/cases/blanks.tokens.json @@ -0,0 +1,13 @@ +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/blanks.pug","end":{"line":3,"column":1}}} +{"type":"tag","loc":{"start":{"line":3,"column":1},"filename":"/cases/blanks.pug","end":{"line":3,"column":3}},"val":"ul"} +{"type":"indent","loc":{"start":{"line":4,"column":1},"filename":"/cases/blanks.pug","end":{"line":4,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":4,"column":3},"filename":"/cases/blanks.pug","end":{"line":4,"column":5}},"val":"li"} +{"type":"text","loc":{"start":{"line":4,"column":6},"filename":"/cases/blanks.pug","end":{"line":4,"column":9}},"val":"foo"} +{"type":"newline","loc":{"start":{"line":6,"column":1},"filename":"/cases/blanks.pug","end":{"line":6,"column":3}}} +{"type":"tag","loc":{"start":{"line":6,"column":3},"filename":"/cases/blanks.pug","end":{"line":6,"column":5}},"val":"li"} +{"type":"text","loc":{"start":{"line":6,"column":6},"filename":"/cases/blanks.pug","end":{"line":6,"column":9}},"val":"bar"} +{"type":"newline","loc":{"start":{"line":8,"column":1},"filename":"/cases/blanks.pug","end":{"line":8,"column":3}}} +{"type":"tag","loc":{"start":{"line":8,"column":3},"filename":"/cases/blanks.pug","end":{"line":8,"column":5}},"val":"li"} +{"type":"text","loc":{"start":{"line":8,"column":6},"filename":"/cases/blanks.pug","end":{"line":8,"column":9}},"val":"baz"} +{"type":"outdent","loc":{"start":{"line":9,"column":1},"filename":"/cases/blanks.pug","end":{"line":9,"column":1}}} +{"type":"eos","loc":{"start":{"line":9,"column":1},"filename":"/cases/blanks.pug","end":{"line":9,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/block-code.tokens.json b/src/test-data/pug-parser/cases/block-code.tokens.json new file mode 100644 index 0000000..bff1694 --- /dev/null +++ b/src/test-data/pug-parser/cases/block-code.tokens.json @@ -0,0 +1,28 @@ +{"type":"blockcode","loc":{"start":{"line":1,"column":1},"filename":"/cases/block-code.pug","end":{"line":1,"column":2}}} +{"type":"start-pipeless-text","loc":{"start":{"line":1,"column":2},"filename":"/cases/block-code.pug","end":{"line":1,"column":2}}} +{"type":"text","loc":{"start":{"line":2,"column":3},"filename":"/cases/block-code.pug","end":{"line":2,"column":32}},"val":"list = [\"uno\", \"dos\", \"tres\","} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/block-code.pug","end":{"line":3,"column":3}}} +{"type":"text","loc":{"start":{"line":3,"column":3},"filename":"/cases/block-code.pug","end":{"line":3,"column":38}},"val":" \"cuatro\", \"cinco\", \"seis\"];"} +{"type":"end-pipeless-text","loc":{"start":{"line":3,"column":38},"filename":"/cases/block-code.pug","end":{"line":3,"column":38}}} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/block-code.pug","end":{"line":4,"column":1}}} +{"type":"comment","loc":{"start":{"line":4,"column":1},"filename":"/cases/block-code.pug","end":{"line":4,"column":70}},"val":" Without a block, the element is accepted and no code is generated","buffer":false} +{"type":"newline","loc":{"start":{"line":5,"column":1},"filename":"/cases/block-code.pug","end":{"line":5,"column":1}}} +{"type":"blockcode","loc":{"start":{"line":5,"column":1},"filename":"/cases/block-code.pug","end":{"line":5,"column":2}}} +{"type":"newline","loc":{"start":{"line":6,"column":1},"filename":"/cases/block-code.pug","end":{"line":6,"column":1}}} +{"type":"each","loc":{"start":{"line":6,"column":1},"filename":"/cases/block-code.pug","end":{"line":6,"column":18}},"val":"item","key":null,"code":"list"} +{"type":"indent","loc":{"start":{"line":7,"column":1},"filename":"/cases/block-code.pug","end":{"line":7,"column":3}},"val":2} +{"type":"blockcode","loc":{"start":{"line":7,"column":3},"filename":"/cases/block-code.pug","end":{"line":7,"column":4}}} +{"type":"start-pipeless-text","loc":{"start":{"line":7,"column":4},"filename":"/cases/block-code.pug","end":{"line":7,"column":4}}} +{"type":"text","loc":{"start":{"line":8,"column":5},"filename":"/cases/block-code.pug","end":{"line":8,"column":28}},"val":"string = item.charAt(0)"} +{"type":"newline","loc":{"start":{"line":9,"column":1},"filename":"/cases/block-code.pug","end":{"line":9,"column":5}}} +{"type":"text","loc":{"start":{"line":9,"column":5},"filename":"/cases/block-code.pug","end":{"line":9,"column":5}},"val":""} +{"type":"newline","loc":{"start":{"line":10,"column":1},"filename":"/cases/block-code.pug","end":{"line":10,"column":5}}} +{"type":"text","loc":{"start":{"line":10,"column":5},"filename":"/cases/block-code.pug","end":{"line":10,"column":23}},"val":" .toUpperCase() +"} +{"type":"newline","loc":{"start":{"line":11,"column":1},"filename":"/cases/block-code.pug","end":{"line":11,"column":5}}} +{"type":"text","loc":{"start":{"line":11,"column":5},"filename":"/cases/block-code.pug","end":{"line":11,"column":19}},"val":"item.slice(1);"} +{"type":"end-pipeless-text","loc":{"start":{"line":11,"column":19},"filename":"/cases/block-code.pug","end":{"line":11,"column":19}}} +{"type":"newline","loc":{"start":{"line":12,"column":1},"filename":"/cases/block-code.pug","end":{"line":12,"column":3}}} +{"type":"tag","loc":{"start":{"line":12,"column":3},"filename":"/cases/block-code.pug","end":{"line":12,"column":5}},"val":"li"} +{"type":"code","loc":{"start":{"line":12,"column":5},"filename":"/cases/block-code.pug","end":{"line":12,"column":13}},"val":"string","mustEscape":true,"buffer":true} +{"type":"outdent","loc":{"start":{"line":13,"column":1},"filename":"/cases/block-code.pug","end":{"line":13,"column":1}}} +{"type":"eos","loc":{"start":{"line":13,"column":1},"filename":"/cases/block-code.pug","end":{"line":13,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/block-expansion.shorthands.tokens.json b/src/test-data/pug-parser/cases/block-expansion.shorthands.tokens.json new file mode 100644 index 0000000..055869e --- /dev/null +++ b/src/test-data/pug-parser/cases/block-expansion.shorthands.tokens.json @@ -0,0 +1,11 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":1,"column":3}},"val":"ul"} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":5}},"val":"li"} +{"type":"class","loc":{"start":{"line":2,"column":5},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":15}},"val":"list-item"} +{"type":":","loc":{"start":{"line":2,"column":15},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":17}}} +{"type":"class","loc":{"start":{"line":2,"column":17},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":21}},"val":"foo"} +{"type":":","loc":{"start":{"line":2,"column":21},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":23}}} +{"type":"id","loc":{"start":{"line":2,"column":23},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":27}},"val":"bar"} +{"type":"text","loc":{"start":{"line":2,"column":28},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":31}},"val":"baz"} +{"type":"outdent","loc":{"start":{"line":2,"column":31},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":31}}} +{"type":"eos","loc":{"start":{"line":2,"column":31},"filename":"/cases/block-expansion.shorthands.pug","end":{"line":2,"column":31}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/block-expansion.tokens.json b/src/test-data/pug-parser/cases/block-expansion.tokens.json new file mode 100644 index 0000000..c49b659 --- /dev/null +++ b/src/test-data/pug-parser/cases/block-expansion.tokens.json @@ -0,0 +1,21 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/block-expansion.pug","end":{"line":1,"column":3}},"val":"ul"} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":5}},"val":"li"} +{"type":":","loc":{"start":{"line":2,"column":5},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":7}}} +{"type":"tag","loc":{"start":{"line":2,"column":7},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":8}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":2,"column":8},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":9}}} +{"type":"attribute","loc":{"start":{"line":2,"column":9},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":17}},"name":"href","mustEscape":true,"val":"'#'"} +{"type":"end-attributes","loc":{"start":{"line":2,"column":17},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":18}}} +{"type":"text","loc":{"start":{"line":2,"column":19},"filename":"/cases/block-expansion.pug","end":{"line":2,"column":22}},"val":"foo"} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":3}}} +{"type":"tag","loc":{"start":{"line":3,"column":3},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":5}},"val":"li"} +{"type":":","loc":{"start":{"line":3,"column":5},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":7}}} +{"type":"tag","loc":{"start":{"line":3,"column":7},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":8}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":3,"column":8},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":9}}} +{"type":"attribute","loc":{"start":{"line":3,"column":9},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":17}},"name":"href","mustEscape":true,"val":"'#'"} +{"type":"end-attributes","loc":{"start":{"line":3,"column":17},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":18}}} +{"type":"text","loc":{"start":{"line":3,"column":19},"filename":"/cases/block-expansion.pug","end":{"line":3,"column":22}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":5,"column":1},"filename":"/cases/block-expansion.pug","end":{"line":5,"column":1}}} +{"type":"tag","loc":{"start":{"line":5,"column":1},"filename":"/cases/block-expansion.pug","end":{"line":5,"column":2}},"val":"p"} +{"type":"text","loc":{"start":{"line":5,"column":3},"filename":"/cases/block-expansion.pug","end":{"line":5,"column":6}},"val":"baz"} +{"type":"eos","loc":{"start":{"line":5,"column":6},"filename":"/cases/block-expansion.pug","end":{"line":5,"column":6}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/blockquote.tokens.json b/src/test-data/pug-parser/cases/blockquote.tokens.json new file mode 100644 index 0000000..cb0b8f0 --- /dev/null +++ b/src/test-data/pug-parser/cases/blockquote.tokens.json @@ -0,0 +1,10 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/blockquote.pug","end":{"line":1,"column":7}},"val":"figure"} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/blockquote.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/blockquote.pug","end":{"line":2,"column":13}},"val":"blockquote"} +{"type":"indent","loc":{"start":{"line":3,"column":1},"filename":"/cases/blockquote.pug","end":{"line":3,"column":5}},"val":4} +{"type":"text","loc":{"start":{"line":3,"column":7},"filename":"/cases/blockquote.pug","end":{"line":3,"column":123}},"val":"Try to define yourself by what you do, and you’ll burnout every time. You are. That is enough. I rest in that."} +{"type":"outdent","loc":{"start":{"line":4,"column":1},"filename":"/cases/blockquote.pug","end":{"line":4,"column":3}}} +{"type":"tag","loc":{"start":{"line":4,"column":3},"filename":"/cases/blockquote.pug","end":{"line":4,"column":13}},"val":"figcaption"} +{"type":"text","loc":{"start":{"line":4,"column":14},"filename":"/cases/blockquote.pug","end":{"line":4,"column":47}},"val":"from @thefray at 1:43pm on May 10"} +{"type":"outdent","loc":{"start":{"line":4,"column":47},"filename":"/cases/blockquote.pug","end":{"line":4,"column":47}}} +{"type":"eos","loc":{"start":{"line":4,"column":47},"filename":"/cases/blockquote.pug","end":{"line":4,"column":47}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/blocks-in-blocks.tokens.json b/src/test-data/pug-parser/cases/blocks-in-blocks.tokens.json new file mode 100644 index 0000000..c3cb941 --- /dev/null +++ b/src/test-data/pug-parser/cases/blocks-in-blocks.tokens.json @@ -0,0 +1,9 @@ +{"type":"extends","loc":{"start":{"line":1,"column":1},"filename":"/cases/blocks-in-blocks.pug","end":{"line":1,"column":8}}} +{"type":"path","loc":{"start":{"line":1,"column":9},"filename":"/cases/blocks-in-blocks.pug","end":{"line":1,"column":48}},"val":"./auxiliary/blocks-in-blocks-layout.pug"} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/blocks-in-blocks.pug","end":{"line":3,"column":1}}} +{"type":"block","loc":{"start":{"line":3,"column":1},"filename":"/cases/blocks-in-blocks.pug","end":{"line":3,"column":11}},"val":"body","mode":"replace"} +{"type":"indent","loc":{"start":{"line":4,"column":1},"filename":"/cases/blocks-in-blocks.pug","end":{"line":4,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":4,"column":3},"filename":"/cases/blocks-in-blocks.pug","end":{"line":4,"column":5}},"val":"h1"} +{"type":"text","loc":{"start":{"line":4,"column":6},"filename":"/cases/blocks-in-blocks.pug","end":{"line":4,"column":12}},"val":"Page 2"} +{"type":"outdent","loc":{"start":{"line":5,"column":1},"filename":"/cases/blocks-in-blocks.pug","end":{"line":5,"column":1}}} +{"type":"eos","loc":{"start":{"line":5,"column":1},"filename":"/cases/blocks-in-blocks.pug","end":{"line":5,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/blocks-in-if.tokens.json b/src/test-data/pug-parser/cases/blocks-in-if.tokens.json new file mode 100644 index 0000000..3f30fc3 --- /dev/null +++ b/src/test-data/pug-parser/cases/blocks-in-if.tokens.json @@ -0,0 +1,44 @@ +{"type":"comment","loc":{"start":{"line":1,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":1,"column":49}},"val":" see https://github.com/pugjs/pug/issues/1589","buffer":false} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":3,"column":1}}} +{"type":"code","loc":{"start":{"line":3,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":3,"column":17}},"val":"var ajax = true","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":5,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":5,"column":1}}} +{"type":"code","loc":{"start":{"line":5,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":5,"column":12}},"val":"if( ajax )","mustEscape":false,"buffer":false} +{"type":"indent","loc":{"start":{"line":6,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":6,"column":5}},"val":4} +{"type":"comment","loc":{"start":{"line":6,"column":5},"filename":"/cases/blocks-in-if.pug","end":{"line":6,"column":46}},"val":" return only contents if ajax requests","buffer":false} +{"type":"newline","loc":{"start":{"line":7,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":7,"column":5}}} +{"type":"block","loc":{"start":{"line":7,"column":5},"filename":"/cases/blocks-in-if.pug","end":{"line":7,"column":19}},"val":"contents","mode":"replace"} +{"type":"indent","loc":{"start":{"line":8,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":8,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":8,"column":9},"filename":"/cases/blocks-in-if.pug","end":{"line":8,"column":10}},"val":"p"} +{"type":"text","loc":{"start":{"line":8,"column":11},"filename":"/cases/blocks-in-if.pug","end":{"line":8,"column":24}},"val":"ajax contents"} +{"type":"outdent","loc":{"start":{"line":10,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":10,"column":1}}} +{"type":"outdent","loc":{"start":{"line":10,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":10,"column":1}}} +{"type":"code","loc":{"start":{"line":10,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":10,"column":6}},"val":"else","mustEscape":false,"buffer":false} +{"type":"indent","loc":{"start":{"line":11,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":11,"column":5}},"val":4} +{"type":"comment","loc":{"start":{"line":11,"column":5},"filename":"/cases/blocks-in-if.pug","end":{"line":11,"column":24}},"val":" return all html","buffer":false} +{"type":"newline","loc":{"start":{"line":12,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":12,"column":5}}} +{"type":"doctype","loc":{"start":{"line":12,"column":5},"filename":"/cases/blocks-in-if.pug","end":{"line":12,"column":17}},"val":"html"} +{"type":"newline","loc":{"start":{"line":13,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":13,"column":5}}} +{"type":"tag","loc":{"start":{"line":13,"column":5},"filename":"/cases/blocks-in-if.pug","end":{"line":13,"column":9}},"val":"html"} +{"type":"indent","loc":{"start":{"line":14,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":14,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":14,"column":9},"filename":"/cases/blocks-in-if.pug","end":{"line":14,"column":13}},"val":"head"} +{"type":"indent","loc":{"start":{"line":15,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":15,"column":13}},"val":12} +{"type":"tag","loc":{"start":{"line":15,"column":13},"filename":"/cases/blocks-in-if.pug","end":{"line":15,"column":17}},"val":"meta"} +{"type":"start-attributes","loc":{"start":{"line":15,"column":17},"filename":"/cases/blocks-in-if.pug","end":{"line":15,"column":18}}} +{"type":"attribute","loc":{"start":{"line":15,"column":19},"filename":"/cases/blocks-in-if.pug","end":{"line":15,"column":33}},"name":"charset","mustEscape":true,"val":"'utf8'"} +{"type":"end-attributes","loc":{"start":{"line":15,"column":34},"filename":"/cases/blocks-in-if.pug","end":{"line":15,"column":35}}} +{"type":"newline","loc":{"start":{"line":16,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":16,"column":13}}} +{"type":"tag","loc":{"start":{"line":16,"column":13},"filename":"/cases/blocks-in-if.pug","end":{"line":16,"column":18}},"val":"title"} +{"type":"text","loc":{"start":{"line":16,"column":19},"filename":"/cases/blocks-in-if.pug","end":{"line":16,"column":25}},"val":"sample"} +{"type":"newline","loc":{"start":{"line":17,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":17,"column":13}}} +{"type":"tag","loc":{"start":{"line":17,"column":13},"filename":"/cases/blocks-in-if.pug","end":{"line":17,"column":17}},"val":"body"} +{"type":"indent","loc":{"start":{"line":18,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":18,"column":17}},"val":16} +{"type":"block","loc":{"start":{"line":18,"column":17},"filename":"/cases/blocks-in-if.pug","end":{"line":18,"column":31}},"val":"contents","mode":"replace"} +{"type":"indent","loc":{"start":{"line":19,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":19,"column":21}},"val":20} +{"type":"tag","loc":{"start":{"line":19,"column":21},"filename":"/cases/blocks-in-if.pug","end":{"line":19,"column":22}},"val":"p"} +{"type":"text","loc":{"start":{"line":19,"column":23},"filename":"/cases/blocks-in-if.pug","end":{"line":19,"column":35}},"val":"all contetns"} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":20,"column":1}}} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":20,"column":1}}} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":20,"column":1}}} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":20,"column":1}}} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":20,"column":1}}} +{"type":"eos","loc":{"start":{"line":20,"column":1},"filename":"/cases/blocks-in-if.pug","end":{"line":20,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/case-blocks.tokens.json b/src/test-data/pug-parser/cases/case-blocks.tokens.json new file mode 100644 index 0000000..165b477 --- /dev/null +++ b/src/test-data/pug-parser/cases/case-blocks.tokens.json @@ -0,0 +1,29 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":1,"column":5}},"val":"html"} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/case-blocks.pug","end":{"line":2,"column":7}},"val":"body"} +{"type":"indent","loc":{"start":{"line":3,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":3,"column":5}},"val":4} +{"type":"code","loc":{"start":{"line":3,"column":5},"filename":"/cases/case-blocks.pug","end":{"line":3,"column":22}},"val":"var friends = 1","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":4,"column":5}}} +{"type":"case","loc":{"start":{"line":4,"column":5},"filename":"/cases/case-blocks.pug","end":{"line":4,"column":17}},"val":"friends"} +{"type":"indent","loc":{"start":{"line":5,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":5,"column":7}},"val":6} +{"type":"when","loc":{"start":{"line":5,"column":7},"filename":"/cases/case-blocks.pug","end":{"line":5,"column":13}},"val":"0"} +{"type":"indent","loc":{"start":{"line":6,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":6,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":6,"column":9},"filename":"/cases/case-blocks.pug","end":{"line":6,"column":10}},"val":"p"} +{"type":"text","loc":{"start":{"line":6,"column":11},"filename":"/cases/case-blocks.pug","end":{"line":6,"column":30}},"val":"you have no friends"} +{"type":"outdent","loc":{"start":{"line":7,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":7,"column":7}}} +{"type":"when","loc":{"start":{"line":7,"column":7},"filename":"/cases/case-blocks.pug","end":{"line":7,"column":13}},"val":"1"} +{"type":"indent","loc":{"start":{"line":8,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":8,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":8,"column":9},"filename":"/cases/case-blocks.pug","end":{"line":8,"column":10}},"val":"p"} +{"type":"text","loc":{"start":{"line":8,"column":11},"filename":"/cases/case-blocks.pug","end":{"line":8,"column":28}},"val":"you have a friend"} +{"type":"outdent","loc":{"start":{"line":9,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":9,"column":7}}} +{"type":"default","loc":{"start":{"line":9,"column":7},"filename":"/cases/case-blocks.pug","end":{"line":9,"column":14}}} +{"type":"indent","loc":{"start":{"line":10,"column":1},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":10,"column":9},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":10}},"val":"p"} +{"type":"text","loc":{"start":{"line":10,"column":11},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":20}},"val":"you have "} +{"type":"interpolated-code","loc":{"start":{"line":10,"column":20},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":30}},"mustEscape":true,"buffer":true,"val":"friends"} +{"type":"text","loc":{"start":{"line":10,"column":30},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":38}},"val":" friends"} +{"type":"outdent","loc":{"start":{"line":10,"column":38},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":38}}} +{"type":"outdent","loc":{"start":{"line":10,"column":38},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":38}}} +{"type":"outdent","loc":{"start":{"line":10,"column":38},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":38}}} +{"type":"outdent","loc":{"start":{"line":10,"column":38},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":38}}} +{"type":"eos","loc":{"start":{"line":10,"column":38},"filename":"/cases/case-blocks.pug","end":{"line":10,"column":38}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/case.tokens.json b/src/test-data/pug-parser/cases/case.tokens.json new file mode 100644 index 0000000..da7847e --- /dev/null +++ b/src/test-data/pug-parser/cases/case.tokens.json @@ -0,0 +1,61 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/case.pug","end":{"line":1,"column":5}},"val":"html"} +{"type":"indent","loc":{"start":{"line":2,"column":1},"filename":"/cases/case.pug","end":{"line":2,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":2,"column":3},"filename":"/cases/case.pug","end":{"line":2,"column":7}},"val":"body"} +{"type":"indent","loc":{"start":{"line":3,"column":1},"filename":"/cases/case.pug","end":{"line":3,"column":5}},"val":4} +{"type":"code","loc":{"start":{"line":3,"column":5},"filename":"/cases/case.pug","end":{"line":3,"column":22}},"val":"var friends = 1","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":4,"column":1},"filename":"/cases/case.pug","end":{"line":4,"column":5}}} +{"type":"case","loc":{"start":{"line":4,"column":5},"filename":"/cases/case.pug","end":{"line":4,"column":17}},"val":"friends"} +{"type":"indent","loc":{"start":{"line":5,"column":1},"filename":"/cases/case.pug","end":{"line":5,"column":7}},"val":6} +{"type":"when","loc":{"start":{"line":5,"column":7},"filename":"/cases/case.pug","end":{"line":5,"column":13}},"val":"0"} +{"type":":","loc":{"start":{"line":5,"column":13},"filename":"/cases/case.pug","end":{"line":5,"column":15}}} +{"type":"tag","loc":{"start":{"line":5,"column":15},"filename":"/cases/case.pug","end":{"line":5,"column":16}},"val":"p"} +{"type":"text","loc":{"start":{"line":5,"column":17},"filename":"/cases/case.pug","end":{"line":5,"column":36}},"val":"you have no friends"} +{"type":"newline","loc":{"start":{"line":6,"column":1},"filename":"/cases/case.pug","end":{"line":6,"column":7}}} +{"type":"when","loc":{"start":{"line":6,"column":7},"filename":"/cases/case.pug","end":{"line":6,"column":13}},"val":"1"} +{"type":":","loc":{"start":{"line":6,"column":13},"filename":"/cases/case.pug","end":{"line":6,"column":15}}} +{"type":"tag","loc":{"start":{"line":6,"column":15},"filename":"/cases/case.pug","end":{"line":6,"column":16}},"val":"p"} +{"type":"text","loc":{"start":{"line":6,"column":17},"filename":"/cases/case.pug","end":{"line":6,"column":34}},"val":"you have a friend"} +{"type":"newline","loc":{"start":{"line":7,"column":1},"filename":"/cases/case.pug","end":{"line":7,"column":7}}} +{"type":"default","loc":{"start":{"line":7,"column":7},"filename":"/cases/case.pug","end":{"line":7,"column":14}}} +{"type":":","loc":{"start":{"line":7,"column":14},"filename":"/cases/case.pug","end":{"line":7,"column":16}}} +{"type":"tag","loc":{"start":{"line":7,"column":16},"filename":"/cases/case.pug","end":{"line":7,"column":17}},"val":"p"} +{"type":"text","loc":{"start":{"line":7,"column":18},"filename":"/cases/case.pug","end":{"line":7,"column":27}},"val":"you have "} +{"type":"interpolated-code","loc":{"start":{"line":7,"column":27},"filename":"/cases/case.pug","end":{"line":7,"column":37}},"mustEscape":true,"buffer":true,"val":"friends"} +{"type":"text","loc":{"start":{"line":7,"column":37},"filename":"/cases/case.pug","end":{"line":7,"column":45}},"val":" friends"} +{"type":"outdent","loc":{"start":{"line":8,"column":1},"filename":"/cases/case.pug","end":{"line":8,"column":5}}} +{"type":"code","loc":{"start":{"line":8,"column":5},"filename":"/cases/case.pug","end":{"line":8,"column":22}},"val":"var friends = 0","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":9,"column":1},"filename":"/cases/case.pug","end":{"line":9,"column":5}}} +{"type":"case","loc":{"start":{"line":9,"column":5},"filename":"/cases/case.pug","end":{"line":9,"column":17}},"val":"friends"} +{"type":"indent","loc":{"start":{"line":10,"column":1},"filename":"/cases/case.pug","end":{"line":10,"column":7}},"val":6} +{"type":"when","loc":{"start":{"line":10,"column":7},"filename":"/cases/case.pug","end":{"line":10,"column":13}},"val":"0"} +{"type":"newline","loc":{"start":{"line":11,"column":1},"filename":"/cases/case.pug","end":{"line":11,"column":7}}} +{"type":"when","loc":{"start":{"line":11,"column":7},"filename":"/cases/case.pug","end":{"line":11,"column":13}},"val":"1"} +{"type":"indent","loc":{"start":{"line":12,"column":1},"filename":"/cases/case.pug","end":{"line":12,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":12,"column":9},"filename":"/cases/case.pug","end":{"line":12,"column":10}},"val":"p"} +{"type":"text","loc":{"start":{"line":12,"column":11},"filename":"/cases/case.pug","end":{"line":12,"column":36}},"val":"you have very few friends"} +{"type":"outdent","loc":{"start":{"line":13,"column":1},"filename":"/cases/case.pug","end":{"line":13,"column":7}}} +{"type":"default","loc":{"start":{"line":13,"column":7},"filename":"/cases/case.pug","end":{"line":13,"column":14}}} +{"type":"indent","loc":{"start":{"line":14,"column":1},"filename":"/cases/case.pug","end":{"line":14,"column":9}},"val":8} +{"type":"tag","loc":{"start":{"line":14,"column":9},"filename":"/cases/case.pug","end":{"line":14,"column":10}},"val":"p"} +{"type":"text","loc":{"start":{"line":14,"column":11},"filename":"/cases/case.pug","end":{"line":14,"column":20}},"val":"you have "} +{"type":"interpolated-code","loc":{"start":{"line":14,"column":20},"filename":"/cases/case.pug","end":{"line":14,"column":30}},"mustEscape":true,"buffer":true,"val":"friends"} +{"type":"text","loc":{"start":{"line":14,"column":30},"filename":"/cases/case.pug","end":{"line":14,"column":38}},"val":" friends"} +{"type":"outdent","loc":{"start":{"line":16,"column":1},"filename":"/cases/case.pug","end":{"line":16,"column":5}}} +{"type":"outdent","loc":{"start":{"line":16,"column":1},"filename":"/cases/case.pug","end":{"line":16,"column":5}}} +{"type":"code","loc":{"start":{"line":16,"column":5},"filename":"/cases/case.pug","end":{"line":16,"column":27}},"val":"var friend = 'Tim:G'","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":17,"column":1},"filename":"/cases/case.pug","end":{"line":17,"column":5}}} +{"type":"case","loc":{"start":{"line":17,"column":5},"filename":"/cases/case.pug","end":{"line":17,"column":16}},"val":"friend"} +{"type":"indent","loc":{"start":{"line":18,"column":1},"filename":"/cases/case.pug","end":{"line":18,"column":7}},"val":6} +{"type":"when","loc":{"start":{"line":18,"column":7},"filename":"/cases/case.pug","end":{"line":18,"column":19}},"val":"'Tim:G'"} +{"type":":","loc":{"start":{"line":18,"column":19},"filename":"/cases/case.pug","end":{"line":18,"column":24}}} +{"type":"tag","loc":{"start":{"line":18,"column":24},"filename":"/cases/case.pug","end":{"line":18,"column":25}},"val":"p"} +{"type":"text","loc":{"start":{"line":18,"column":26},"filename":"/cases/case.pug","end":{"line":18,"column":44}},"val":"Friend is a string"} +{"type":"newline","loc":{"start":{"line":19,"column":1},"filename":"/cases/case.pug","end":{"line":19,"column":7}}} +{"type":"when","loc":{"start":{"line":19,"column":7},"filename":"/cases/case.pug","end":{"line":19,"column":22}},"val":"{tim: 'g'}"} +{"type":":","loc":{"start":{"line":19,"column":22},"filename":"/cases/case.pug","end":{"line":19,"column":24}}} +{"type":"tag","loc":{"start":{"line":19,"column":24},"filename":"/cases/case.pug","end":{"line":19,"column":25}},"val":"p"} +{"type":"text","loc":{"start":{"line":19,"column":26},"filename":"/cases/case.pug","end":{"line":19,"column":45}},"val":"Friend is an object"} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/case.pug","end":{"line":20,"column":1}}} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/case.pug","end":{"line":20,"column":1}}} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/case.pug","end":{"line":20,"column":1}}} +{"type":"eos","loc":{"start":{"line":20,"column":1},"filename":"/cases/case.pug","end":{"line":20,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/classes-empty.tokens.json b/src/test-data/pug-parser/cases/classes-empty.tokens.json new file mode 100644 index 0000000..1e9fe57 --- /dev/null +++ b/src/test-data/pug-parser/cases/classes-empty.tokens.json @@ -0,0 +1,15 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/classes-empty.pug","end":{"line":1,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":1,"column":2},"filename":"/cases/classes-empty.pug","end":{"line":1,"column":3}}} +{"type":"attribute","loc":{"start":{"line":1,"column":3},"filename":"/cases/classes-empty.pug","end":{"line":1,"column":11}},"name":"class","mustEscape":true,"val":"''"} +{"type":"end-attributes","loc":{"start":{"line":1,"column":11},"filename":"/cases/classes-empty.pug","end":{"line":1,"column":12}}} +{"type":"newline","loc":{"start":{"line":2,"column":1},"filename":"/cases/classes-empty.pug","end":{"line":2,"column":1}}} +{"type":"tag","loc":{"start":{"line":2,"column":1},"filename":"/cases/classes-empty.pug","end":{"line":2,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":2,"column":2},"filename":"/cases/classes-empty.pug","end":{"line":2,"column":3}}} +{"type":"attribute","loc":{"start":{"line":2,"column":3},"filename":"/cases/classes-empty.pug","end":{"line":2,"column":13}},"name":"class","mustEscape":true,"val":"null"} +{"type":"end-attributes","loc":{"start":{"line":2,"column":13},"filename":"/cases/classes-empty.pug","end":{"line":2,"column":14}}} +{"type":"newline","loc":{"start":{"line":3,"column":1},"filename":"/cases/classes-empty.pug","end":{"line":3,"column":1}}} +{"type":"tag","loc":{"start":{"line":3,"column":1},"filename":"/cases/classes-empty.pug","end":{"line":3,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":3,"column":2},"filename":"/cases/classes-empty.pug","end":{"line":3,"column":3}}} +{"type":"attribute","loc":{"start":{"line":3,"column":3},"filename":"/cases/classes-empty.pug","end":{"line":3,"column":18}},"name":"class","mustEscape":true,"val":"undefined"} +{"type":"end-attributes","loc":{"start":{"line":3,"column":18},"filename":"/cases/classes-empty.pug","end":{"line":3,"column":19}}} +{"type":"eos","loc":{"start":{"line":3,"column":19},"filename":"/cases/classes-empty.pug","end":{"line":3,"column":19}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/classes.tokens.json b/src/test-data/pug-parser/cases/classes.tokens.json new file mode 100644 index 0000000..e5fc7ee --- /dev/null +++ b/src/test-data/pug-parser/cases/classes.tokens.json @@ -0,0 +1,27 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/classes.pug","end":{"line":1,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":1,"column":2},"filename":"/cases/classes.pug","end":{"line":1,"column":3}}} +{"type":"attribute","loc":{"start":{"line":1,"column":3},"filename":"/cases/classes.pug","end":{"line":1,"column":30}},"name":"class","mustEscape":true,"val":"['foo', 'bar', 'baz']"} +{"type":"end-attributes","loc":{"start":{"line":1,"column":30},"filename":"/cases/classes.pug","end":{"line":1,"column":31}}} +{"type":"newline","loc":{"start":{"line":5,"column":1},"filename":"/cases/classes.pug","end":{"line":5,"column":1}}} +{"type":"tag","loc":{"start":{"line":5,"column":1},"filename":"/cases/classes.pug","end":{"line":5,"column":2}},"val":"a"} +{"type":"class","loc":{"start":{"line":5,"column":2},"filename":"/cases/classes.pug","end":{"line":5,"column":6}},"val":"foo"} +{"type":"start-attributes","loc":{"start":{"line":5,"column":6},"filename":"/cases/classes.pug","end":{"line":5,"column":7}}} +{"type":"attribute","loc":{"start":{"line":5,"column":7},"filename":"/cases/classes.pug","end":{"line":5,"column":18}},"name":"class","mustEscape":true,"val":"'bar'"} +{"type":"end-attributes","loc":{"start":{"line":5,"column":18},"filename":"/cases/classes.pug","end":{"line":5,"column":19}}} +{"type":"class","loc":{"start":{"line":5,"column":19},"filename":"/cases/classes.pug","end":{"line":5,"column":23}},"val":"baz"} +{"type":"newline","loc":{"start":{"line":9,"column":1},"filename":"/cases/classes.pug","end":{"line":9,"column":1}}} +{"type":"tag","loc":{"start":{"line":9,"column":1},"filename":"/cases/classes.pug","end":{"line":9,"column":2}},"val":"a"} +{"type":"class","loc":{"start":{"line":9,"column":2},"filename":"/cases/classes.pug","end":{"line":9,"column":14}},"val":"foo-bar_baz"} +{"type":"newline","loc":{"start":{"line":11,"column":1},"filename":"/cases/classes.pug","end":{"line":11,"column":1}}} +{"type":"tag","loc":{"start":{"line":11,"column":1},"filename":"/cases/classes.pug","end":{"line":11,"column":2}},"val":"a"} +{"type":"start-attributes","loc":{"start":{"line":11,"column":2},"filename":"/cases/classes.pug","end":{"line":11,"column":3}}} +{"type":"attribute","loc":{"start":{"line":11,"column":3},"filename":"/cases/classes.pug","end":{"line":11,"column":43}},"name":"class","mustEscape":true,"val":"{foo: true, bar: false, baz: true}"} +{"type":"end-attributes","loc":{"start":{"line":11,"column":43},"filename":"/cases/classes.pug","end":{"line":11,"column":44}}} +{"type":"newline","loc":{"start":{"line":13,"column":1},"filename":"/cases/classes.pug","end":{"line":13,"column":1}}} +{"type":"tag","loc":{"start":{"line":13,"column":1},"filename":"/cases/classes.pug","end":{"line":13,"column":2}},"val":"a"} +{"type":"class","loc":{"start":{"line":13,"column":2},"filename":"/cases/classes.pug","end":{"line":13,"column":7}},"val":"-foo"} +{"type":"newline","loc":{"start":{"line":14,"column":1},"filename":"/cases/classes.pug","end":{"line":14,"column":1}}} +{"type":"tag","loc":{"start":{"line":14,"column":1},"filename":"/cases/classes.pug","end":{"line":14,"column":2}},"val":"a"} +{"type":"class","loc":{"start":{"line":14,"column":2},"filename":"/cases/classes.pug","end":{"line":14,"column":7}},"val":"3foo"} +{"type":"newline","loc":{"start":{"line":15,"column":1},"filename":"/cases/classes.pug","end":{"line":15,"column":1}}} +{"type":"eos","loc":{"start":{"line":15,"column":1},"filename":"/cases/classes.pug","end":{"line":15,"column":1}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/code.conditionals.tokens.json b/src/test-data/pug-parser/cases/code.conditionals.tokens.json new file mode 100644 index 0000000..9f9d21b --- /dev/null +++ b/src/test-data/pug-parser/cases/code.conditionals.tokens.json @@ -0,0 +1,86 @@ +{"type":"newline","loc":{"start":{"line":2,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":2,"column":1}}} +{"type":"code","loc":{"start":{"line":2,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":2,"column":12}},"val":"if (true)","mustEscape":false,"buffer":false} +{"type":"indent","loc":{"start":{"line":3,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":3,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":3,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":3,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":3,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":3,"column":8}},"val":"foo"} +{"type":"outdent","loc":{"start":{"line":4,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":4,"column":1}}} +{"type":"code","loc":{"start":{"line":4,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":4,"column":7}},"val":"else","mustEscape":false,"buffer":false} +{"type":"indent","loc":{"start":{"line":5,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":5,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":5,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":5,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":5,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":5,"column":8}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":7,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":7,"column":1}}} +{"type":"code","loc":{"start":{"line":7,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":7,"column":14}},"val":"if (true) {","mustEscape":false,"buffer":false} +{"type":"indent","loc":{"start":{"line":8,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":8,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":8,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":8,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":8,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":8,"column":8}},"val":"foo"} +{"type":"outdent","loc":{"start":{"line":9,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":9,"column":1}}} +{"type":"code","loc":{"start":{"line":9,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":9,"column":11}},"val":"} else {","mustEscape":false,"buffer":false} +{"type":"indent","loc":{"start":{"line":10,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":10,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":10,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":10,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":10,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":10,"column":8}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":11,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":11,"column":1}}} +{"type":"code","loc":{"start":{"line":11,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":11,"column":4}},"val":"}","mustEscape":false,"buffer":false} +{"type":"newline","loc":{"start":{"line":13,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":13,"column":1}}} +{"type":"if","loc":{"start":{"line":13,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":13,"column":8}},"val":"true"} +{"type":"indent","loc":{"start":{"line":14,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":14,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":14,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":14,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":14,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":14,"column":8}},"val":"foo"} +{"type":"newline","loc":{"start":{"line":15,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":15,"column":3}}} +{"type":"tag","loc":{"start":{"line":15,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":15,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":15,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":15,"column":8}},"val":"bar"} +{"type":"newline","loc":{"start":{"line":16,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":16,"column":3}}} +{"type":"tag","loc":{"start":{"line":16,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":16,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":16,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":16,"column":8}},"val":"baz"} +{"type":"outdent","loc":{"start":{"line":17,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":17,"column":1}}} +{"type":"else","loc":{"start":{"line":17,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":17,"column":5}},"val":""} +{"type":"indent","loc":{"start":{"line":18,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":18,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":18,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":18,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":18,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":18,"column":8}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":20,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":20,"column":1}}} +{"type":"if","loc":{"start":{"line":20,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":20,"column":12}},"val":"!(true)"} +{"type":"indent","loc":{"start":{"line":21,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":21,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":21,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":21,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":21,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":21,"column":8}},"val":"foo"} +{"type":"outdent","loc":{"start":{"line":22,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":22,"column":1}}} +{"type":"else","loc":{"start":{"line":22,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":22,"column":5}},"val":""} +{"type":"indent","loc":{"start":{"line":23,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":23,"column":3}},"val":2} +{"type":"tag","loc":{"start":{"line":23,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":23,"column":4}},"val":"p"} +{"type":"text","loc":{"start":{"line":23,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":23,"column":8}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":25,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":25,"column":1}}} +{"type":"if","loc":{"start":{"line":25,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":25,"column":12}},"val":"'nested'"} +{"type":"indent","loc":{"start":{"line":26,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":26,"column":3}},"val":2} +{"type":"if","loc":{"start":{"line":26,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":26,"column":13}},"val":"'works'"} +{"type":"indent","loc":{"start":{"line":27,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":27,"column":5}},"val":4} +{"type":"tag","loc":{"start":{"line":27,"column":5},"filename":"/cases/code.conditionals.pug","end":{"line":27,"column":6}},"val":"p"} +{"type":"text","loc":{"start":{"line":27,"column":7},"filename":"/cases/code.conditionals.pug","end":{"line":27,"column":10}},"val":"yay"} +{"type":"outdent","loc":{"start":{"line":29,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":29,"column":1}}} +{"type":"outdent","loc":{"start":{"line":29,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":29,"column":1}}} +{"type":"comment","loc":{"start":{"line":29,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":29,"column":23}},"val":" allow empty blocks","buffer":false} +{"type":"newline","loc":{"start":{"line":30,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":30,"column":1}}} +{"type":"if","loc":{"start":{"line":30,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":30,"column":9}},"val":"false"} +{"type":"newline","loc":{"start":{"line":31,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":31,"column":1}}} +{"type":"else","loc":{"start":{"line":31,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":31,"column":5}},"val":""} +{"type":"indent","loc":{"start":{"line":32,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":32,"column":3}},"val":2} +{"type":"class","loc":{"start":{"line":32,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":32,"column":7}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":33,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":33,"column":1}}} +{"type":"if","loc":{"start":{"line":33,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":33,"column":8}},"val":"true"} +{"type":"indent","loc":{"start":{"line":34,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":34,"column":3}},"val":2} +{"type":"class","loc":{"start":{"line":34,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":34,"column":7}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":35,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":35,"column":1}}} +{"type":"else","loc":{"start":{"line":35,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":35,"column":5}},"val":""} +{"type":"newline","loc":{"start":{"line":36,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":36,"column":1}}} +{"type":"class","loc":{"start":{"line":36,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":36,"column":6}},"val":"bing"} +{"type":"newline","loc":{"start":{"line":38,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":38,"column":1}}} +{"type":"if","loc":{"start":{"line":38,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":38,"column":9}},"val":"false"} +{"type":"indent","loc":{"start":{"line":39,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":39,"column":3}},"val":2} +{"type":"class","loc":{"start":{"line":39,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":39,"column":8}},"val":"bing"} +{"type":"outdent","loc":{"start":{"line":40,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":40,"column":1}}} +{"type":"else-if","loc":{"start":{"line":40,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":40,"column":14}},"val":"false"} +{"type":"indent","loc":{"start":{"line":41,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":41,"column":3}},"val":2} +{"type":"class","loc":{"start":{"line":41,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":41,"column":7}},"val":"bar"} +{"type":"outdent","loc":{"start":{"line":42,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":42,"column":1}}} +{"type":"else","loc":{"start":{"line":42,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":42,"column":5}},"val":""} +{"type":"indent","loc":{"start":{"line":43,"column":1},"filename":"/cases/code.conditionals.pug","end":{"line":43,"column":3}},"val":2} +{"type":"class","loc":{"start":{"line":43,"column":3},"filename":"/cases/code.conditionals.pug","end":{"line":43,"column":7}},"val":"foo"} +{"type":"outdent","loc":{"start":{"line":43,"column":7},"filename":"/cases/code.conditionals.pug","end":{"line":43,"column":7}}} +{"type":"eos","loc":{"start":{"line":43,"column":7},"filename":"/cases/code.conditionals.pug","end":{"line":43,"column":7}}} \ No newline at end of file diff --git a/src/test-data/pug-parser/cases/code.escape.tokens.json b/src/test-data/pug-parser/cases/code.escape.tokens.json new file mode 100644 index 0000000..45fee43 --- /dev/null +++ b/src/test-data/pug-parser/cases/code.escape.tokens.json @@ -0,0 +1,6 @@ +{"type":"tag","loc":{"start":{"line":1,"column":1},"filename":"/cases/code.escape.pug","end":{"line":1,"column":2}},"val":"p"} +{"type":"code","loc":{"start":{"line":1,"column":2},"filename":"/cases/code.escape.pug","end":{"line":1,"column":14}},"val":"'' +doctype html +html + + head + title= "Some " + "JavaScript" + != js + + + + body \ No newline at end of file diff --git a/src/test-data/pug/test/README.md b/src/test-data/pug/test/README.md new file mode 100644 index 0000000..0989992 --- /dev/null +++ b/src/test-data/pug/test/README.md @@ -0,0 +1,15 @@ +# Running Tests + +To run tests (with node.js installed) you must complete 2 steps. + +## 1 Install dependencies + +``` +npm install +``` + +## 2 Run tests + +``` +npm test +``` diff --git a/src/test-data/pug/test/__snapshots__/pug.test.js.snap b/src/test-data/pug/test/__snapshots__/pug.test.js.snap new file mode 100644 index 0000000..8e3600c --- /dev/null +++ b/src/test-data/pug/test/__snapshots__/pug.test.js.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pug .compileClient() should support module syntax in pug.compileClient(str, options) when inlineRuntimeFunctions it false 1`] = ` +"var pug = require(\\"pug-runtime\\"); +function template(locals) { + var pug_html = \\"\\", + pug_mixins = {}, + pug_interp; + var pug_debug_filename, pug_debug_line; + try { + var self = locals || {}; + pug_debug_line = 1; + pug_html = pug_html + '\\\\u003Cdiv class=\\"bar\\"\\\\u003E'; + pug_debug_line = 1; + pug_html = + pug_html + + pug.escape(null == (pug_interp = self.foo) ? \\"\\" : pug_interp) + + \\"\\\\u003C\\\\u002Fdiv\\\\u003E\\"; + } catch (err) { + pug.rethrow(err, pug_debug_filename, pug_debug_line); + } + return pug_html; +} +module.exports = template; +" +`; + +exports[`pug .compileClient() should support module syntax in pug.compileClient(str, options) when inlineRuntimeFunctions it true 1`] = ` +"function pug_escape(e) { + var a = \\"\\" + e, + t = pug_match_html.exec(a); + if (!t) return e; + var r, + c, + n, + s = \\"\\"; + for (r = t.index, c = 0; r < a.length; r++) { + switch (a.charCodeAt(r)) { + case 34: + n = \\""\\"; + break; + case 38: + n = \\"&\\"; + break; + case 60: + n = \\"<\\"; + break; + case 62: + n = \\">\\"; + break; + default: + continue; + } + c !== r && (s += a.substring(c, r)), (c = r + 1), (s += n); + } + return c !== r ? s + a.substring(c, r) : s; +} +var pug_match_html = /[\\"&<>]/; +function pug_rethrow(e, n, r, t) { + if (!(e instanceof Error)) throw e; + if (!((\\"undefined\\" == typeof window && n) || t)) + throw ((e.message += \\" on line \\" + r), e); + var o, a, i, s; + try { + (t = t || require(\\"fs\\").readFileSync(n, { encoding: \\"utf8\\" })), + (o = 3), + (a = t.split(\\"\\\\n\\")), + (i = Math.max(r - o, 0)), + (s = Math.min(a.length, r + o)); + } catch (t) { + return ( + (e.message += \\" - could not read from \\" + n + \\" (\\" + t.message + \\")\\"), + void pug_rethrow(e, null, r) + ); + } + (o = a + .slice(i, s) + .map(function(e, n) { + var t = n + i + 1; + return (t == r ? \\" > \\" : \\" \\") + t + \\"| \\" + e; + }) + .join(\\"\\\\n\\")), + (e.path = n); + try { + e.message = (n || \\"Pug\\") + \\":\\" + r + \\"\\\\n\\" + o + \\"\\\\n\\\\n\\" + e.message; + } catch (e) {} + throw e; +} +function template(locals) { + var pug_html = \\"\\", + pug_mixins = {}, + pug_interp; + var pug_debug_filename, pug_debug_line; + try { + var self = locals || {}; + pug_debug_line = 1; + pug_html = pug_html + '\\\\u003Cdiv class=\\"bar\\"\\\\u003E'; + pug_debug_line = 1; + pug_html = + pug_html + + pug_escape(null == (pug_interp = self.foo) ? \\"\\" : pug_interp) + + \\"\\\\u003C\\\\u002Fdiv\\\\u003E\\"; + } catch (err) { + pug_rethrow(err, pug_debug_filename, pug_debug_line); + } + return pug_html; +} +module.exports = template; +" +`; diff --git a/src/test-data/pug/test/anti-cases/attrs.unescaped.pug b/src/test-data/pug/test/anti-cases/attrs.unescaped.pug new file mode 100644 index 0000000..ab47e09 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/attrs.unescaped.pug @@ -0,0 +1,3 @@ +script(type='text/x-template') + #user(id!='user-<%= user.id %>') + h1 <%= user.title %> \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/case-when.pug b/src/test-data/pug/test/anti-cases/case-when.pug new file mode 100644 index 0000000..74977d1 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/case-when.pug @@ -0,0 +1,4 @@ +when 5 + .foo +when 6 + .bar \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/case-without-with.pug b/src/test-data/pug/test/anti-cases/case-without-with.pug new file mode 100644 index 0000000..3cbf016 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/case-without-with.pug @@ -0,0 +1,2 @@ +case foo + .div \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/else-condition.pug b/src/test-data/pug/test/anti-cases/else-condition.pug new file mode 100644 index 0000000..93ff87e --- /dev/null +++ b/src/test-data/pug/test/anti-cases/else-condition.pug @@ -0,0 +1,4 @@ +if foo + div +else bar + article \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/else-without-if.pug b/src/test-data/pug/test/anti-cases/else-without-if.pug new file mode 100644 index 0000000..3062364 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/else-without-if.pug @@ -0,0 +1,2 @@ +else + .foo \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug b/src/test-data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug new file mode 100644 index 0000000..3d01493 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/inlining-a-mixin-after-a-tag.pug @@ -0,0 +1 @@ +foo()+bar() \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/key-char-ending-badly.pug b/src/test-data/pug/test/anti-cases/key-char-ending-badly.pug new file mode 100644 index 0000000..45ca24a --- /dev/null +++ b/src/test-data/pug/test/anti-cases/key-char-ending-badly.pug @@ -0,0 +1 @@ +div("foo"abc) diff --git a/src/test-data/pug/test/anti-cases/key-ending-badly.pug b/src/test-data/pug/test/anti-cases/key-ending-badly.pug new file mode 100644 index 0000000..8e3c305 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/key-ending-badly.pug @@ -0,0 +1 @@ +div(foo!~abc) diff --git a/src/test-data/pug/test/anti-cases/mismatched-inline-tag.pug b/src/test-data/pug/test/anti-cases/mismatched-inline-tag.pug new file mode 100644 index 0000000..35fa2aa --- /dev/null +++ b/src/test-data/pug/test/anti-cases/mismatched-inline-tag.pug @@ -0,0 +1,2 @@ +//- #1871 +p #[strong a} diff --git a/src/test-data/pug/test/anti-cases/mixin-args-syntax-error.pug b/src/test-data/pug/test/anti-cases/mixin-args-syntax-error.pug new file mode 100644 index 0000000..d0b725b --- /dev/null +++ b/src/test-data/pug/test/anti-cases/mixin-args-syntax-error.pug @@ -0,0 +1,2 @@ +mixin foo(a, b) ++foo('a'b'b') diff --git a/src/test-data/pug/test/anti-cases/mixins-blocks-with-bodies.pug b/src/test-data/pug/test/anti-cases/mixins-blocks-with-bodies.pug new file mode 100644 index 0000000..e7e6281 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/mixins-blocks-with-bodies.pug @@ -0,0 +1,3 @@ +mixin foo + block + bar \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug b/src/test-data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug new file mode 100644 index 0000000..fc9c884 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/multiple-non-nested-tags-on-a-line.pug @@ -0,0 +1 @@ +foo()bar \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/non-existant-filter.pug b/src/test-data/pug/test/anti-cases/non-existant-filter.pug new file mode 100644 index 0000000..8caa922 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/non-existant-filter.pug @@ -0,0 +1,2 @@ +:not-a-valid-filter + foo bar \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/non-mixin-block.pug b/src/test-data/pug/test/anti-cases/non-mixin-block.pug new file mode 100644 index 0000000..11ff9bc --- /dev/null +++ b/src/test-data/pug/test/anti-cases/non-mixin-block.pug @@ -0,0 +1,2 @@ +div + block \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/open-brace-in-attributes.pug b/src/test-data/pug/test/anti-cases/open-brace-in-attributes.pug new file mode 100644 index 0000000..7b5f21d --- /dev/null +++ b/src/test-data/pug/test/anti-cases/open-brace-in-attributes.pug @@ -0,0 +1 @@ +div(title=[) \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/readme.md b/src/test-data/pug/test/anti-cases/readme.md new file mode 100644 index 0000000..6dae996 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/readme.md @@ -0,0 +1 @@ +This folder collects examples of files that are not valid `pug`, but were at some point accepted by the parser without throwing an error. The tests ensure that all these cases now throw some form of error message (hopefully a helpful one). \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/self-closing-tag-with-block.pug b/src/test-data/pug/test/anti-cases/self-closing-tag-with-block.pug new file mode 100644 index 0000000..b1b0c1d --- /dev/null +++ b/src/test-data/pug/test/anti-cases/self-closing-tag-with-block.pug @@ -0,0 +1,2 @@ +input + | Inputs cannot have content diff --git a/src/test-data/pug/test/anti-cases/self-closing-tag-with-body.pug b/src/test-data/pug/test/anti-cases/self-closing-tag-with-body.pug new file mode 100644 index 0000000..55fbed1 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/self-closing-tag-with-body.pug @@ -0,0 +1 @@ +input Input's can't have content \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/self-closing-tag-with-code.pug b/src/test-data/pug/test/anti-cases/self-closing-tag-with-code.pug new file mode 100644 index 0000000..e836232 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/self-closing-tag-with-code.pug @@ -0,0 +1 @@ +input= 'Inputs cannot have code' diff --git a/src/test-data/pug/test/anti-cases/tabs-and-spaces.pug b/src/test-data/pug/test/anti-cases/tabs-and-spaces.pug new file mode 100644 index 0000000..07868c2 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/tabs-and-spaces.pug @@ -0,0 +1,3 @@ +div + div + article \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/unclosed-interpolated-call.pug b/src/test-data/pug/test/anti-cases/unclosed-interpolated-call.pug new file mode 100644 index 0000000..63d02db --- /dev/null +++ b/src/test-data/pug/test/anti-cases/unclosed-interpolated-call.pug @@ -0,0 +1 @@ ++#{myMixin \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/unclosed-interpolated-tag.pug b/src/test-data/pug/test/anti-cases/unclosed-interpolated-tag.pug new file mode 100644 index 0000000..be66079 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/unclosed-interpolated-tag.pug @@ -0,0 +1,4 @@ +mixin item + block + ++item( Contact \ No newline at end of file diff --git a/src/test-data/pug/test/anti-cases/unclosed-interpolation.pug b/src/test-data/pug/test/anti-cases/unclosed-interpolation.pug new file mode 100644 index 0000000..4698dd9 --- /dev/null +++ b/src/test-data/pug/test/anti-cases/unclosed-interpolation.pug @@ -0,0 +1 @@ +#{myMixin \ No newline at end of file diff --git a/src/test-data/pug/test/browser/index.html b/src/test-data/pug/test/browser/index.html new file mode 100644 index 0000000..d88e5f6 --- /dev/null +++ b/src/test-data/pug/test/browser/index.html @@ -0,0 +1,10 @@ +
\ No newline at end of file diff --git a/src/test-data/pug/test/browser/index.pug b/src/test-data/pug/test/browser/index.pug new file mode 100644 index 0000000..17e7759 --- /dev/null +++ b/src/test-data/pug/test/browser/index.pug @@ -0,0 +1,20 @@ +!!! 5 +html + head + body + textarea#input(placeholder='write pug here', style='width: 100%; min-height: 400px;'). + p + author + != myName + pre(style='background: #ECECEC;width: 100%; min-height: 400px;') + code#output + script(src='../../pug.js') + script. + var input = document.getElementById('input'); + var output = document.getElementById('output'); + setInterval(function () { + pug.render(input.value, {myName: 'Forbes Lindesay', pretty: true}, function (err, res) { + if (err) throw err; + output.textContent = res; + }) + }, 500) \ No newline at end of file diff --git a/src/test-data/pug/test/cases-es2015/attr.html b/src/test-data/pug/test/cases-es2015/attr.html new file mode 100644 index 0000000..fba8cc1 --- /dev/null +++ b/src/test-data/pug/test/cases-es2015/attr.html @@ -0,0 +1 @@ +
diff --git a/src/test-data/pug/test/cases-es2015/attr.pug b/src/test-data/pug/test/cases-es2015/attr.pug new file mode 100644 index 0000000..d19080f --- /dev/null +++ b/src/test-data/pug/test/cases-es2015/attr.pug @@ -0,0 +1,3 @@ +- var avatar = '219b77f9d21de75e81851b6b886057c7' + +div.avatar-div(style=`background-image: url(https://www.gravatar.com/avatar/${avatar})`) diff --git a/src/test-data/pug/test/cases/attrs-data.html b/src/test-data/pug/test/cases/attrs-data.html new file mode 100644 index 0000000..71116d3 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs-data.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/test-data/pug/test/cases/attrs-data.pug b/src/test-data/pug/test/cases/attrs-data.pug new file mode 100644 index 0000000..9e5b4b6 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs-data.pug @@ -0,0 +1,7 @@ +- var user = { name: 'tobi' } +foo(data-user=user) +foo(data-items=[1,2,3]) +foo(data-username='tobi') +foo(data-escaped={message: "Let's rock!"}) +foo(data-ampersand={message: "a quote: " this & that"}) +foo(data-epoc=new Date(0)) diff --git a/src/test-data/pug/test/cases/attrs.colon.html b/src/test-data/pug/test/cases/attrs.colon.html new file mode 100644 index 0000000..f8e02d6 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.colon.html @@ -0,0 +1 @@ +
Click Me! diff --git a/src/test-data/pug/test/cases/attrs.colon.pug b/src/test-data/pug/test/cases/attrs.colon.pug new file mode 100644 index 0000000..ed7ea7c --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.colon.pug @@ -0,0 +1,9 @@ +//- Tests for using a colon-prefexed attribute (typical when using short-cut for Vue.js `v-bind`) +div(:my-var="model") +span(v-for="item in items" :key="item.id" :value="item.name") +span( + v-for="item in items" + :key="item.id" + :value="item.name" +) +a(:link="goHere" value="static" :my-value="dynamic" @click="onClick()" :another="more") Click Me! diff --git a/src/test-data/pug/test/cases/attrs.html b/src/test-data/pug/test/cases/attrs.html new file mode 100644 index 0000000..9dcaee5 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.html @@ -0,0 +1,20 @@ +contactsave + +contactsave + + + + + + + + + + +
diff --git a/src/test-data/pug/test/cases/attrs.js.html b/src/test-data/pug/test/cases/attrs.js.html new file mode 100644 index 0000000..edd3813 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.js.html @@ -0,0 +1,5 @@ + + + +
+
\ No newline at end of file diff --git a/src/test-data/pug/test/cases/attrs.js.pug b/src/test-data/pug/test/cases/attrs.js.pug new file mode 100644 index 0000000..910c13a --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.js.pug @@ -0,0 +1,17 @@ +- var id = 5 +- function answer() { return 42; } +a(href='/user/' + id, class='button') +a(href = '/user/' + id, class = 'button') +meta(key='answer', value=answer()) +a(class = ['class1', 'class2']) +a.tag-class(class = ['class1', 'class2']) + +a(href='/user/' + id class='button') +a(href = '/user/' + id class = 'button') +meta(key='answer' value=answer()) +a(class = ['class1', 'class2']) +a.tag-class(class = ['class1', 'class2']) + +div(id=id)&attributes({foo: 'bar'}) +- var bar = null +div(foo=null bar=bar)&attributes({baz: 'baz'}) diff --git a/src/test-data/pug/test/cases/attrs.pug b/src/test-data/pug/test/cases/attrs.pug new file mode 100644 index 0000000..d4420e3 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.pug @@ -0,0 +1,43 @@ +a(href='/contact') contact +a(href='/save').button save +a(foo, bar, baz) +a(foo='foo, bar, baz', bar=1) +a(foo='((foo))', bar= (1) ? 1 : 0 ) +select + option(value='foo', selected) Foo + option(selected, value='bar') Bar +a(foo="class:") +input(pattern='\\S+') + +a(href='/contact') contact +a(href='/save').button save +a(foo bar baz) +a(foo='foo, bar, baz' bar=1) +a(foo='((foo))' bar= (1) ? 1 : 0 ) +select + option(value='foo' selected) Foo + option(selected value='bar') Bar +a(foo="class:") +input(pattern='\\S+') +foo(terse="true") +foo(date=new Date(0)) + +foo(abc + ,def) +foo(abc, + def) +foo(abc, + def) +foo(abc + ,def) +foo(abc + def) +foo(abc + def) + +- var attrs = {foo: 'bar', bar: ''} + +div&attributes(attrs) + +a(foo='foo' "bar"="bar") +a(foo='foo' 'bar'='bar') diff --git a/src/test-data/pug/test/cases/attrs.unescaped.html b/src/test-data/pug/test/cases/attrs.unescaped.html new file mode 100644 index 0000000..2c2f3f1 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.unescaped.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/attrs.unescaped.pug b/src/test-data/pug/test/cases/attrs.unescaped.pug new file mode 100644 index 0000000..36a4e10 --- /dev/null +++ b/src/test-data/pug/test/cases/attrs.unescaped.pug @@ -0,0 +1,3 @@ +script(type='text/x-template') + div(id!='user-<%= user.id %>') + h1 <%= user.title %> \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/1794-extends.pug b/src/test-data/pug/test/cases/auxiliary/1794-extends.pug new file mode 100644 index 0000000..99649d6 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/1794-extends.pug @@ -0,0 +1 @@ +block content \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/1794-include.pug b/src/test-data/pug/test/cases/auxiliary/1794-include.pug new file mode 100644 index 0000000..b9c03b4 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/1794-include.pug @@ -0,0 +1,4 @@ +mixin test() + .test&attributes(attributes) + ++test() \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug b/src/test-data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug new file mode 100644 index 0000000..17ca8a0 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/blocks-in-blocks-layout.pug @@ -0,0 +1,8 @@ +doctype html +html + head + title Default title + body + block body + .container + block content diff --git a/src/test-data/pug/test/cases/auxiliary/dialog.pug b/src/test-data/pug/test/cases/auxiliary/dialog.pug new file mode 100644 index 0000000..607bdec --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/dialog.pug @@ -0,0 +1,6 @@ + +extends window.pug + +block window-content + .dialog + block content diff --git a/src/test-data/pug/test/cases/auxiliary/empty-block.pug b/src/test-data/pug/test/cases/auxiliary/empty-block.pug new file mode 100644 index 0000000..776e5fe --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/empty-block.pug @@ -0,0 +1,2 @@ +block test + diff --git a/src/test-data/pug/test/cases/auxiliary/escapes.html b/src/test-data/pug/test/cases/auxiliary/escapes.html new file mode 100644 index 0000000..3b414f2 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/escapes.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/extends-empty-block-1.pug b/src/test-data/pug/test/cases/auxiliary/extends-empty-block-1.pug new file mode 100644 index 0000000..2729803 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/extends-empty-block-1.pug @@ -0,0 +1,5 @@ +extends empty-block.pug + +block test + div test1 + diff --git a/src/test-data/pug/test/cases/auxiliary/extends-empty-block-2.pug b/src/test-data/pug/test/cases/auxiliary/extends-empty-block-2.pug new file mode 100644 index 0000000..beb2e83 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/extends-empty-block-2.pug @@ -0,0 +1,5 @@ +extends empty-block.pug + +block test + div test2 + diff --git a/src/test-data/pug/test/cases/auxiliary/extends-from-root.pug b/src/test-data/pug/test/cases/auxiliary/extends-from-root.pug new file mode 100644 index 0000000..da52beb --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/extends-from-root.pug @@ -0,0 +1,4 @@ +extends /auxiliary/layout.pug + +block content + include /auxiliary/include-from-root.pug diff --git a/src/test-data/pug/test/cases/auxiliary/extends-relative.pug b/src/test-data/pug/test/cases/auxiliary/extends-relative.pug new file mode 100644 index 0000000..612879a --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/extends-relative.pug @@ -0,0 +1,4 @@ +extends ../../cases/auxiliary/layout + +block content + include ../../cases/auxiliary/include-from-root diff --git a/src/test-data/pug/test/cases/auxiliary/filter-in-include.pug b/src/test-data/pug/test/cases/auxiliary/filter-in-include.pug new file mode 100644 index 0000000..a3df945 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/filter-in-include.pug @@ -0,0 +1,8 @@ +html + head + style(type="text/css") + :less + @pad: 15px; + body { + padding: @pad; + } diff --git a/src/test-data/pug/test/cases/auxiliary/includable.js b/src/test-data/pug/test/cases/auxiliary/includable.js new file mode 100644 index 0000000..38c071e --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/includable.js @@ -0,0 +1,8 @@ +var STRING_SUBSTITUTIONS = { + // table of character substitutions + '\t': '\\t', + '\r': '\\r', + '\n': '\\n', + '"': '\\"', + '\\': '\\\\', +}; diff --git a/src/test-data/pug/test/cases/auxiliary/include-from-root.pug b/src/test-data/pug/test/cases/auxiliary/include-from-root.pug new file mode 100644 index 0000000..93c364b --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/include-from-root.pug @@ -0,0 +1 @@ +h1 hello \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug new file mode 100644 index 0000000..890febc --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.mixin.block.pug @@ -0,0 +1,11 @@ +mixin article() + article + block + +html + head + title My Application + block head + body + +article + block content diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug new file mode 100644 index 0000000..61033fa --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grand-grandparent.pug @@ -0,0 +1,2 @@ +h1 grand-grandparent +block grand-grandparent \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug new file mode 100644 index 0000000..f8ad4b8 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-grandparent.pug @@ -0,0 +1,6 @@ +extends inheritance.extend.recursive-grand-grandparent.pug + +block grand-grandparent + h2 grandparent + block grandparent + diff --git a/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug new file mode 100644 index 0000000..72d7230 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/inheritance.extend.recursive-parent.pug @@ -0,0 +1,5 @@ +extends inheritance.extend.recursive-grandparent.pug + +block grandparent + h3 parent + block parent \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/layout.include.pug b/src/test-data/pug/test/cases/auxiliary/layout.include.pug new file mode 100644 index 0000000..96734bf --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/layout.include.pug @@ -0,0 +1,7 @@ +html + head + title My Application + block head + body + block content + include window.pug diff --git a/src/test-data/pug/test/cases/auxiliary/layout.pug b/src/test-data/pug/test/cases/auxiliary/layout.pug new file mode 100644 index 0000000..7d183b3 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/layout.pug @@ -0,0 +1,6 @@ +html + head + title My Application + block head + body + block content \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug b/src/test-data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug new file mode 100644 index 0000000..e51eb01 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/mixin-at-end-of-file.pug @@ -0,0 +1,3 @@ +mixin slide + section.slide + block \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/mixins.pug b/src/test-data/pug/test/cases/auxiliary/mixins.pug new file mode 100644 index 0000000..0c14c1d --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/mixins.pug @@ -0,0 +1,3 @@ + +mixin foo() + p bar \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/pet.pug b/src/test-data/pug/test/cases/auxiliary/pet.pug new file mode 100644 index 0000000..ebee3a8 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/pet.pug @@ -0,0 +1,3 @@ +.pet + h1 {{name}} + p {{name}} is a {{species}} that is {{age}} old \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/smile.html b/src/test-data/pug/test/cases/auxiliary/smile.html new file mode 100644 index 0000000..05a0c49 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/smile.html @@ -0,0 +1 @@ +

:)

\ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/window.pug b/src/test-data/pug/test/cases/auxiliary/window.pug new file mode 100644 index 0000000..7ab7132 --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/window.pug @@ -0,0 +1,4 @@ + +.window + a(href='#').close Close + block window-content \ No newline at end of file diff --git a/src/test-data/pug/test/cases/auxiliary/yield-nested.pug b/src/test-data/pug/test/cases/auxiliary/yield-nested.pug new file mode 100644 index 0000000..0771c0a --- /dev/null +++ b/src/test-data/pug/test/cases/auxiliary/yield-nested.pug @@ -0,0 +1,10 @@ +html + head + title + body + h1 Page + #content + #content-wrapper + yield + #footer + stuff \ No newline at end of file diff --git a/src/test-data/pug/test/cases/basic.html b/src/test-data/pug/test/cases/basic.html new file mode 100644 index 0000000..a01532a --- /dev/null +++ b/src/test-data/pug/test/cases/basic.html @@ -0,0 +1,5 @@ + + +

Title

+ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/basic.pug b/src/test-data/pug/test/cases/basic.pug new file mode 100644 index 0000000..77066d1 --- /dev/null +++ b/src/test-data/pug/test/cases/basic.pug @@ -0,0 +1,3 @@ +html + body + h1 Title \ No newline at end of file diff --git a/src/test-data/pug/test/cases/blanks.html b/src/test-data/pug/test/cases/blanks.html new file mode 100644 index 0000000..d58268c --- /dev/null +++ b/src/test-data/pug/test/cases/blanks.html @@ -0,0 +1,5 @@ +
    +
  • foo
  • +
  • bar
  • +
  • baz
  • +
\ No newline at end of file diff --git a/src/test-data/pug/test/cases/blanks.pug b/src/test-data/pug/test/cases/blanks.pug new file mode 100644 index 0000000..67b0697 --- /dev/null +++ b/src/test-data/pug/test/cases/blanks.pug @@ -0,0 +1,8 @@ + + +ul + li foo + + li bar + + li baz diff --git a/src/test-data/pug/test/cases/block-code.html b/src/test-data/pug/test/cases/block-code.html new file mode 100644 index 0000000..489fe5d --- /dev/null +++ b/src/test-data/pug/test/cases/block-code.html @@ -0,0 +1,7 @@ + +
  • Uno
  • +
  • Dos
  • +
  • Tres
  • +
  • Cuatro
  • +
  • Cinco
  • +
  • Seis
  • diff --git a/src/test-data/pug/test/cases/block-code.pug b/src/test-data/pug/test/cases/block-code.pug new file mode 100644 index 0000000..9ab6854 --- /dev/null +++ b/src/test-data/pug/test/cases/block-code.pug @@ -0,0 +1,12 @@ +- + list = ["uno", "dos", "tres", + "cuatro", "cinco", "seis"]; +//- Without a block, the element is accepted and no code is generated +- +each item in list + - + string = item.charAt(0) + + .toUpperCase() + + item.slice(1); + li= string diff --git a/src/test-data/pug/test/cases/block-expansion.html b/src/test-data/pug/test/cases/block-expansion.html new file mode 100644 index 0000000..3c24259 --- /dev/null +++ b/src/test-data/pug/test/cases/block-expansion.html @@ -0,0 +1,5 @@ + +

    baz

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/block-expansion.pug b/src/test-data/pug/test/cases/block-expansion.pug new file mode 100644 index 0000000..fb40f9a --- /dev/null +++ b/src/test-data/pug/test/cases/block-expansion.pug @@ -0,0 +1,5 @@ +ul + li: a(href='#') foo + li: a(href='#') bar + +p baz \ No newline at end of file diff --git a/src/test-data/pug/test/cases/block-expansion.shorthands.html b/src/test-data/pug/test/cases/block-expansion.shorthands.html new file mode 100644 index 0000000..96cf0e7 --- /dev/null +++ b/src/test-data/pug/test/cases/block-expansion.shorthands.html @@ -0,0 +1,7 @@ +
      +
    • +
      +
      baz
      +
      +
    • +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/block-expansion.shorthands.pug b/src/test-data/pug/test/cases/block-expansion.shorthands.pug new file mode 100644 index 0000000..c52a126 --- /dev/null +++ b/src/test-data/pug/test/cases/block-expansion.shorthands.pug @@ -0,0 +1,2 @@ +ul + li.list-item: .foo: #bar baz \ No newline at end of file diff --git a/src/test-data/pug/test/cases/blockquote.html b/src/test-data/pug/test/cases/blockquote.html new file mode 100644 index 0000000..92b64de --- /dev/null +++ b/src/test-data/pug/test/cases/blockquote.html @@ -0,0 +1,4 @@ +
    +
    Try to define yourself by what you do, and you’ll burnout every time. You are. That is enough. I rest in that.
    +
    from @thefray at 1:43pm on May 10
    +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/blockquote.pug b/src/test-data/pug/test/cases/blockquote.pug new file mode 100644 index 0000000..a23b70f --- /dev/null +++ b/src/test-data/pug/test/cases/blockquote.pug @@ -0,0 +1,4 @@ +figure + blockquote + | Try to define yourself by what you do, and you’ll burnout every time. You are. That is enough. I rest in that. + figcaption from @thefray at 1:43pm on May 10 \ No newline at end of file diff --git a/src/test-data/pug/test/cases/blocks-in-blocks.html b/src/test-data/pug/test/cases/blocks-in-blocks.html new file mode 100644 index 0000000..d7955ab --- /dev/null +++ b/src/test-data/pug/test/cases/blocks-in-blocks.html @@ -0,0 +1,9 @@ + + + + Default title + + +

    Page 2

    + + diff --git a/src/test-data/pug/test/cases/blocks-in-blocks.pug b/src/test-data/pug/test/cases/blocks-in-blocks.pug new file mode 100644 index 0000000..13077d9 --- /dev/null +++ b/src/test-data/pug/test/cases/blocks-in-blocks.pug @@ -0,0 +1,4 @@ +extends ./auxiliary/blocks-in-blocks-layout.pug + +block body + h1 Page 2 diff --git a/src/test-data/pug/test/cases/blocks-in-if.html b/src/test-data/pug/test/cases/blocks-in-if.html new file mode 100644 index 0000000..c3b9107 --- /dev/null +++ b/src/test-data/pug/test/cases/blocks-in-if.html @@ -0,0 +1 @@ +

    ajax contents

    diff --git a/src/test-data/pug/test/cases/blocks-in-if.pug b/src/test-data/pug/test/cases/blocks-in-if.pug new file mode 100644 index 0000000..e0c6361 --- /dev/null +++ b/src/test-data/pug/test/cases/blocks-in-if.pug @@ -0,0 +1,19 @@ +//- see https://github.com/pugjs/pug/issues/1589 + +-var ajax = true + +-if( ajax ) + //- return only contents if ajax requests + block contents + p ajax contents + +-else + //- return all html + doctype html + html + head + meta( charset='utf8' ) + title sample + body + block contents + p all contetns diff --git a/src/test-data/pug/test/cases/case-blocks.html b/src/test-data/pug/test/cases/case-blocks.html new file mode 100644 index 0000000..893b07d --- /dev/null +++ b/src/test-data/pug/test/cases/case-blocks.html @@ -0,0 +1,5 @@ + + +

    you have a friend

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/case-blocks.pug b/src/test-data/pug/test/cases/case-blocks.pug new file mode 100644 index 0000000..345cd41 --- /dev/null +++ b/src/test-data/pug/test/cases/case-blocks.pug @@ -0,0 +1,10 @@ +html + body + - var friends = 1 + case friends + when 0 + p you have no friends + when 1 + p you have a friend + default + p you have #{friends} friends \ No newline at end of file diff --git a/src/test-data/pug/test/cases/case.html b/src/test-data/pug/test/cases/case.html new file mode 100644 index 0000000..f264fb7 --- /dev/null +++ b/src/test-data/pug/test/cases/case.html @@ -0,0 +1,8 @@ + + + +

    you have a friend

    +

    you have very few friends

    +

    Friend is a string

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/case.pug b/src/test-data/pug/test/cases/case.pug new file mode 100644 index 0000000..0fbe2ef --- /dev/null +++ b/src/test-data/pug/test/cases/case.pug @@ -0,0 +1,19 @@ +html + body + - var friends = 1 + case friends + when 0: p you have no friends + when 1: p you have a friend + default: p you have #{friends} friends + - var friends = 0 + case friends + when 0 + when 1 + p you have very few friends + default + p you have #{friends} friends + + - var friend = 'Tim:G' + case friend + when 'Tim:G': p Friend is a string + when {tim: 'g'}: p Friend is an object diff --git a/src/test-data/pug/test/cases/classes-empty.html b/src/test-data/pug/test/cases/classes-empty.html new file mode 100644 index 0000000..bcc28a9 --- /dev/null +++ b/src/test-data/pug/test/cases/classes-empty.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/classes-empty.pug b/src/test-data/pug/test/cases/classes-empty.pug new file mode 100644 index 0000000..5e66d84 --- /dev/null +++ b/src/test-data/pug/test/cases/classes-empty.pug @@ -0,0 +1,3 @@ +a(class='') +a(class=null) +a(class=undefined) \ No newline at end of file diff --git a/src/test-data/pug/test/cases/classes.html b/src/test-data/pug/test/cases/classes.html new file mode 100644 index 0000000..07da8c5 --- /dev/null +++ b/src/test-data/pug/test/cases/classes.html @@ -0,0 +1 @@ + diff --git a/src/test-data/pug/test/cases/classes.pug b/src/test-data/pug/test/cases/classes.pug new file mode 100644 index 0000000..67e1a1b --- /dev/null +++ b/src/test-data/pug/test/cases/classes.pug @@ -0,0 +1,11 @@ +a(class=['foo', 'bar', 'baz']) + + + +a.foo(class='bar').baz + + + +a.foo-bar_baz + +a(class={foo: true, bar: false, baz: true}) diff --git a/src/test-data/pug/test/cases/code.conditionals.html b/src/test-data/pug/test/cases/code.conditionals.html new file mode 100644 index 0000000..1370312 --- /dev/null +++ b/src/test-data/pug/test/cases/code.conditionals.html @@ -0,0 +1,11 @@ +

    foo

    +

    foo

    +

    foo

    +

    bar

    +

    baz

    +

    bar

    +

    yay

    +
    +
    +
    +
    diff --git a/src/test-data/pug/test/cases/code.conditionals.pug b/src/test-data/pug/test/cases/code.conditionals.pug new file mode 100644 index 0000000..aa4c715 --- /dev/null +++ b/src/test-data/pug/test/cases/code.conditionals.pug @@ -0,0 +1,43 @@ + +- if (true) + p foo +- else + p bar + +- if (true) { + p foo +- } else { + p bar +- } + +if true + p foo + p bar + p baz +else + p bar + +unless true + p foo +else + p bar + +if 'nested' + if 'works' + p yay + +//- allow empty blocks +if false +else + .bar +if true + .bar +else +.bing + +if false + .bing +else if false + .bar +else + .foo \ No newline at end of file diff --git a/src/test-data/pug/test/cases/code.escape.html b/src/test-data/pug/test/cases/code.escape.html new file mode 100644 index 0000000..c0e1758 --- /dev/null +++ b/src/test-data/pug/test/cases/code.escape.html @@ -0,0 +1,2 @@ +

    <script>

    +

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/escape-chars.pug b/src/test-data/pug/test/cases/escape-chars.pug new file mode 100644 index 0000000..f7978d6 --- /dev/null +++ b/src/test-data/pug/test/cases/escape-chars.pug @@ -0,0 +1,2 @@ +script. + var re = /\d+/; \ No newline at end of file diff --git a/src/test-data/pug/test/cases/escape-test.html b/src/test-data/pug/test/cases/escape-test.html new file mode 100644 index 0000000..15e72d9 --- /dev/null +++ b/src/test-data/pug/test/cases/escape-test.html @@ -0,0 +1,9 @@ + + + + escape-test + + + + + diff --git a/src/test-data/pug/test/cases/escape-test.pug b/src/test-data/pug/test/cases/escape-test.pug new file mode 100644 index 0000000..168c549 --- /dev/null +++ b/src/test-data/pug/test/cases/escape-test.pug @@ -0,0 +1,8 @@ +doctype html +html + head + title escape-test + body + textarea + - var txt = '' + | #{txt} diff --git a/src/test-data/pug/test/cases/escaping-class-attribute.html b/src/test-data/pug/test/cases/escaping-class-attribute.html new file mode 100644 index 0000000..9563642 --- /dev/null +++ b/src/test-data/pug/test/cases/escaping-class-attribute.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/escaping-class-attribute.pug b/src/test-data/pug/test/cases/escaping-class-attribute.pug new file mode 100644 index 0000000..dffbb8b --- /dev/null +++ b/src/test-data/pug/test/cases/escaping-class-attribute.pug @@ -0,0 +1,6 @@ +foo(attr="<%= bar %>") +foo(class="<%= bar %>") +foo(attr!="<%= bar %>") +foo(class!="<%= bar %>") +foo(class!="<%= bar %> lol rofl") +foo(class!="<%= bar %> lol rofl <%= lmao %>") diff --git a/src/test-data/pug/test/cases/filter-in-include.html b/src/test-data/pug/test/cases/filter-in-include.html new file mode 100644 index 0000000..b6b5636 --- /dev/null +++ b/src/test-data/pug/test/cases/filter-in-include.html @@ -0,0 +1,7 @@ + + + + diff --git a/src/test-data/pug/test/cases/filter-in-include.pug b/src/test-data/pug/test/cases/filter-in-include.pug new file mode 100644 index 0000000..dce48fa --- /dev/null +++ b/src/test-data/pug/test/cases/filter-in-include.pug @@ -0,0 +1 @@ +include ./auxiliary/filter-in-include.pug diff --git a/src/test-data/pug/test/cases/filters-empty.html b/src/test-data/pug/test/cases/filters-empty.html new file mode 100644 index 0000000..9ad128f --- /dev/null +++ b/src/test-data/pug/test/cases/filters-empty.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/test-data/pug/test/cases/filters-empty.pug b/src/test-data/pug/test/cases/filters-empty.pug new file mode 100644 index 0000000..7aa64de --- /dev/null +++ b/src/test-data/pug/test/cases/filters-empty.pug @@ -0,0 +1,6 @@ +- var users = [{ name: 'tobi', age: 2 }] + +fb:users + for user in users + fb:user(age=user.age) + :cdata diff --git a/src/test-data/pug/test/cases/filters.coffeescript.html b/src/test-data/pug/test/cases/filters.coffeescript.html new file mode 100644 index 0000000..7394061 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.coffeescript.html @@ -0,0 +1,9 @@ + diff --git a/src/test-data/pug/test/cases/filters.coffeescript.pug b/src/test-data/pug/test/cases/filters.coffeescript.pug new file mode 100644 index 0000000..f2be6f8 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.coffeescript.pug @@ -0,0 +1,6 @@ +script(type='text/javascript') + :coffee-script + regexp = /\n/ + :coffee-script(minify=true) + math = + square: (value) -> value * value diff --git a/src/test-data/pug/test/cases/filters.custom.html b/src/test-data/pug/test/cases/filters.custom.html new file mode 100644 index 0000000..811701c --- /dev/null +++ b/src/test-data/pug/test/cases/filters.custom.html @@ -0,0 +1,8 @@ + + + BEGINLine 1 +Line 2 + +Line 4END + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/filters.custom.pug b/src/test-data/pug/test/cases/filters.custom.pug new file mode 100644 index 0000000..16808f6 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.custom.pug @@ -0,0 +1,7 @@ +html + body + :custom(opt='val' num=2) + Line 1 + Line 2 + + Line 4 diff --git a/src/test-data/pug/test/cases/filters.include.custom.html b/src/test-data/pug/test/cases/filters.include.custom.html new file mode 100644 index 0000000..05169e5 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.include.custom.html @@ -0,0 +1,10 @@ + + + +

    BEGINhtml
    +  body
    +    pre
    +      include:custom(opt='val' num=2) filters.include.custom.pug
    +END
    + + diff --git a/src/test-data/pug/test/cases/filters.include.custom.pug b/src/test-data/pug/test/cases/filters.include.custom.pug new file mode 100644 index 0000000..5811147 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.include.custom.pug @@ -0,0 +1,4 @@ +html + body + pre + include:custom(opt='val' num=2) filters.include.custom.pug diff --git a/src/test-data/pug/test/cases/filters.include.html b/src/test-data/pug/test/cases/filters.include.html new file mode 100644 index 0000000..1dc755f --- /dev/null +++ b/src/test-data/pug/test/cases/filters.include.html @@ -0,0 +1,19 @@ + +

    Just some markdown tests.

    +

    With new line.

    + + + + + diff --git a/src/test-data/pug/test/cases/filters.include.pug b/src/test-data/pug/test/cases/filters.include.pug new file mode 100644 index 0000000..e7ea3db --- /dev/null +++ b/src/test-data/pug/test/cases/filters.include.pug @@ -0,0 +1,7 @@ +html + body + include:markdown-it some.md + script + include:coffee-script(minify=true) include-filter-coffee.coffee + script + include:coffee-script(minify=false) include-filter-coffee.coffee diff --git a/src/test-data/pug/test/cases/filters.inline.html b/src/test-data/pug/test/cases/filters.inline.html new file mode 100644 index 0000000..e602ebd --- /dev/null +++ b/src/test-data/pug/test/cases/filters.inline.html @@ -0,0 +1,3 @@ + +

    + before after

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/filters.inline.pug b/src/test-data/pug/test/cases/filters.inline.pug new file mode 100644 index 0000000..7b57985 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.inline.pug @@ -0,0 +1 @@ +p before #[:cdata inside] after \ No newline at end of file diff --git a/src/test-data/pug/test/cases/filters.less.html b/src/test-data/pug/test/cases/filters.less.html new file mode 100644 index 0000000..5cdb913 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.less.html @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/filters.less.pug b/src/test-data/pug/test/cases/filters.less.pug new file mode 100644 index 0000000..a3df945 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.less.pug @@ -0,0 +1,8 @@ +html + head + style(type="text/css") + :less + @pad: 15px; + body { + padding: @pad; + } diff --git a/src/test-data/pug/test/cases/filters.markdown.html b/src/test-data/pug/test/cases/filters.markdown.html new file mode 100644 index 0000000..aa3d975 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.markdown.html @@ -0,0 +1,5 @@ + +

    This is some awesome markdown +whoop.

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/filters.markdown.pug b/src/test-data/pug/test/cases/filters.markdown.pug new file mode 100644 index 0000000..30b1e4f --- /dev/null +++ b/src/test-data/pug/test/cases/filters.markdown.pug @@ -0,0 +1,5 @@ +html + body + :markdown + This is _some_ awesome **markdown** + whoop. diff --git a/src/test-data/pug/test/cases/filters.nested.html b/src/test-data/pug/test/cases/filters.nested.html new file mode 100644 index 0000000..a5a2af3 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.nested.html @@ -0,0 +1,2 @@ + + diff --git a/src/test-data/pug/test/cases/filters.nested.pug b/src/test-data/pug/test/cases/filters.nested.pug new file mode 100644 index 0000000..c79ccdd --- /dev/null +++ b/src/test-data/pug/test/cases/filters.nested.pug @@ -0,0 +1,10 @@ +script + :cdata:uglify-js + (function() { + console.log('test') + })() +script + :cdata:uglify-js:coffee-script + (-> + console.log 'test' + )() diff --git a/src/test-data/pug/test/cases/filters.stylus.html b/src/test-data/pug/test/cases/filters.stylus.html new file mode 100644 index 0000000..d131a14 --- /dev/null +++ b/src/test-data/pug/test/cases/filters.stylus.html @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/filters.stylus.pug b/src/test-data/pug/test/cases/filters.stylus.pug new file mode 100644 index 0000000..323d29c --- /dev/null +++ b/src/test-data/pug/test/cases/filters.stylus.pug @@ -0,0 +1,7 @@ +html + head + style(type="text/css") + :stylus + body + padding: 50px + body diff --git a/src/test-data/pug/test/cases/html.html b/src/test-data/pug/test/cases/html.html new file mode 100644 index 0000000..a038efd --- /dev/null +++ b/src/test-data/pug/test/cases/html.html @@ -0,0 +1,9 @@ +
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    + + +

    You can embed html as well.

    +

    Even as the body of a block expansion.

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/html.pug b/src/test-data/pug/test/cases/html.pug new file mode 100644 index 0000000..0e5422d --- /dev/null +++ b/src/test-data/pug/test/cases/html.pug @@ -0,0 +1,13 @@ +- var version = 1449104952939 + +
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    + + + + +p You can embed html as well. +p: Even as the body of a block expansion. diff --git a/src/test-data/pug/test/cases/html5.html b/src/test-data/pug/test/cases/html5.html new file mode 100644 index 0000000..83a553a --- /dev/null +++ b/src/test-data/pug/test/cases/html5.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/html5.pug b/src/test-data/pug/test/cases/html5.pug new file mode 100644 index 0000000..8dc68e2 --- /dev/null +++ b/src/test-data/pug/test/cases/html5.pug @@ -0,0 +1,4 @@ +doctype html +input(type='checkbox', checked) +input(type='checkbox', checked=true) +input(type='checkbox', checked=false) \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-extends-from-root.html b/src/test-data/pug/test/cases/include-extends-from-root.html new file mode 100644 index 0000000..3916f5d --- /dev/null +++ b/src/test-data/pug/test/cases/include-extends-from-root.html @@ -0,0 +1,8 @@ + + + My Application + + +

    hello

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-extends-from-root.pug b/src/test-data/pug/test/cases/include-extends-from-root.pug new file mode 100644 index 0000000..a79a57d --- /dev/null +++ b/src/test-data/pug/test/cases/include-extends-from-root.pug @@ -0,0 +1 @@ +include /auxiliary/extends-from-root.pug diff --git a/src/test-data/pug/test/cases/include-extends-of-common-template.html b/src/test-data/pug/test/cases/include-extends-of-common-template.html new file mode 100644 index 0000000..dd04738 --- /dev/null +++ b/src/test-data/pug/test/cases/include-extends-of-common-template.html @@ -0,0 +1,2 @@ +
    test1
    +
    test2
    diff --git a/src/test-data/pug/test/cases/include-extends-of-common-template.pug b/src/test-data/pug/test/cases/include-extends-of-common-template.pug new file mode 100644 index 0000000..2511f52 --- /dev/null +++ b/src/test-data/pug/test/cases/include-extends-of-common-template.pug @@ -0,0 +1,2 @@ +include auxiliary/extends-empty-block-1.pug +include auxiliary/extends-empty-block-2.pug diff --git a/src/test-data/pug/test/cases/include-extends-relative.html b/src/test-data/pug/test/cases/include-extends-relative.html new file mode 100644 index 0000000..3916f5d --- /dev/null +++ b/src/test-data/pug/test/cases/include-extends-relative.html @@ -0,0 +1,8 @@ + + + My Application + + +

    hello

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-extends-relative.pug b/src/test-data/pug/test/cases/include-extends-relative.pug new file mode 100644 index 0000000..1b5238c --- /dev/null +++ b/src/test-data/pug/test/cases/include-extends-relative.pug @@ -0,0 +1 @@ +include ../cases/auxiliary/extends-relative.pug diff --git a/src/test-data/pug/test/cases/include-filter-coffee.coffee b/src/test-data/pug/test/cases/include-filter-coffee.coffee new file mode 100644 index 0000000..9723cd7 --- /dev/null +++ b/src/test-data/pug/test/cases/include-filter-coffee.coffee @@ -0,0 +1,2 @@ +math = + square: (value) -> value * value diff --git a/src/test-data/pug/test/cases/include-only-text-body.html b/src/test-data/pug/test/cases/include-only-text-body.html new file mode 100644 index 0000000..f86b593 --- /dev/null +++ b/src/test-data/pug/test/cases/include-only-text-body.html @@ -0,0 +1 @@ +The message is "" \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-only-text-body.pug b/src/test-data/pug/test/cases/include-only-text-body.pug new file mode 100644 index 0000000..fdb080c --- /dev/null +++ b/src/test-data/pug/test/cases/include-only-text-body.pug @@ -0,0 +1,3 @@ +| The message is " +yield +| " diff --git a/src/test-data/pug/test/cases/include-only-text.html b/src/test-data/pug/test/cases/include-only-text.html new file mode 100644 index 0000000..6936ae4 --- /dev/null +++ b/src/test-data/pug/test/cases/include-only-text.html @@ -0,0 +1,5 @@ + + +

    The message is "hello world"

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-only-text.pug b/src/test-data/pug/test/cases/include-only-text.pug new file mode 100644 index 0000000..ede4f0f --- /dev/null +++ b/src/test-data/pug/test/cases/include-only-text.pug @@ -0,0 +1,5 @@ +html + body + p + include include-only-text-body.pug + em hello world diff --git a/src/test-data/pug/test/cases/include-with-text-head.html b/src/test-data/pug/test/cases/include-with-text-head.html new file mode 100644 index 0000000..716f359 --- /dev/null +++ b/src/test-data/pug/test/cases/include-with-text-head.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-with-text-head.pug b/src/test-data/pug/test/cases/include-with-text-head.pug new file mode 100644 index 0000000..4e670c0 --- /dev/null +++ b/src/test-data/pug/test/cases/include-with-text-head.pug @@ -0,0 +1,3 @@ +head + script(type='text/javascript'). + alert('hello world'); diff --git a/src/test-data/pug/test/cases/include-with-text.html b/src/test-data/pug/test/cases/include-with-text.html new file mode 100644 index 0000000..78386f7 --- /dev/null +++ b/src/test-data/pug/test/cases/include-with-text.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include-with-text.pug b/src/test-data/pug/test/cases/include-with-text.pug new file mode 100644 index 0000000..bc83ea5 --- /dev/null +++ b/src/test-data/pug/test/cases/include-with-text.pug @@ -0,0 +1,4 @@ +html + include include-with-text-head.pug + script(src='/caustic.js') + script(src='/app.js') diff --git a/src/test-data/pug/test/cases/include.script.html b/src/test-data/pug/test/cases/include.script.html new file mode 100644 index 0000000..cdd37c2 --- /dev/null +++ b/src/test-data/pug/test/cases/include.script.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include.script.pug b/src/test-data/pug/test/cases/include.script.pug new file mode 100644 index 0000000..f449144 --- /dev/null +++ b/src/test-data/pug/test/cases/include.script.pug @@ -0,0 +1,2 @@ +script#pet-template(type='text/x-template') + include auxiliary/pet.pug diff --git a/src/test-data/pug/test/cases/include.yield.nested.html b/src/test-data/pug/test/cases/include.yield.nested.html new file mode 100644 index 0000000..947b615 --- /dev/null +++ b/src/test-data/pug/test/cases/include.yield.nested.html @@ -0,0 +1,17 @@ + + + + + +

    Page

    +
    +
    +

    some content

    +

    and some more

    +
    +
    + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/include.yield.nested.pug b/src/test-data/pug/test/cases/include.yield.nested.pug new file mode 100644 index 0000000..f4a7d69 --- /dev/null +++ b/src/test-data/pug/test/cases/include.yield.nested.pug @@ -0,0 +1,4 @@ + +include auxiliary/yield-nested.pug + p some content + p and some more diff --git a/src/test-data/pug/test/cases/includes-with-ext-js.html b/src/test-data/pug/test/cases/includes-with-ext-js.html new file mode 100644 index 0000000..26c9184 --- /dev/null +++ b/src/test-data/pug/test/cases/includes-with-ext-js.html @@ -0,0 +1,2 @@ +
    var x = '\n here is some \n new lined text';
    +
    diff --git a/src/test-data/pug/test/cases/includes-with-ext-js.pug b/src/test-data/pug/test/cases/includes-with-ext-js.pug new file mode 100644 index 0000000..65bfa8a --- /dev/null +++ b/src/test-data/pug/test/cases/includes-with-ext-js.pug @@ -0,0 +1,3 @@ +pre + code + include javascript-new-lines.js diff --git a/src/test-data/pug/test/cases/includes.html b/src/test-data/pug/test/cases/includes.html new file mode 100644 index 0000000..eb61d5c --- /dev/null +++ b/src/test-data/pug/test/cases/includes.html @@ -0,0 +1,18 @@ +

    bar

    + +

    :)

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/includes.pug b/src/test-data/pug/test/cases/includes.pug new file mode 100644 index 0000000..7761ce2 --- /dev/null +++ b/src/test-data/pug/test/cases/includes.pug @@ -0,0 +1,10 @@ + +include auxiliary/mixins.pug + ++foo + +body + include auxiliary/smile.html + include auxiliary/escapes.html + script(type="text/javascript") + include:verbatim auxiliary/includable.js diff --git a/src/test-data/pug/test/cases/inheritance.alert-dialog.html b/src/test-data/pug/test/cases/inheritance.alert-dialog.html new file mode 100644 index 0000000..88a5dc6 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.alert-dialog.html @@ -0,0 +1,6 @@ +
    Close +
    +

    Alert!

    +

    I'm an alert!

    +
    +
    diff --git a/src/test-data/pug/test/cases/inheritance.alert-dialog.pug b/src/test-data/pug/test/cases/inheritance.alert-dialog.pug new file mode 100644 index 0000000..7afcaf0 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.alert-dialog.pug @@ -0,0 +1,6 @@ + +extends auxiliary/dialog.pug + +block content + h1 Alert! + p I'm an alert! diff --git a/src/test-data/pug/test/cases/inheritance.defaults.html b/src/test-data/pug/test/cases/inheritance.defaults.html new file mode 100644 index 0000000..e6878d1 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.defaults.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.defaults.pug b/src/test-data/pug/test/cases/inheritance.defaults.pug new file mode 100644 index 0000000..aaead83 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.defaults.pug @@ -0,0 +1,6 @@ +html + head + block head + script(src='jquery.js') + script(src='keymaster.js') + script(src='caustic.js') \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.extend.html b/src/test-data/pug/test/cases/inheritance.extend.html new file mode 100644 index 0000000..1f4eae4 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.html @@ -0,0 +1,10 @@ + + + My Application + + + +

    Page

    +

    Some content

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.extend.include.html b/src/test-data/pug/test/cases/inheritance.extend.include.html new file mode 100644 index 0000000..66da1cc --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.include.html @@ -0,0 +1,14 @@ + + + My Application + + + +

    Page

    +

    Some content

    +
    Close +

    Awesome

    +

    Now we can extend included blocks!

    +
    + + diff --git a/src/test-data/pug/test/cases/inheritance.extend.include.pug b/src/test-data/pug/test/cases/inheritance.extend.include.pug new file mode 100644 index 0000000..b67dfc3 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.include.pug @@ -0,0 +1,13 @@ + +extend auxiliary/layout.include.pug + +block head + script(src='jquery.js') + +block content + h2 Page + p Some content + +block window-content + h2 Awesome + p Now we can extend included blocks! diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.block.html b/src/test-data/pug/test/cases/inheritance.extend.mixins.block.html new file mode 100644 index 0000000..0ea5d94 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.mixins.block.html @@ -0,0 +1,10 @@ + + + My Application + + +
    +

    Hello World!

    +
    + + diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.block.pug b/src/test-data/pug/test/cases/inheritance.extend.mixins.block.pug new file mode 100644 index 0000000..775a5dc --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.mixins.block.pug @@ -0,0 +1,4 @@ +extend auxiliary/inheritance.extend.mixin.block.pug + +block content + p Hello World! diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.html b/src/test-data/pug/test/cases/inheritance.extend.mixins.html new file mode 100644 index 0000000..618e2b1 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.mixins.html @@ -0,0 +1,9 @@ + + + My Application + + +

    The meaning of life

    +

    Foo bar baz!

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.extend.mixins.pug b/src/test-data/pug/test/cases/inheritance.extend.mixins.pug new file mode 100644 index 0000000..ceaa412 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.mixins.pug @@ -0,0 +1,11 @@ + +extend auxiliary/layout.pug + +mixin article(title) + if title + h1= title + block + +block content + +article("The meaning of life") + p Foo bar baz! diff --git a/src/test-data/pug/test/cases/inheritance.extend.pug b/src/test-data/pug/test/cases/inheritance.extend.pug new file mode 100644 index 0000000..4ce3a6f --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.pug @@ -0,0 +1,9 @@ + +extend auxiliary/layout.pug + +block head + script(src='jquery.js') + +block content + h2 Page + p Some content diff --git a/src/test-data/pug/test/cases/inheritance.extend.recursive.html b/src/test-data/pug/test/cases/inheritance.extend.recursive.html new file mode 100644 index 0000000..d5d0522 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.recursive.html @@ -0,0 +1,4 @@ +

    grand-grandparent

    +

    grandparent

    +

    parent

    +

    child

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.extend.recursive.pug b/src/test-data/pug/test/cases/inheritance.extend.recursive.pug new file mode 100644 index 0000000..5842523 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.recursive.pug @@ -0,0 +1,4 @@ +extends /auxiliary/inheritance.extend.recursive-parent.pug + +block parent + h4 child \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.extend.whitespace.html b/src/test-data/pug/test/cases/inheritance.extend.whitespace.html new file mode 100644 index 0000000..1f4eae4 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.whitespace.html @@ -0,0 +1,10 @@ + + + My Application + + + +

    Page

    +

    Some content

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.extend.whitespace.pug b/src/test-data/pug/test/cases/inheritance.extend.whitespace.pug new file mode 100644 index 0000000..25ee9e0 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.extend.whitespace.pug @@ -0,0 +1,13 @@ + +extend auxiliary/layout.pug + +block head + + script(src='jquery.js') + +block content + + + + h2 Page + p Some content diff --git a/src/test-data/pug/test/cases/inheritance.html b/src/test-data/pug/test/cases/inheritance.html new file mode 100644 index 0000000..1f4eae4 --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.html @@ -0,0 +1,10 @@ + + + My Application + + + +

    Page

    +

    Some content

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inheritance.pug b/src/test-data/pug/test/cases/inheritance.pug new file mode 100644 index 0000000..dd5415d --- /dev/null +++ b/src/test-data/pug/test/cases/inheritance.pug @@ -0,0 +1,9 @@ + +extends auxiliary/layout.pug + +block head + script(src='jquery.js') + +block content + h2 Page + p Some content diff --git a/src/test-data/pug/test/cases/inline-tag.html b/src/test-data/pug/test/cases/inline-tag.html new file mode 100644 index 0000000..7ea3af7 --- /dev/null +++ b/src/test-data/pug/test/cases/inline-tag.html @@ -0,0 +1,21 @@ + +

    bing foo bong

    +

    + bing + foo + [foo] + + bong + +

    +

    + bing + foo + [foo] + + bong +

    +

    + #[strong escaped] + #[escaped +

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/inline-tag.pug b/src/test-data/pug/test/cases/inline-tag.pug new file mode 100644 index 0000000..df7b549 --- /dev/null +++ b/src/test-data/pug/test/cases/inline-tag.pug @@ -0,0 +1,19 @@ +p bing #[strong foo] bong + +p. + bing + #[strong foo] + #[strong= '[foo]'] + #[- var foo = 'foo]'] + bong + +p + | bing + | #[strong foo] + | #[strong= '[foo]'] + | #[- var foo = 'foo]'] + | bong + +p. + \#[strong escaped] + \#[#[strong escaped] diff --git a/src/test-data/pug/test/cases/intepolated-elements.html b/src/test-data/pug/test/cases/intepolated-elements.html new file mode 100644 index 0000000..721fa02 --- /dev/null +++ b/src/test-data/pug/test/cases/intepolated-elements.html @@ -0,0 +1,4 @@ + +

    with inline link

    +

    Some text

    +

    Some text with inline link

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/intepolated-elements.pug b/src/test-data/pug/test/cases/intepolated-elements.pug new file mode 100644 index 0000000..5fe8bcf --- /dev/null +++ b/src/test-data/pug/test/cases/intepolated-elements.pug @@ -0,0 +1,3 @@ +p #[a.rho(href='#', class='rho--modifier') with inline link] +p Some text #[a.rho(href='#', class='rho--modifier')] +p Some text #[a.rho(href='#', class='rho--modifier') with inline link] \ No newline at end of file diff --git a/src/test-data/pug/test/cases/interpolated-mixin.html b/src/test-data/pug/test/cases/interpolated-mixin.html new file mode 100644 index 0000000..101aa95 --- /dev/null +++ b/src/test-data/pug/test/cases/interpolated-mixin.html @@ -0,0 +1,3 @@ + +

    This also works http://www.bing.com so hurrah for Pug +

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/interpolated-mixin.pug b/src/test-data/pug/test/cases/interpolated-mixin.pug new file mode 100644 index 0000000..ae8fc74 --- /dev/null +++ b/src/test-data/pug/test/cases/interpolated-mixin.pug @@ -0,0 +1,4 @@ +mixin linkit(url) + a(href=url)= url + +p This also works #[+linkit('http://www.bing.com')] so hurrah for Pug \ No newline at end of file diff --git a/src/test-data/pug/test/cases/interpolation.escape.html b/src/test-data/pug/test/cases/interpolation.escape.html new file mode 100644 index 0000000..8dd546b --- /dev/null +++ b/src/test-data/pug/test/cases/interpolation.escape.html @@ -0,0 +1,6 @@ + + some + #{text} + here + My ID is {42} + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/interpolation.escape.pug b/src/test-data/pug/test/cases/interpolation.escape.pug new file mode 100644 index 0000000..cff251b --- /dev/null +++ b/src/test-data/pug/test/cases/interpolation.escape.pug @@ -0,0 +1,7 @@ + +- var id = 42; +foo + | some + | \#{text} + | here + | My ID #{"is {" + id + "}"} \ No newline at end of file diff --git a/src/test-data/pug/test/cases/javascript-new-lines.js b/src/test-data/pug/test/cases/javascript-new-lines.js new file mode 100644 index 0000000..bb0c26f --- /dev/null +++ b/src/test-data/pug/test/cases/javascript-new-lines.js @@ -0,0 +1 @@ +var x = '\n here is some \n new lined text'; diff --git a/src/test-data/pug/test/cases/layout.append.html b/src/test-data/pug/test/cases/layout.append.html new file mode 100644 index 0000000..bc5e126 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.append.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/cases/layout.append.pug b/src/test-data/pug/test/cases/layout.append.pug new file mode 100644 index 0000000..d771bc9 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.append.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/append/app-layout.pug + +block append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/cases/layout.append.without-block.html b/src/test-data/pug/test/cases/layout.append.without-block.html new file mode 100644 index 0000000..bc5e126 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.append.without-block.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/cases/layout.append.without-block.pug b/src/test-data/pug/test/cases/layout.append.without-block.pug new file mode 100644 index 0000000..19842fc --- /dev/null +++ b/src/test-data/pug/test/cases/layout.append.without-block.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/append-without-block/app-layout.pug + +append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/cases/layout.multi.append.prepend.block.html b/src/test-data/pug/test/cases/layout.multi.append.prepend.block.html new file mode 100644 index 0000000..314c2b3 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.multi.append.prepend.block.html @@ -0,0 +1,8 @@ +

    Last prepend must appear at top

    +

    Something prepended to content

    +
    Defined content
    +

    Something appended to content

    +

    Last append must be most last

    + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/layout.multi.append.prepend.block.pug b/src/test-data/pug/test/cases/layout.multi.append.prepend.block.pug new file mode 100644 index 0000000..79d15b1 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.multi.append.prepend.block.pug @@ -0,0 +1,19 @@ +extends ../fixtures/multi-append-prepend-block/redefine.pug + +append content + p.first.append Something appended to content + +prepend content + p.first.prepend Something prepended to content + +append content + p.last.append Last append must be most last + +prepend content + p.last.prepend Last prepend must appear at top + +append head + script(src='jquery.js') + +prepend head + script(src='foo.js') diff --git a/src/test-data/pug/test/cases/layout.prepend.html b/src/test-data/pug/test/cases/layout.prepend.html new file mode 100644 index 0000000..8753a42 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.prepend.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/cases/layout.prepend.pug b/src/test-data/pug/test/cases/layout.prepend.pug new file mode 100644 index 0000000..4659a11 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.prepend.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/prepend/app-layout.pug + +block prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/cases/layout.prepend.without-block.html b/src/test-data/pug/test/cases/layout.prepend.without-block.html new file mode 100644 index 0000000..8753a42 --- /dev/null +++ b/src/test-data/pug/test/cases/layout.prepend.without-block.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/cases/layout.prepend.without-block.pug b/src/test-data/pug/test/cases/layout.prepend.without-block.pug new file mode 100644 index 0000000..516d01b --- /dev/null +++ b/src/test-data/pug/test/cases/layout.prepend.without-block.pug @@ -0,0 +1,6 @@ + +extends ../fixtures/prepend-without-block/app-layout.pug + +prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/cases/mixin-at-end-of-file.html b/src/test-data/pug/test/cases/mixin-at-end-of-file.html new file mode 100644 index 0000000..495ca32 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-at-end-of-file.html @@ -0,0 +1,3 @@ +
    +

    some awesome content

    +
    diff --git a/src/test-data/pug/test/cases/mixin-at-end-of-file.pug b/src/test-data/pug/test/cases/mixin-at-end-of-file.pug new file mode 100644 index 0000000..3d2faa1 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-at-end-of-file.pug @@ -0,0 +1,4 @@ +include ./auxiliary/mixin-at-end-of-file.pug + ++slide() + p some awesome content diff --git a/src/test-data/pug/test/cases/mixin-block-with-space.html b/src/test-data/pug/test/cases/mixin-block-with-space.html new file mode 100644 index 0000000..5f1fc02 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-block-with-space.html @@ -0,0 +1,3 @@ + +
    This text should appear +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin-block-with-space.pug b/src/test-data/pug/test/cases/mixin-block-with-space.pug new file mode 100644 index 0000000..471aac8 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-block-with-space.pug @@ -0,0 +1,6 @@ +mixin m(id) + div + block + ++m() + | This text should appear \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin-hoist.html b/src/test-data/pug/test/cases/mixin-hoist.html new file mode 100644 index 0000000..1755a30 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-hoist.html @@ -0,0 +1,5 @@ + + +

    Pug

    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin-hoist.pug b/src/test-data/pug/test/cases/mixin-hoist.pug new file mode 100644 index 0000000..eb2c423 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-hoist.pug @@ -0,0 +1,7 @@ + +mixin foo() + h1= title + +html + body + +foo diff --git a/src/test-data/pug/test/cases/mixin-via-include.html b/src/test-data/pug/test/cases/mixin-via-include.html new file mode 100644 index 0000000..8124337 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-via-include.html @@ -0,0 +1 @@ +

    bar

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin-via-include.pug b/src/test-data/pug/test/cases/mixin-via-include.pug new file mode 100644 index 0000000..bb7b6d2 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin-via-include.pug @@ -0,0 +1,5 @@ +//- regression test for https://github.com/pugjs/pug/issues/1435 + +include ../fixtures/mixin-include.pug + ++bang \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin.attrs.html b/src/test-data/pug/test/cases/mixin.attrs.html new file mode 100644 index 0000000..2f2e0ef --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.attrs.html @@ -0,0 +1,32 @@ + +
    Hello World +
    +
    +

    Section 1

    +

    Some important content.

    +
    +
    +

    Section 2

    +

    Even more important content.

    + +
    +
    +
    +

    Section 3

    +

    Last content.

    + +
    +
    +
    +

    Some final words.

    +
    +
    +
    + +
    +
    work
    +
    +

    1

    +

    2

    +

    3

    +

    4

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin.attrs.pug b/src/test-data/pug/test/cases/mixin.attrs.pug new file mode 100644 index 0000000..82a46ff --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.attrs.pug @@ -0,0 +1,59 @@ +mixin centered(title) + div.centered(id=attributes.id) + - if (title) + h1(class=attributes.class)= title + block + - if (attributes.href) + .footer + a(href=attributes.href) Back + +mixin main(title) + div.stretch + +centered(title).highlight&attributes(attributes) + block + +mixin bottom + div.bottom&attributes(attributes) + block + +body + +centered#First Hello World + +centered('Section 1')#Second + p Some important content. + +centered('Section 2')#Third.foo(href='menu.html', class='bar') + p Even more important content. + +main('Section 3')(href='#') + p Last content. + +bottom.foo(class='bar', name='end', id='Last', data-attr='baz') + p Some final words. + +bottom(class=['class1', 'class2']) + +mixin foo + div.thing(attr1='foo', attr2='bar')&attributes(attributes) + +- var val = '' +- var classes = ['foo', 'bar'] ++foo(attr3='baz' data-foo=val data-bar!=val class=classes).thunk + +//- Regression test for #1424 +mixin work_filmstrip_item(work) + div&attributes(attributes)= work ++work_filmstrip_item('work')("data-profile"='profile', "data-creator-name"='name') + +mixin my-mixin(arg1, arg2, arg3, arg4) + p= arg1 + p= arg2 + p= arg3 + p= arg4 + ++foo( + attr3="qux" + class="baz" +) + ++my-mixin( +'1', + '2', + '3', + '4' +) diff --git a/src/test-data/pug/test/cases/mixin.block-tag-behaviour.html b/src/test-data/pug/test/cases/mixin.block-tag-behaviour.html new file mode 100644 index 0000000..580dbe0 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.block-tag-behaviour.html @@ -0,0 +1,22 @@ + + +
    +

    Foo

    +

    I'm article foo

    +
    + + + + +
    +

    Something

    +

    + I'm a much longer + text-only article, + but you can still + inline html tags + in me if you want. +

    +
    + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin.block-tag-behaviour.pug b/src/test-data/pug/test/cases/mixin.block-tag-behaviour.pug new file mode 100644 index 0000000..1d2d2d3 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.block-tag-behaviour.pug @@ -0,0 +1,24 @@ + +mixin article(name) + section.article + h1= name + block + +html + body + +article('Foo'): p I'm article foo + +mixin article(name) + section.article + h1= name + p + block + +html + body + +article('Something'). + I'm a much longer + text-only article, + but you can still + inline html tags + in me if you want. \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin.blocks.html b/src/test-data/pug/test/cases/mixin.blocks.html new file mode 100644 index 0000000..def5c6f --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.blocks.html @@ -0,0 +1,34 @@ + + +
    + + + +
    + + + + +
    + + + +
    + + + + +
    + +
    + + +
    +
    +

    one

    +

    two

    +

    three

    +
    +
    +
    123 +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin.blocks.pug b/src/test-data/pug/test/cases/mixin.blocks.pug new file mode 100644 index 0000000..30c9990 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.blocks.pug @@ -0,0 +1,44 @@ + + +mixin form(method, action) + form(method=method, action=action) + - var csrf_token_from_somewhere = 'hey' + input(type='hidden', name='_csrf', value=csrf_token_from_somewhere) + block + +html + body + +form('GET', '/search') + input(type='text', name='query', placeholder='Search') + input(type='submit', value='Search') + +html + body + +form('POST', '/search') + input(type='text', name='query', placeholder='Search') + input(type='submit', value='Search') + +html + body + +form('POST', '/search') + +mixin bar() + #bar + block + +mixin foo() + #foo + +bar + block + ++foo + p one + p two + p three + + +mixin baz + #baz + block + ++baz()= '123' \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixin.merge.html b/src/test-data/pug/test/cases/mixin.merge.html new file mode 100644 index 0000000..e513d35 --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.merge.html @@ -0,0 +1,34 @@ + +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    +

    One

    +

    Two

    +

    Three

    +

    Four

    + diff --git a/src/test-data/pug/test/cases/mixin.merge.pug b/src/test-data/pug/test/cases/mixin.merge.pug new file mode 100644 index 0000000..f0d217d --- /dev/null +++ b/src/test-data/pug/test/cases/mixin.merge.pug @@ -0,0 +1,15 @@ +mixin foo + p.bar&attributes(attributes) One + p.baz.quux&attributes(attributes) Two + p&attributes(attributes) Three + p.bar&attributes(attributes)(class="baz") Four + +body + +foo.hello + +foo#world + +foo.hello#world + +foo.hello.world + +foo(class="hello") + +foo.hello(class="world") + +foo + +foo&attributes({class: "hello"}) \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixins-unused.html b/src/test-data/pug/test/cases/mixins-unused.html new file mode 100644 index 0000000..5db7bc1 --- /dev/null +++ b/src/test-data/pug/test/cases/mixins-unused.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixins-unused.pug b/src/test-data/pug/test/cases/mixins-unused.pug new file mode 100644 index 0000000..b0af6cc --- /dev/null +++ b/src/test-data/pug/test/cases/mixins-unused.pug @@ -0,0 +1,3 @@ +mixin never-called + .wtf This isn't something we ever want to output +body \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixins.html b/src/test-data/pug/test/cases/mixins.html new file mode 100644 index 0000000..a75b175 --- /dev/null +++ b/src/test-data/pug/test/cases/mixins.html @@ -0,0 +1,23 @@ + +
    +

    Tobi

    +
    +
    +

    This

    +

    is regular, javascript

    +
    +
    +
    + +
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    +
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    + +
    This is interpolated
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/mixins.pug b/src/test-data/pug/test/cases/mixins.pug new file mode 100644 index 0000000..4e45671 --- /dev/null +++ b/src/test-data/pug/test/cases/mixins.pug @@ -0,0 +1,32 @@ +mixin comment(title, str) + .comment + h2= title + p.body= str + + +mixin comment (title, str) + .comment + h2= title + p.body= str + +#user + h1 Tobi + .comments + +comment('This', + (('is regular, javascript'))) + +mixin list + ul + li foo + li bar + li baz + +body + +list() + + list() + +mixin foobar(str) + div#interpolation= str + 'interpolated' + +- var suffix = "bar" ++#{'foo' + suffix}('This is ') diff --git a/src/test-data/pug/test/cases/mixins.rest-args.html b/src/test-data/pug/test/cases/mixins.rest-args.html new file mode 100644 index 0000000..5b37365 --- /dev/null +++ b/src/test-data/pug/test/cases/mixins.rest-args.html @@ -0,0 +1,6 @@ +
      +
    • 1
    • +
    • 2
    • +
    • 3
    • +
    • 4
    • +
    diff --git a/src/test-data/pug/test/cases/mixins.rest-args.pug b/src/test-data/pug/test/cases/mixins.rest-args.pug new file mode 100644 index 0000000..929a927 --- /dev/null +++ b/src/test-data/pug/test/cases/mixins.rest-args.pug @@ -0,0 +1,6 @@ +mixin list(tag, ...items) + #{tag} + each item in items + li= item + ++list('ul', 1, 2, 3, 4) diff --git a/src/test-data/pug/test/cases/namespaces.html b/src/test-data/pug/test/cases/namespaces.html new file mode 100644 index 0000000..90522ac --- /dev/null +++ b/src/test-data/pug/test/cases/namespaces.html @@ -0,0 +1,2 @@ +Something + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/namespaces.pug b/src/test-data/pug/test/cases/namespaces.pug new file mode 100644 index 0000000..0694677 --- /dev/null +++ b/src/test-data/pug/test/cases/namespaces.pug @@ -0,0 +1,2 @@ +fb:user:role Something +foo(fb:foo='bar') \ No newline at end of file diff --git a/src/test-data/pug/test/cases/nesting.html b/src/test-data/pug/test/cases/nesting.html new file mode 100644 index 0000000..56c15cb --- /dev/null +++ b/src/test-data/pug/test/cases/nesting.html @@ -0,0 +1,11 @@ +
      +
    • a
    • +
    • b
    • +
    • +
        +
      • c
      • +
      • d
      • +
      +
    • +
    • e
    • +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/nesting.pug b/src/test-data/pug/test/cases/nesting.pug new file mode 100644 index 0000000..f8cab4d --- /dev/null +++ b/src/test-data/pug/test/cases/nesting.pug @@ -0,0 +1,8 @@ +ul + li a + li b + li + ul + li c + li d + li e \ No newline at end of file diff --git a/src/test-data/pug/test/cases/pipeless-comments.html b/src/test-data/pug/test/cases/pipeless-comments.html new file mode 100644 index 0000000..5f9af83 --- /dev/null +++ b/src/test-data/pug/test/cases/pipeless-comments.html @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/pipeless-comments.pug b/src/test-data/pug/test/cases/pipeless-comments.pug new file mode 100644 index 0000000..426e459 --- /dev/null +++ b/src/test-data/pug/test/cases/pipeless-comments.pug @@ -0,0 +1,4 @@ +// + .foo + .bar + .hey diff --git a/src/test-data/pug/test/cases/pipeless-filters.html b/src/test-data/pug/test/cases/pipeless-filters.html new file mode 100644 index 0000000..64e4cb7 --- /dev/null +++ b/src/test-data/pug/test/cases/pipeless-filters.html @@ -0,0 +1,2 @@ +
    code sample
    +

    Heading

    diff --git a/src/test-data/pug/test/cases/pipeless-filters.pug b/src/test-data/pug/test/cases/pipeless-filters.pug new file mode 100644 index 0000000..b24c25a --- /dev/null +++ b/src/test-data/pug/test/cases/pipeless-filters.pug @@ -0,0 +1,4 @@ +:markdown-it + code sample + + # Heading diff --git a/src/test-data/pug/test/cases/pipeless-tag.html b/src/test-data/pug/test/cases/pipeless-tag.html new file mode 100644 index 0000000..f6f8935 --- /dev/null +++ b/src/test-data/pug/test/cases/pipeless-tag.html @@ -0,0 +1,3 @@ + +
      what
    +is going on
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/pipeless-tag.pug b/src/test-data/pug/test/cases/pipeless-tag.pug new file mode 100644 index 0000000..d521da4 --- /dev/null +++ b/src/test-data/pug/test/cases/pipeless-tag.pug @@ -0,0 +1,3 @@ +pre. + what + is #{'going'} #[| #{'on'}] diff --git a/src/test-data/pug/test/cases/pre.html b/src/test-data/pug/test/cases/pre.html new file mode 100644 index 0000000..33bab4e --- /dev/null +++ b/src/test-data/pug/test/cases/pre.html @@ -0,0 +1,7 @@ +
    foo
    +bar
    +baz
    +
    +
    foo
    +bar
    +baz
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/pre.pug b/src/test-data/pug/test/cases/pre.pug new file mode 100644 index 0000000..75673c5 --- /dev/null +++ b/src/test-data/pug/test/cases/pre.pug @@ -0,0 +1,10 @@ +pre. + foo + bar + baz + +pre + code. + foo + bar + baz \ No newline at end of file diff --git a/src/test-data/pug/test/cases/quotes.html b/src/test-data/pug/test/cases/quotes.html new file mode 100644 index 0000000..592b136 --- /dev/null +++ b/src/test-data/pug/test/cases/quotes.html @@ -0,0 +1,2 @@ +

    "foo"

    +

    'foo'

    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/quotes.pug b/src/test-data/pug/test/cases/quotes.pug new file mode 100644 index 0000000..499c835 --- /dev/null +++ b/src/test-data/pug/test/cases/quotes.pug @@ -0,0 +1,2 @@ +p "foo" +p 'foo' \ No newline at end of file diff --git a/src/test-data/pug/test/cases/regression.1794.html b/src/test-data/pug/test/cases/regression.1794.html new file mode 100644 index 0000000..b322cca --- /dev/null +++ b/src/test-data/pug/test/cases/regression.1794.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/regression.1794.pug b/src/test-data/pug/test/cases/regression.1794.pug new file mode 100644 index 0000000..fb33c31 --- /dev/null +++ b/src/test-data/pug/test/cases/regression.1794.pug @@ -0,0 +1,4 @@ +extends ./auxiliary/1794-extends.pug + +block content + include ./auxiliary/1794-include.pug \ No newline at end of file diff --git a/src/test-data/pug/test/cases/regression.784.html b/src/test-data/pug/test/cases/regression.784.html new file mode 100644 index 0000000..933e986 --- /dev/null +++ b/src/test-data/pug/test/cases/regression.784.html @@ -0,0 +1 @@ +
    google.com
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/regression.784.pug b/src/test-data/pug/test/cases/regression.784.pug new file mode 100644 index 0000000..bab7540 --- /dev/null +++ b/src/test-data/pug/test/cases/regression.784.pug @@ -0,0 +1,2 @@ +- var url = 'http://www.google.com' +.url #{url.replace('http://', '').replace(/^www\./, '')} \ No newline at end of file diff --git a/src/test-data/pug/test/cases/script.whitespace.html b/src/test-data/pug/test/cases/script.whitespace.html new file mode 100644 index 0000000..45b7ced --- /dev/null +++ b/src/test-data/pug/test/cases/script.whitespace.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/script.whitespace.pug b/src/test-data/pug/test/cases/script.whitespace.pug new file mode 100644 index 0000000..e0afc3a --- /dev/null +++ b/src/test-data/pug/test/cases/script.whitespace.pug @@ -0,0 +1,6 @@ +script. + if (foo) { + + bar(); + + } \ No newline at end of file diff --git a/src/test-data/pug/test/cases/scripts.html b/src/test-data/pug/test/cases/scripts.html new file mode 100644 index 0000000..e3dc48b --- /dev/null +++ b/src/test-data/pug/test/cases/scripts.html @@ -0,0 +1,9 @@ + + + + +
    \ No newline at end of file diff --git a/src/test-data/pug/test/cases/scripts.non-js.html b/src/test-data/pug/test/cases/scripts.non-js.html new file mode 100644 index 0000000..9daff38 --- /dev/null +++ b/src/test-data/pug/test/cases/scripts.non-js.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/scripts.non-js.pug b/src/test-data/pug/test/cases/scripts.non-js.pug new file mode 100644 index 0000000..9f9a408 --- /dev/null +++ b/src/test-data/pug/test/cases/scripts.non-js.pug @@ -0,0 +1,9 @@ +script#user-template(type='text/template') + #user + h1 <%= user.name %> + p <%= user.description %> + +script#user-template(type='text/template'). + if (foo) { + bar(); + } \ No newline at end of file diff --git a/src/test-data/pug/test/cases/scripts.pug b/src/test-data/pug/test/cases/scripts.pug new file mode 100644 index 0000000..d28887f --- /dev/null +++ b/src/test-data/pug/test/cases/scripts.pug @@ -0,0 +1,8 @@ +script. + if (foo) { + bar(); + } +script!= 'foo()' +script foo() +script +div \ No newline at end of file diff --git a/src/test-data/pug/test/cases/self-closing-html.html b/src/test-data/pug/test/cases/self-closing-html.html new file mode 100644 index 0000000..02a38d0 --- /dev/null +++ b/src/test-data/pug/test/cases/self-closing-html.html @@ -0,0 +1,4 @@ + + +
    + diff --git a/src/test-data/pug/test/cases/self-closing-html.pug b/src/test-data/pug/test/cases/self-closing-html.pug new file mode 100644 index 0000000..094e42a --- /dev/null +++ b/src/test-data/pug/test/cases/self-closing-html.pug @@ -0,0 +1,4 @@ +doctype html +html + body + br/ diff --git a/src/test-data/pug/test/cases/single-period.html b/src/test-data/pug/test/cases/single-period.html new file mode 100644 index 0000000..430944c --- /dev/null +++ b/src/test-data/pug/test/cases/single-period.html @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/src/test-data/pug/test/cases/single-period.pug b/src/test-data/pug/test/cases/single-period.pug new file mode 100644 index 0000000..f3d734c --- /dev/null +++ b/src/test-data/pug/test/cases/single-period.pug @@ -0,0 +1 @@ +span . \ No newline at end of file diff --git a/src/test-data/pug/test/cases/some-included.styl b/src/test-data/pug/test/cases/some-included.styl new file mode 100644 index 0000000..7458543 --- /dev/null +++ b/src/test-data/pug/test/cases/some-included.styl @@ -0,0 +1,2 @@ +body + padding 10px diff --git a/src/test-data/pug/test/cases/some.md b/src/test-data/pug/test/cases/some.md new file mode 100644 index 0000000..8ea3e54 --- /dev/null +++ b/src/test-data/pug/test/cases/some.md @@ -0,0 +1,3 @@ +Just _some_ markdown **tests**. + +With new line. diff --git a/src/test-data/pug/test/cases/some.styl b/src/test-data/pug/test/cases/some.styl new file mode 100644 index 0000000..f77222d --- /dev/null +++ b/src/test-data/pug/test/cases/some.styl @@ -0,0 +1 @@ +@import "some-included" diff --git a/src/test-data/pug/test/cases/source.html b/src/test-data/pug/test/cases/source.html new file mode 100644 index 0000000..1881c0f --- /dev/null +++ b/src/test-data/pug/test/cases/source.html @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/source.pug b/src/test-data/pug/test/cases/source.pug new file mode 100644 index 0000000..db22b80 --- /dev/null +++ b/src/test-data/pug/test/cases/source.pug @@ -0,0 +1,4 @@ +html + audio(preload='auto', autobuffer, controls) + source(src='foo') + source(src='bar') \ No newline at end of file diff --git a/src/test-data/pug/test/cases/styles.html b/src/test-data/pug/test/cases/styles.html new file mode 100644 index 0000000..251556e --- /dev/null +++ b/src/test-data/pug/test/cases/styles.html @@ -0,0 +1,20 @@ + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/src/test-data/pug/test/cases/styles.pug b/src/test-data/pug/test/cases/styles.pug new file mode 100644 index 0000000..9618353 --- /dev/null +++ b/src/test-data/pug/test/cases/styles.pug @@ -0,0 +1,19 @@ +html + head + style. + body { + padding: 50px; + } + body + div(style='color:red;background:green') + div(style={color: 'red', background: 'green'}) + div&attributes({style: 'color:red;background:green'}) + div&attributes({style: {color: 'red', background: 'green'}}) + mixin div() + div&attributes(attributes) + +div(style='color:red;background:green') + +div(style={color: 'red', background: 'green'}) + - var bg = 'green'; + div(style={color: 'red', background: bg}) + div&attributes({style: {color: 'red', background: bg}}) + +div(style={color: 'red', background: bg}) diff --git a/src/test-data/pug/test/cases/tag.interpolation.html b/src/test-data/pug/test/cases/tag.interpolation.html new file mode 100644 index 0000000..9f2816c --- /dev/null +++ b/src/test-data/pug/test/cases/tag.interpolation.html @@ -0,0 +1,9 @@ +

    value

    +

    value

    +here + diff --git a/src/test-data/pug/test/cases/tag.interpolation.pug b/src/test-data/pug/test/cases/tag.interpolation.pug new file mode 100644 index 0000000..d923ddb --- /dev/null +++ b/src/test-data/pug/test/cases/tag.interpolation.pug @@ -0,0 +1,22 @@ + +- var tag = 'p' +- var foo = 'bar' + +#{tag} value +#{tag}(foo='bar') value +#{foo ? 'a' : 'li'}(something) here + +mixin item(icon) + li + if attributes.href + a&attributes(attributes) + img.icon(src=icon) + block + else + span&attributes(attributes) + img.icon(src=icon) + block + +ul + +item('contact') Contact + +item(href='/contact') Contact diff --git a/src/test-data/pug/test/cases/tags.self-closing.html b/src/test-data/pug/test/cases/tags.self-closing.html new file mode 100644 index 0000000..4f0bc7b --- /dev/null +++ b/src/test-data/pug/test/cases/tags.self-closing.html @@ -0,0 +1,14 @@ + + + + + + + / + / + + + / + / + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/tags.self-closing.pug b/src/test-data/pug/test/cases/tags.self-closing.pug new file mode 100644 index 0000000..9207c32 --- /dev/null +++ b/src/test-data/pug/test/cases/tags.self-closing.pug @@ -0,0 +1,19 @@ + +body + foo + foo(bar='baz') + foo/ + foo(bar='baz')/ + foo / + foo(bar='baz') / + #{'foo'}/ + #{'foo'}(bar='baz')/ + #{'foo'} / + #{'foo'}(bar='baz') / + //- can have a single space after them + img + //- can have lots of white space after them + img + #{ + 'foo' + }/ diff --git a/src/test-data/pug/test/cases/template.html b/src/test-data/pug/test/cases/template.html new file mode 100644 index 0000000..2054e05 --- /dev/null +++ b/src/test-data/pug/test/cases/template.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/template.pug b/src/test-data/pug/test/cases/template.pug new file mode 100644 index 0000000..20e086b --- /dev/null +++ b/src/test-data/pug/test/cases/template.pug @@ -0,0 +1,9 @@ +script(type='text/x-template') + article + h2 {{title}} + p {{description}} + +script(type='text/x-template'). + article + h2 {{title}} + p {{description}} diff --git a/src/test-data/pug/test/cases/text-block.html b/src/test-data/pug/test/cases/text-block.html new file mode 100644 index 0000000..fae8caa --- /dev/null +++ b/src/test-data/pug/test/cases/text-block.html @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/text-block.pug b/src/test-data/pug/test/cases/text-block.pug new file mode 100644 index 0000000..a032fa7 --- /dev/null +++ b/src/test-data/pug/test/cases/text-block.pug @@ -0,0 +1,6 @@ + +label Username: + input(type='text', name='user[name]') + +label Password: + input(type='text', name='user[pass]') \ No newline at end of file diff --git a/src/test-data/pug/test/cases/text.html b/src/test-data/pug/test/cases/text.html new file mode 100644 index 0000000..0b0efb0 --- /dev/null +++ b/src/test-data/pug/test/cases/text.html @@ -0,0 +1,36 @@ + + +

    +

    +

    + foo + bar + + + baz +

    +

    + foo + + + bar + baz + +

    foo + + +bar +baz + +
    foo
    +  bar
    +    baz
    +.
    +
    foo
    +  bar
    +    baz
    +.
    +
    foo + bar + baz +. diff --git a/src/test-data/pug/test/cases/text.pug b/src/test-data/pug/test/cases/text.pug new file mode 100644 index 0000000..abb0e0b --- /dev/null +++ b/src/test-data/pug/test/cases/text.pug @@ -0,0 +1,46 @@ +option(value='') -- (selected) -- + +p + +p. + +p + | foo + | bar + | + | + | baz + +p. + foo + + + bar + baz + +. + +. + foo + + + bar + baz + +pre + | foo + | bar + | baz + | . + +pre. + foo + bar + baz + . + +. + foo + bar + baz + . diff --git a/src/test-data/pug/test/cases/utf8bom.html b/src/test-data/pug/test/cases/utf8bom.html new file mode 100644 index 0000000..e3e18f0 --- /dev/null +++ b/src/test-data/pug/test/cases/utf8bom.html @@ -0,0 +1 @@ +

    "foo"

    diff --git a/src/test-data/pug/test/cases/utf8bom.pug b/src/test-data/pug/test/cases/utf8bom.pug new file mode 100644 index 0000000..9a32814 --- /dev/null +++ b/src/test-data/pug/test/cases/utf8bom.pug @@ -0,0 +1 @@ +p "foo" diff --git a/src/test-data/pug/test/cases/vars.html b/src/test-data/pug/test/cases/vars.html new file mode 100644 index 0000000..e9b7590 --- /dev/null +++ b/src/test-data/pug/test/cases/vars.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/vars.pug b/src/test-data/pug/test/cases/vars.pug new file mode 100644 index 0000000..46451a9 --- /dev/null +++ b/src/test-data/pug/test/cases/vars.pug @@ -0,0 +1,3 @@ +- var foo = 'bar' +- var list = [1,2,3] +a(class=list, id=foo) \ No newline at end of file diff --git a/src/test-data/pug/test/cases/while.html b/src/test-data/pug/test/cases/while.html new file mode 100644 index 0000000..dff7ff6 --- /dev/null +++ b/src/test-data/pug/test/cases/while.html @@ -0,0 +1,11 @@ +
      +
    • 2
    • +
    • 3
    • +
    • 4
    • +
    • 5
    • +
    • 6
    • +
    • 7
    • +
    • 8
    • +
    • 9
    • +
    • 10
    • +
    diff --git a/src/test-data/pug/test/cases/while.pug b/src/test-data/pug/test/cases/while.pug new file mode 100644 index 0000000..059b54b --- /dev/null +++ b/src/test-data/pug/test/cases/while.pug @@ -0,0 +1,5 @@ +- var x = 1; +ul + while x < 10 + - x++; + li= x diff --git a/src/test-data/pug/test/cases/xml.html b/src/test-data/pug/test/cases/xml.html new file mode 100644 index 0000000..5fd9f1a --- /dev/null +++ b/src/test-data/pug/test/cases/xml.html @@ -0,0 +1,3 @@ + + +http://google.com \ No newline at end of file diff --git a/src/test-data/pug/test/cases/xml.pug b/src/test-data/pug/test/cases/xml.pug new file mode 100644 index 0000000..2b21fa4 --- /dev/null +++ b/src/test-data/pug/test/cases/xml.pug @@ -0,0 +1,3 @@ +doctype xml +category(term='some term')/ +link http://google.com \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield-before-conditional-head.html b/src/test-data/pug/test/cases/yield-before-conditional-head.html new file mode 100644 index 0000000..35ace64 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-before-conditional-head.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield-before-conditional-head.pug b/src/test-data/pug/test/cases/yield-before-conditional-head.pug new file mode 100644 index 0000000..8515406 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-before-conditional-head.pug @@ -0,0 +1,5 @@ +head + script(src='/jquery.js') + yield + if false + script(src='/jquery.ui.js') diff --git a/src/test-data/pug/test/cases/yield-before-conditional.html b/src/test-data/pug/test/cases/yield-before-conditional.html new file mode 100644 index 0000000..7a3f184 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-before-conditional.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield-before-conditional.pug b/src/test-data/pug/test/cases/yield-before-conditional.pug new file mode 100644 index 0000000..56b3385 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-before-conditional.pug @@ -0,0 +1,5 @@ +html + body + include yield-before-conditional-head.pug + script(src='/caustic.js') + script(src='/app.js') diff --git a/src/test-data/pug/test/cases/yield-head.html b/src/test-data/pug/test/cases/yield-head.html new file mode 100644 index 0000000..83f92b5 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-head.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield-head.pug b/src/test-data/pug/test/cases/yield-head.pug new file mode 100644 index 0000000..1428be6 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-head.pug @@ -0,0 +1,4 @@ +head + script(src='/jquery.js') + yield + script(src='/jquery.ui.js') diff --git a/src/test-data/pug/test/cases/yield-title-head.html b/src/test-data/pug/test/cases/yield-title-head.html new file mode 100644 index 0000000..ae62c27 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-title-head.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield-title-head.pug b/src/test-data/pug/test/cases/yield-title-head.pug new file mode 100644 index 0000000..5ec7d32 --- /dev/null +++ b/src/test-data/pug/test/cases/yield-title-head.pug @@ -0,0 +1,5 @@ +head + title + yield + script(src='/jquery.js') + script(src='/jquery.ui.js') diff --git a/src/test-data/pug/test/cases/yield-title.html b/src/test-data/pug/test/cases/yield-title.html new file mode 100644 index 0000000..83ef1fb --- /dev/null +++ b/src/test-data/pug/test/cases/yield-title.html @@ -0,0 +1,9 @@ + + + + My Title + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield-title.pug b/src/test-data/pug/test/cases/yield-title.pug new file mode 100644 index 0000000..54b5f4d --- /dev/null +++ b/src/test-data/pug/test/cases/yield-title.pug @@ -0,0 +1,4 @@ +html + body + include yield-title-head.pug + | My Title diff --git a/src/test-data/pug/test/cases/yield.html b/src/test-data/pug/test/cases/yield.html new file mode 100644 index 0000000..b16459d --- /dev/null +++ b/src/test-data/pug/test/cases/yield.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/test-data/pug/test/cases/yield.pug b/src/test-data/pug/test/cases/yield.pug new file mode 100644 index 0000000..7579241 --- /dev/null +++ b/src/test-data/pug/test/cases/yield.pug @@ -0,0 +1,5 @@ +html + body + include yield-head.pug + script(src='/caustic.js') + script(src='/app.js') diff --git a/src/test-data/pug/test/dependencies/dependency1.pug b/src/test-data/pug/test/dependencies/dependency1.pug new file mode 100644 index 0000000..5c24ab5 --- /dev/null +++ b/src/test-data/pug/test/dependencies/dependency1.pug @@ -0,0 +1 @@ +strong dependency1 diff --git a/src/test-data/pug/test/dependencies/dependency2.pug b/src/test-data/pug/test/dependencies/dependency2.pug new file mode 100644 index 0000000..76c1170 --- /dev/null +++ b/src/test-data/pug/test/dependencies/dependency2.pug @@ -0,0 +1 @@ +include dependency3.pug diff --git a/src/test-data/pug/test/dependencies/dependency3.pug b/src/test-data/pug/test/dependencies/dependency3.pug new file mode 100644 index 0000000..8cb467c --- /dev/null +++ b/src/test-data/pug/test/dependencies/dependency3.pug @@ -0,0 +1 @@ +strong dependency3 diff --git a/src/test-data/pug/test/dependencies/extends1.pug b/src/test-data/pug/test/dependencies/extends1.pug new file mode 100644 index 0000000..9fe5a9e --- /dev/null +++ b/src/test-data/pug/test/dependencies/extends1.pug @@ -0,0 +1 @@ +extends dependency1.pug diff --git a/src/test-data/pug/test/dependencies/extends2.pug b/src/test-data/pug/test/dependencies/extends2.pug new file mode 100644 index 0000000..802810c --- /dev/null +++ b/src/test-data/pug/test/dependencies/extends2.pug @@ -0,0 +1 @@ +extends dependency2.pug diff --git a/src/test-data/pug/test/dependencies/include1.pug b/src/test-data/pug/test/dependencies/include1.pug new file mode 100644 index 0000000..923e9ea --- /dev/null +++ b/src/test-data/pug/test/dependencies/include1.pug @@ -0,0 +1 @@ +include dependency1.pug diff --git a/src/test-data/pug/test/dependencies/include2.pug b/src/test-data/pug/test/dependencies/include2.pug new file mode 100644 index 0000000..0f93cec --- /dev/null +++ b/src/test-data/pug/test/dependencies/include2.pug @@ -0,0 +1 @@ +include dependency2.pug diff --git a/src/test-data/pug/test/duplicate-block/__snapshots__/index.test.js.snap b/src/test-data/pug/test/duplicate-block/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..b861222 --- /dev/null +++ b/src/test-data/pug/test/duplicate-block/__snapshots__/index.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`layout with duplicate block 1`] = `"
    Hello World
    "`; + +exports[`layout with duplicate block 2`] = `"
    Hello World
    "`; diff --git a/src/test-data/pug/test/duplicate-block/index.pug b/src/test-data/pug/test/duplicate-block/index.pug new file mode 100644 index 0000000..87a454b --- /dev/null +++ b/src/test-data/pug/test/duplicate-block/index.pug @@ -0,0 +1,4 @@ +extends ./layout-with-duplicate-block.pug + +block content + div Hello World diff --git a/src/test-data/pug/test/duplicate-block/index.test.js b/src/test-data/pug/test/duplicate-block/index.test.js new file mode 100644 index 0000000..d77631f --- /dev/null +++ b/src/test-data/pug/test/duplicate-block/index.test.js @@ -0,0 +1,10 @@ +const pug = require('../../'); + +test('layout with duplicate block', () => { + const outputWithAjax = pug.renderFile(__dirname + '/index.pug', {ajax: true}); + const outputWithoutAjax = pug.renderFile(__dirname + '/index.pug', { + ajax: false, + }); + expect(outputWithAjax).toMatchSnapshot(); + expect(outputWithoutAjax).toMatchSnapshot(); +}); diff --git a/src/test-data/pug/test/duplicate-block/layout-with-duplicate-block.pug b/src/test-data/pug/test/duplicate-block/layout-with-duplicate-block.pug new file mode 100644 index 0000000..41f1160 --- /dev/null +++ b/src/test-data/pug/test/duplicate-block/layout-with-duplicate-block.pug @@ -0,0 +1,8 @@ +if ajax + block content +else + doctype html + html + head + body + block content diff --git a/src/test-data/pug/test/eachOf/__snapshots__/index.test.js.snap b/src/test-data/pug/test/eachOf/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..b898335 --- /dev/null +++ b/src/test-data/pug/test/eachOf/__snapshots__/index.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Proper Usage Brackets 1`] = `"
  • a
  • b
  • foo
  • bar
  • "`; + +exports[`Proper Usage No Brackets 1`] = `"
  • a
  • b
  • foo
  • bar
  • "`; diff --git a/src/test-data/pug/test/eachOf/error/left-side.pug b/src/test-data/pug/test/eachOf/error/left-side.pug new file mode 100644 index 0000000..b2a081d --- /dev/null +++ b/src/test-data/pug/test/eachOf/error/left-side.pug @@ -0,0 +1,3 @@ +each [key, val of users + li= key + li= val diff --git a/src/test-data/pug/test/eachOf/error/no-brackets.pug b/src/test-data/pug/test/eachOf/error/no-brackets.pug new file mode 100644 index 0000000..83fbbff --- /dev/null +++ b/src/test-data/pug/test/eachOf/error/no-brackets.pug @@ -0,0 +1,3 @@ +each key, val of users + li= key + li= val diff --git a/src/test-data/pug/test/eachOf/error/one-val.pug b/src/test-data/pug/test/eachOf/error/one-val.pug new file mode 100644 index 0000000..0c6ff70 --- /dev/null +++ b/src/test-data/pug/test/eachOf/error/one-val.pug @@ -0,0 +1,3 @@ +each [key] of users + li= key + li= val diff --git a/src/test-data/pug/test/eachOf/error/right-side.pug b/src/test-data/pug/test/eachOf/error/right-side.pug new file mode 100644 index 0000000..6a6aab5 --- /dev/null +++ b/src/test-data/pug/test/eachOf/error/right-side.pug @@ -0,0 +1,3 @@ +each key, val] of users + li= key + li= val diff --git a/src/test-data/pug/test/eachOf/index.test.js b/src/test-data/pug/test/eachOf/index.test.js new file mode 100644 index 0000000..69da0ba --- /dev/null +++ b/src/test-data/pug/test/eachOf/index.test.js @@ -0,0 +1,44 @@ +const pug = require('../../'); + +describe('Inproper Usage', () => { + test('Only left-side bracket', () => { + expect(() => pug.compileFile(__dirname + '/error/left-side.pug')).toThrow( + 'The value variable for each must either be a valid identifier (e.g. `item`) or a pair of identifiers in square brackets (e.g. `[key, value]`).' + ); + }); + test('Only right-side bracket', () => { + expect(() => pug.compileFile(__dirname + '/error/right-side.pug')).toThrow( + 'The value variable for each must either be a valid identifier (e.g. `item`) or a pair of identifiers in square brackets (e.g. `[key, value]`).' + ); + }); + test('Only one value inside brackets', () => { + expect(() => pug.compileFile(__dirname + '/error/one-val.pug')).toThrow( + 'The value variable for each must either be a valid identifier (e.g. `item`) or a pair of identifiers in square brackets (e.g. `[key, value]`).' + ); + }); + test('No brackets', () => { + expect(() => pug.compileFile(__dirname + '/error/no-brackets.pug')).toThrow( + 'The value variable for each must either be a valid identifier (e.g. `item`) or a pair of identifiers in square brackets (e.g. `[key, value]`).' + ); + }); +}); +describe('Proper Usage', () => { + test('Brackets', () => { + const html = pug.renderFile(__dirname + '/passing/brackets.pug', { + users: new Map([ + ['a', 'b'], + ['foo', 'bar'], + ]), + }); + expect(html).toMatchSnapshot(); + }); + test('No Brackets', () => { + const html = pug.renderFile(__dirname + '/passing/no-brackets.pug', { + users: new Map([ + ['a', 'b'], + ['foo', 'bar'], + ]), + }); + expect(html).toMatchSnapshot(); + }); +}); diff --git a/src/test-data/pug/test/eachOf/passing/brackets.pug b/src/test-data/pug/test/eachOf/passing/brackets.pug new file mode 100644 index 0000000..b8193d5 --- /dev/null +++ b/src/test-data/pug/test/eachOf/passing/brackets.pug @@ -0,0 +1,3 @@ +each [key, val] of users + li= key + li= val diff --git a/src/test-data/pug/test/eachOf/passing/no-brackets.pug b/src/test-data/pug/test/eachOf/passing/no-brackets.pug new file mode 100644 index 0000000..880fee5 --- /dev/null +++ b/src/test-data/pug/test/eachOf/passing/no-brackets.pug @@ -0,0 +1,3 @@ +each data of users + li= data[0] + li= data[1] diff --git a/src/test-data/pug/test/error.reporting.test.js b/src/test-data/pug/test/error.reporting.test.js new file mode 100644 index 0000000..99ee8e3 --- /dev/null +++ b/src/test-data/pug/test/error.reporting.test.js @@ -0,0 +1,260 @@ +/** + * Module dependencies. + */ + +var pug = require('../'); +var assert = require('assert'); +var fs = require('fs'); + +// Shortcut + +function getError(str, options) { + try { + pug.render(str, options); + } catch (ex) { + return ex; + } + throw new Error('Input was supposed to result in an error.'); +} +function getFileError(name, options) { + try { + pug.renderFile(name, options); + } catch (ex) { + return ex; + } + throw new Error('Input was supposed to result in an error.'); +} + +describe('error reporting', function() { + describe('compile time errors', function() { + describe('with no filename', function() { + it('includes detail of where the error was thrown', function() { + var err = getError('foo('); + expect(err.message).toMatch(/Pug:1/); + expect(err.message).toMatch(/foo\(/); + }); + }); + describe('with a filename', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getError('foo(', {filename: 'test.pug'}); + expect(err.message).toMatch(/test\.pug:1/); + expect(err.message).toMatch(/foo\(/); + }); + }); + describe('with a layout without block declaration (syntax)', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/compile.with.layout.syntax.error.pug', + {} + ); + expect(err.message).toMatch(/[\\\/]layout.syntax.error.pug:2/); + expect(err.message).toMatch(/foo\(/); + }); + }); + describe('with a layout without block declaration (locals)', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/compile.with.layout.locals.error.pug', + {} + ); + expect(err.message).toMatch(/[\\\/]layout.locals.error.pug:2/); + expect(err.message).toMatch(/is not a function/); + }); + }); + describe('with a include (syntax)', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/compile.with.include.syntax.error.pug', + {} + ); + expect(err.message).toMatch(/[\\\/]include.syntax.error.pug:2/); + expect(err.message).toMatch(/foo\(/); + }); + }); + describe('with a include (locals)', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/compile.with.include.locals.error.pug', + {} + ); + expect(err.message).toMatch(/[\\\/]include.locals.error.pug:2/); + expect(err.message).toMatch(/foo\(/); + }); + + it('handles compileDebug option properly', function() { + var err = getFileError( + __dirname + '/fixtures/compile.with.include.locals.error.pug', + { + compileDebug: true, + } + ); + expect(err.message).toMatch(/[\\\/]include.locals.error.pug:2/); + expect(err.message).toMatch(/foo is not a function/); + }); + }); + + describe('with a layout (without block) with an include (syntax)', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + + '/fixtures/compile.with.layout.with.include.syntax.error.pug', + {} + ); + expect(err.message).toMatch(/[\\\/]include.syntax.error.pug:2/); + expect(err.message).toMatch(/foo\(/); + }); + }); + describe('with a layout (without block) with an include (locals)', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + + '/fixtures/compile.with.layout.with.include.locals.error.pug', + {} + ); + expect(err.message).toMatch(/[\\\/]include.locals.error.pug:2/); + expect(err.message).toMatch(/foo\(/); + }); + }); + describe('block that is never actually used', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/invalid-block-in-extends.pug', + {} + ); + expect(err.message).toMatch(/invalid-block-in-extends.pug:6/); + expect(err.message).toMatch(/content/); + }); + }); + describe('Unexpected character', function() { + it('includes details of where the error was thrown', function() { + var err = getError('ul?', {}); + expect(err.message).toMatch(/unexpected text \"\?\"/); + }); + }); + describe('Include filtered', function() { + it('includes details of where the error was thrown', function() { + var err = getError('include:verbatim()!', {}); + assert(err.message.indexOf('unexpected text "!"') !== -1); + var err = getError('include:verbatim ', {}); + assert(err.message.indexOf('missing path for include') !== -1); + }); + }); + describe('mixin block followed by a lot of blank lines', function() { + it('reports the correct line number', function() { + var err = getError('mixin test\n block\n\ndiv()Test'); + var line = /Pug\:(\d+)/.exec(err.message); + assert(line, 'Line number must be included in error message'); + assert( + line[1] === '4', + 'The error should be reported on line 4, not line ' + line[1] + ); + }); + }); + }); + describe('runtime errors', function() { + describe('with no filename and `compileDebug` left undefined', function() { + it('just reports the line number', function() { + var sentinel = new Error('sentinel'); + var err = getError('-foo()', { + foo: function() { + throw sentinel; + }, + }); + expect(err.message).toMatch(/on line 1/); + }); + }); + describe('with no filename and `compileDebug` set to `true`', function() { + it('includes detail of where the error was thrown', function() { + var sentinel = new Error('sentinel'); + var err = getError('-foo()', { + foo: function() { + throw sentinel; + }, + compileDebug: true, + }); + expect(err.message).toMatch(/Pug:1/); + expect(err.message).toMatch(/-foo\(\)/); + }); + }); + describe('with a filename that does not correspond to a real file and `compileDebug` left undefined', function() { + it('just reports the line number', function() { + var sentinel = new Error('sentinel'); + var err = getError('-foo()', { + foo: function() { + throw sentinel; + }, + filename: 'fake.pug', + }); + expect(err.message).toMatch(/on line 1/); + }); + }); + describe('with a filename that corresponds to a real file and `compileDebug` left undefined', function() { + it('includes detail of where the error was thrown including the filename', function() { + var sentinel = new Error('sentinel'); + var path = __dirname + '/fixtures/runtime.error.pug'; + var err = getError(fs.readFileSync(path, 'utf8'), { + foo: function() { + throw sentinel; + }, + filename: path, + }); + expect(err.message).toMatch(/fixtures[\\\/]runtime\.error\.pug:1/); + expect(err.message).toMatch(/-foo\(\)/); + }); + }); + describe('in a mixin', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/runtime.with.mixin.error.pug', + {} + ); + expect(err.message).toMatch(/mixin.error.pug:2/); + expect(err.message).toMatch(/Cannot read property 'length' of null/); + }); + }); + describe('in a layout', function() { + it('includes detail of where the error was thrown including the filename', function() { + var err = getFileError( + __dirname + '/fixtures/runtime.layout.error.pug', + {} + ); + expect(err.message).toMatch(/layout.with.runtime.error.pug:3/); + expect(err.message).toMatch( + /Cannot read property 'length' of undefined/ + ); + }); + }); + }); + describe('deprecated features', function() { + it('warns about element-with-multiple-attributes', function() { + var consoleWarn = console.warn; + var log = ''; + console.warn = function(str) { + log += str; + }; + var res = pug.renderFile( + __dirname + '/fixtures/element-with-multiple-attributes.pug' + ); + console.warn = consoleWarn; + expect(log).toMatch(/element-with-multiple-attributes.pug, line 1:/); + expect(log).toMatch( + /You should not have pug tags with multiple attributes/ + ); + expect(res).toBe('
    '); + }); + }); + describe("if you throw something that isn't an error", function() { + it('just rethrows without modification', function() { + var err = getError('- throw "foo"'); + expect(err).toBe('foo'); + }); + }); + describe('import without a filename for a basedir', function() { + it('throws an error', function() { + var err = getError('include foo.pug'); + expect(err.message).toMatch(/the "filename" option is required to use/); + var err = getError('include /foo.pug'); + expect(err.message).toMatch(/the "basedir" option is required to use/); + }); + }); +}); diff --git a/src/test-data/pug/test/examples.test.js b/src/test-data/pug/test/examples.test.js new file mode 100644 index 0000000..a4a4c22 --- /dev/null +++ b/src/test-data/pug/test/examples.test.js @@ -0,0 +1,23 @@ +'use strict'; + +var fs = require('fs'); +var pug = require('../'); + +describe('examples', function() { + fs.readdirSync(__dirname + '/../examples').forEach(function(example) { + if (/\.js$/.test(example)) { + it(example + ' does not throw any error', function() { + var log = console.log; + var err = console.error; + console.log = function() {}; + console.error = function() {}; + try { + require('../examples/' + example); + } finally { + console.log = log; + console.error = err; + } + }); + } + }); +}); diff --git a/src/test-data/pug/test/extends-not-top-level/default.pug b/src/test-data/pug/test/extends-not-top-level/default.pug new file mode 100644 index 0000000..94ae27e --- /dev/null +++ b/src/test-data/pug/test/extends-not-top-level/default.pug @@ -0,0 +1,2 @@ +body + block content diff --git a/src/test-data/pug/test/extends-not-top-level/duplicate.pug b/src/test-data/pug/test/extends-not-top-level/duplicate.pug new file mode 100644 index 0000000..3786c4c --- /dev/null +++ b/src/test-data/pug/test/extends-not-top-level/duplicate.pug @@ -0,0 +1,2 @@ +extends default +extends default diff --git a/src/test-data/pug/test/extends-not-top-level/index.pug b/src/test-data/pug/test/extends-not-top-level/index.pug new file mode 100644 index 0000000..8064ff4 --- /dev/null +++ b/src/test-data/pug/test/extends-not-top-level/index.pug @@ -0,0 +1,10 @@ +mixin content + if bar + extends default + block content + block + else + block + ++content + h1 Hello! diff --git a/src/test-data/pug/test/extends-not-top-level/index.test.js b/src/test-data/pug/test/extends-not-top-level/index.test.js new file mode 100644 index 0000000..a0fbf27 --- /dev/null +++ b/src/test-data/pug/test/extends-not-top-level/index.test.js @@ -0,0 +1,15 @@ +const pug = require('../../'); + +// regression test for #2404 + +test('extends not top level should throw an error', () => { + expect(() => pug.compileFile(__dirname + '/index.pug')).toThrow( + 'Declaration of template inheritance ("extends") should be the first thing in the file. There can only be one extends statement per file.' + ); +}); + +test('duplicate extends should throw an error', () => { + expect(() => pug.compileFile(__dirname + '/duplicate.pug')).toThrow( + 'Declaration of template inheritance ("extends") should be the first thing in the file. There can only be one extends statement per file.' + ); +}); diff --git a/src/test-data/pug/test/fixtures/append-without-block/app-layout.pug b/src/test-data/pug/test/fixtures/append-without-block/app-layout.pug new file mode 100644 index 0000000..1b55872 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append-without-block/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout.pug + +append head + script(src='app.js') diff --git a/src/test-data/pug/test/fixtures/append-without-block/layout.pug b/src/test-data/pug/test/fixtures/append-without-block/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append-without-block/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/append-without-block/page.pug b/src/test-data/pug/test/fixtures/append-without-block/page.pug new file mode 100644 index 0000000..e607ae7 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append-without-block/page.pug @@ -0,0 +1,6 @@ + +extends app-layout.pug + +append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/fixtures/append/app-layout.pug b/src/test-data/pug/test/fixtures/append/app-layout.pug new file mode 100644 index 0000000..48bf886 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout + +block append head + script(src='app.js') \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/append/layout.pug b/src/test-data/pug/test/fixtures/append/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/append/page.html b/src/test-data/pug/test/fixtures/append/page.html new file mode 100644 index 0000000..bc5e126 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append/page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/fixtures/append/page.pug b/src/test-data/pug/test/fixtures/append/page.pug new file mode 100644 index 0000000..1ae9909 --- /dev/null +++ b/src/test-data/pug/test/fixtures/append/page.pug @@ -0,0 +1,6 @@ + +extends app-layout + +block append head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/fixtures/compile.with.include.locals.error.pug b/src/test-data/pug/test/fixtures/compile.with.include.locals.error.pug new file mode 100644 index 0000000..0cabc64 --- /dev/null +++ b/src/test-data/pug/test/fixtures/compile.with.include.locals.error.pug @@ -0,0 +1 @@ +include include.locals.error.pug \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/compile.with.include.syntax.error.pug b/src/test-data/pug/test/fixtures/compile.with.include.syntax.error.pug new file mode 100644 index 0000000..3ab355a --- /dev/null +++ b/src/test-data/pug/test/fixtures/compile.with.include.syntax.error.pug @@ -0,0 +1 @@ +include include.syntax.error.pug \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.locals.error.pug b/src/test-data/pug/test/fixtures/compile.with.layout.locals.error.pug new file mode 100644 index 0000000..d6df843 --- /dev/null +++ b/src/test-data/pug/test/fixtures/compile.with.layout.locals.error.pug @@ -0,0 +1 @@ +extends layout.locals.error.pug \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.syntax.error.pug b/src/test-data/pug/test/fixtures/compile.with.layout.syntax.error.pug new file mode 100644 index 0000000..616d7e8 --- /dev/null +++ b/src/test-data/pug/test/fixtures/compile.with.layout.syntax.error.pug @@ -0,0 +1 @@ +extends layout.syntax.error.pug \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug b/src/test-data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug new file mode 100644 index 0000000..cd5ebb1 --- /dev/null +++ b/src/test-data/pug/test/fixtures/compile.with.layout.with.include.locals.error.pug @@ -0,0 +1 @@ +extends compile.with.include.locals.error.pug \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug b/src/test-data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug new file mode 100644 index 0000000..a6221b3 --- /dev/null +++ b/src/test-data/pug/test/fixtures/compile.with.layout.with.include.syntax.error.pug @@ -0,0 +1 @@ +extends compile.with.include.syntax.error.pug \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/element-with-multiple-attributes.pug b/src/test-data/pug/test/fixtures/element-with-multiple-attributes.pug new file mode 100644 index 0000000..e76f560 --- /dev/null +++ b/src/test-data/pug/test/fixtures/element-with-multiple-attributes.pug @@ -0,0 +1 @@ +div(attr='val')(foo='bar') \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/include.locals.error.pug b/src/test-data/pug/test/fixtures/include.locals.error.pug new file mode 100644 index 0000000..bd604a9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/include.locals.error.pug @@ -0,0 +1,2 @@ + += foo() \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/include.syntax.error.pug b/src/test-data/pug/test/fixtures/include.syntax.error.pug new file mode 100644 index 0000000..8b0542a --- /dev/null +++ b/src/test-data/pug/test/fixtures/include.syntax.error.pug @@ -0,0 +1,2 @@ + += foo( \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/invalid-block-in-extends.pug b/src/test-data/pug/test/fixtures/invalid-block-in-extends.pug new file mode 100644 index 0000000..981321a --- /dev/null +++ b/src/test-data/pug/test/fixtures/invalid-block-in-extends.pug @@ -0,0 +1,7 @@ +extends ./layout.pug + +block title + title My Article + +block contents + // oops, that's not a block \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/issue-1593/include-layout.pug b/src/test-data/pug/test/fixtures/issue-1593/include-layout.pug new file mode 100644 index 0000000..224225f --- /dev/null +++ b/src/test-data/pug/test/fixtures/issue-1593/include-layout.pug @@ -0,0 +1,2 @@ +.included-layout + block include-body diff --git a/src/test-data/pug/test/fixtures/issue-1593/include.pug b/src/test-data/pug/test/fixtures/issue-1593/include.pug new file mode 100644 index 0000000..2e33e5b --- /dev/null +++ b/src/test-data/pug/test/fixtures/issue-1593/include.pug @@ -0,0 +1,4 @@ +extends ./include-layout.pug + +block include-body + .include-body diff --git a/src/test-data/pug/test/fixtures/issue-1593/index.pug b/src/test-data/pug/test/fixtures/issue-1593/index.pug new file mode 100644 index 0000000..5346ac9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/issue-1593/index.pug @@ -0,0 +1,7 @@ +extends ./layout.pug + +block body-a + .body-a +block body-b + .body-b + include ./include.pug diff --git a/src/test-data/pug/test/fixtures/issue-1593/layout.pug b/src/test-data/pug/test/fixtures/issue-1593/layout.pug new file mode 100644 index 0000000..a235db7 --- /dev/null +++ b/src/test-data/pug/test/fixtures/issue-1593/layout.pug @@ -0,0 +1,3 @@ +.layout-body + block body-a + block body-b diff --git a/src/test-data/pug/test/fixtures/layout.locals.error.pug b/src/test-data/pug/test/fixtures/layout.locals.error.pug new file mode 100644 index 0000000..bd604a9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/layout.locals.error.pug @@ -0,0 +1,2 @@ + += foo() \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/layout.pug b/src/test-data/pug/test/fixtures/layout.pug new file mode 100644 index 0000000..87518e5 --- /dev/null +++ b/src/test-data/pug/test/fixtures/layout.pug @@ -0,0 +1,6 @@ +doctype html +html + head + block title + body + block body \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/layout.syntax.error.pug b/src/test-data/pug/test/fixtures/layout.syntax.error.pug new file mode 100644 index 0000000..8b0542a --- /dev/null +++ b/src/test-data/pug/test/fixtures/layout.syntax.error.pug @@ -0,0 +1,2 @@ + += foo( \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/layout.with.runtime.error.pug b/src/test-data/pug/test/fixtures/layout.with.runtime.error.pug new file mode 100644 index 0000000..73d3a0d --- /dev/null +++ b/src/test-data/pug/test/fixtures/layout.with.runtime.error.pug @@ -0,0 +1,5 @@ +html + body + = foo.length + block content + diff --git a/src/test-data/pug/test/fixtures/mixin-include.pug b/src/test-data/pug/test/fixtures/mixin-include.pug new file mode 100644 index 0000000..491fc70 --- /dev/null +++ b/src/test-data/pug/test/fixtures/mixin-include.pug @@ -0,0 +1,5 @@ +mixin bang + +foo + +mixin foo + p bar \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/mixin.error.pug b/src/test-data/pug/test/fixtures/mixin.error.pug new file mode 100644 index 0000000..5a4fdf4 --- /dev/null +++ b/src/test-data/pug/test/fixtures/mixin.error.pug @@ -0,0 +1,2 @@ +mixin mixin-with-error(foo) + - foo.length diff --git a/src/test-data/pug/test/fixtures/multi-append-prepend-block/redefine.pug b/src/test-data/pug/test/fixtures/multi-append-prepend-block/redefine.pug new file mode 100644 index 0000000..abc178e --- /dev/null +++ b/src/test-data/pug/test/fixtures/multi-append-prepend-block/redefine.pug @@ -0,0 +1,5 @@ +extends root.pug + +block content + .content + | Defined content diff --git a/src/test-data/pug/test/fixtures/multi-append-prepend-block/root.pug b/src/test-data/pug/test/fixtures/multi-append-prepend-block/root.pug new file mode 100644 index 0000000..8e3334a --- /dev/null +++ b/src/test-data/pug/test/fixtures/multi-append-prepend-block/root.pug @@ -0,0 +1,5 @@ +block content + | default content + +block head + script(src='/app.js') \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/perf.pug b/src/test-data/pug/test/fixtures/perf.pug new file mode 100644 index 0000000..9aa454b --- /dev/null +++ b/src/test-data/pug/test/fixtures/perf.pug @@ -0,0 +1,32 @@ +.data + ol.sortable#contents + each item in report + if (!item.parent) + div + li.chapter(data-ref= item.id) + a(href='/admin/report/detail/' + item.id) + = item.name + - var chp = item.id + ol.sortable + each item in report + if (item.parent === chp && item.type === 'section') + div + li.section(data-ref= item.id) + a(href='/admin/report/detail/' + item.id) + = item.name + - var sec = item.id + ol.sortable + each item in report + if (item.parent === sec && item.type === 'page') + div + li.page(data-ref= item.id) + a(href='/admin/report/detail/' + item.id) + = item.name + - var page = item.id + ol.sortable + each item in report + if (item.parent === page && item.type === 'subpage') + div + li.subpage(data-ref= item.id) + a(href='/admin/report/detail/' + item.id) + = item.name \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/app-layout.pug b/src/test-data/pug/test/fixtures/prepend-without-block/app-layout.pug new file mode 100644 index 0000000..53f89ba --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend-without-block/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout.pug + +prepend head + script(src='app.js') diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/layout.pug b/src/test-data/pug/test/fixtures/prepend-without-block/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend-without-block/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/page.html b/src/test-data/pug/test/fixtures/prepend-without-block/page.html new file mode 100644 index 0000000..8753a42 --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend-without-block/page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/fixtures/prepend-without-block/page.pug b/src/test-data/pug/test/fixtures/prepend-without-block/page.pug new file mode 100644 index 0000000..6b9bb01 --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend-without-block/page.pug @@ -0,0 +1,6 @@ + +extends app-layout.pug + +prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/fixtures/prepend/app-layout.pug b/src/test-data/pug/test/fixtures/prepend/app-layout.pug new file mode 100644 index 0000000..7040eec --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend/app-layout.pug @@ -0,0 +1,5 @@ + +extends layout.pug + +block prepend head + script(src='app.js') diff --git a/src/test-data/pug/test/fixtures/prepend/layout.pug b/src/test-data/pug/test/fixtures/prepend/layout.pug new file mode 100644 index 0000000..671b3c9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend/layout.pug @@ -0,0 +1,7 @@ + +html + block head + script(src='vendor/jquery.js') + script(src='vendor/caustic.js') + body + block body \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/prepend/page.html b/src/test-data/pug/test/fixtures/prepend/page.html new file mode 100644 index 0000000..8753a42 --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend/page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test-data/pug/test/fixtures/prepend/page.pug b/src/test-data/pug/test/fixtures/prepend/page.pug new file mode 100644 index 0000000..c2a91c9 --- /dev/null +++ b/src/test-data/pug/test/fixtures/prepend/page.pug @@ -0,0 +1,6 @@ + +extends app-layout.pug + +block prepend head + script(src='foo.js') + script(src='bar.js') diff --git a/src/test-data/pug/test/fixtures/runtime.error.pug b/src/test-data/pug/test/fixtures/runtime.error.pug new file mode 100644 index 0000000..27794a4 --- /dev/null +++ b/src/test-data/pug/test/fixtures/runtime.error.pug @@ -0,0 +1 @@ +-foo() \ No newline at end of file diff --git a/src/test-data/pug/test/fixtures/runtime.layout.error.pug b/src/test-data/pug/test/fixtures/runtime.layout.error.pug new file mode 100644 index 0000000..fc66a78 --- /dev/null +++ b/src/test-data/pug/test/fixtures/runtime.layout.error.pug @@ -0,0 +1,3 @@ +extends layout.with.runtime.error.pug +block content + | some content diff --git a/src/test-data/pug/test/fixtures/runtime.with.mixin.error.pug b/src/test-data/pug/test/fixtures/runtime.with.mixin.error.pug new file mode 100644 index 0000000..4226433 --- /dev/null +++ b/src/test-data/pug/test/fixtures/runtime.with.mixin.error.pug @@ -0,0 +1,3 @@ +include mixin.error.pug + ++mixin-with-error(null) diff --git a/src/test-data/pug/test/fixtures/scripts.pug b/src/test-data/pug/test/fixtures/scripts.pug new file mode 100644 index 0000000..30fabcf --- /dev/null +++ b/src/test-data/pug/test/fixtures/scripts.pug @@ -0,0 +1,2 @@ +script(src='/jquery.js') +script(src='/caustic.js') \ No newline at end of file diff --git a/src/test-data/pug/test/markdown-it/comment.md b/src/test-data/pug/test/markdown-it/comment.md new file mode 100644 index 0000000..b84a7bb --- /dev/null +++ b/src/test-data/pug/test/markdown-it/comment.md @@ -0,0 +1 @@ +

    Hello World!

    \ No newline at end of file diff --git a/src/test-data/pug/test/markdown-it/index.test.js b/src/test-data/pug/test/markdown-it/index.test.js new file mode 100644 index 0000000..00bef9f --- /dev/null +++ b/src/test-data/pug/test/markdown-it/index.test.js @@ -0,0 +1,13 @@ +const pug = require('../../'); + +test('inline and include markdow-it should match ', () => { + const outputMarkdownInline = pug.renderFile( + __dirname + '/layout-markdown-inline.pug' + ); + + const outputMarkdownIncludes = pug.renderFile( + __dirname + '/layout-markdown-include.pug' + ); + + expect(outputMarkdownIncludes).toEqual(outputMarkdownInline); +}); diff --git a/src/test-data/pug/test/markdown-it/layout-markdown-include.pug b/src/test-data/pug/test/markdown-it/layout-markdown-include.pug new file mode 100644 index 0000000..6a776da --- /dev/null +++ b/src/test-data/pug/test/markdown-it/layout-markdown-include.pug @@ -0,0 +1 @@ +include:markdown-it(html=true) comment.md \ No newline at end of file diff --git a/src/test-data/pug/test/markdown-it/layout-markdown-inline.pug b/src/test-data/pug/test/markdown-it/layout-markdown-inline.pug new file mode 100644 index 0000000..ee46c9c --- /dev/null +++ b/src/test-data/pug/test/markdown-it/layout-markdown-inline.pug @@ -0,0 +1,2 @@ +:markdown-it(html=true) +

    Hello World!

    \ No newline at end of file diff --git a/src/test-data/pug/test/output-es2015/attr.html b/src/test-data/pug/test/output-es2015/attr.html new file mode 100644 index 0000000..63a82c0 --- /dev/null +++ b/src/test-data/pug/test/output-es2015/attr.html @@ -0,0 +1,2 @@ + +
    \ No newline at end of file diff --git a/src/test-data/pug/test/plugins.test.js b/src/test-data/pug/test/plugins.test.js new file mode 100644 index 0000000..21447bc --- /dev/null +++ b/src/test-data/pug/test/plugins.test.js @@ -0,0 +1,18 @@ +const pug = require('../'); + +test('#3295 - lexer plugins should be used in tag interpolation', () => { + const lex = { + advance(lexer) { + if ('~' === lexer.input.charAt(0)) { + lexer.tokens.push(lexer.tok('text', 'twiddle-dee-dee')); + lexer.consume(1); + lexer.incrementColumn(1); + return true; + } + }, + }; + const input = 'p Look at #[~]'; + const expected = '

    Look at twiddle-dee-dee

    '; + const output = pug.render(input, {plugins: [{lex}]}); + expect(output).toEqual(expected); +}); diff --git a/src/test-data/pug/test/pug.test.js b/src/test-data/pug/test/pug.test.js new file mode 100644 index 0000000..f688949 --- /dev/null +++ b/src/test-data/pug/test/pug.test.js @@ -0,0 +1,1510 @@ +'use strict'; + +var assert = require('assert'); +var fs = require('fs'); +var path = require('path'); +var pug = require('../'); + +var perfTest = fs.readFileSync(__dirname + '/fixtures/perf.pug', 'utf8'); + +try { + fs.mkdirSync(__dirname + '/temp'); +} catch (ex) { + if (ex.code !== 'EEXIST') { + throw ex; + } +} + +describe('pug', function() { + describe('unit tests with .render()', function() { + it('should support doctypes', function() { + assert.equal( + '', + pug.render('doctype xml') + ); + assert.equal('', pug.render('doctype html')); + assert.equal('', pug.render('doctype foo bar baz')); + assert.equal('', pug.render('doctype html')); + assert.equal('', pug.render('doctype', {doctype: 'html'})); + assert.equal( + '', + pug.render('doctype html', {doctype: 'xml'}) + ); + assert.equal('', pug.render('html')); + assert.equal( + '', + pug.render('html', {doctype: 'html'}) + ); + assert.equal( + '', + pug.render('doctype html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN') + ); + }); + + it('should support Buffers', function() { + assert.equal('

    foo

    ', pug.render(Buffer.from('p foo'))); + }); + + it('should support line endings', function() { + var src = ['p', 'div', 'img']; + + var html = ['

    ', '
    ', ''].join(''); + + assert.equal(html, pug.render(src.join('\n'))); + assert.equal(html, pug.render(src.join('\r'))); + assert.equal(html, pug.render(src.join('\r\n'))); + + html = ['

    ', '
    ', ''].join(''); + + assert.equal(html, pug.render(src.join('\n'), {doctype: 'html'})); + assert.equal(html, pug.render(src.join('\r'), {doctype: 'html'})); + assert.equal(html, pug.render(src.join('\r\n'), {doctype: 'html'})); + }); + + it('should support single quotes', function() { + assert.equal("

    'foo'

    ", pug.render("p 'foo'")); + assert.equal("

    'foo'

    ", pug.render("p\n | 'foo'")); + assert.equal( + '', + pug.render("- var path = 'foo';\na(href='/' + path)") + ); + }); + + it('should support block-expansion', function() { + assert.equal( + '
  • foo
  • bar
  • baz
  • ', + pug.render('li: a foo\nli: a bar\nli: a baz') + ); + assert.equal( + '
  • foo
  • bar
  • baz
  • ', + pug.render('li.first: a foo\nli: a bar\nli: a baz') + ); + assert.equal( + '
    baz
    ', + pug.render('.foo: .bar baz') + ); + }); + + it('should support tags', function() { + var str = ['p', 'div', 'img', 'br/'].join('\n'); + + var html = ['

    ', '
    ', '', '
    '].join(''); + + assert.equal(html, pug.render(str), 'Test basic tags'); + assert.equal( + '', + pug.render('fb:foo-bar'), + 'Test hyphens' + ); + assert.equal( + '
    ', + pug.render('div.something'), + 'Test classes' + ); + assert.equal( + '
    ', + pug.render('div#something'), + 'Test ids' + ); + assert.equal( + '
    ', + pug.render('.something'), + 'Test stand-alone classes' + ); + assert.equal( + '
    ', + pug.render('#something'), + 'Test stand-alone ids' + ); + assert.equal('
    ', pug.render('#foo.bar')); + assert.equal('
    ', pug.render('.bar#foo')); + assert.equal( + '
    ', + pug.render('div#foo(class="bar")') + ); + assert.equal( + '
    ', + pug.render('div(class="bar")#foo') + ); + assert.equal( + '
    ', + pug.render('div(id="bar").foo') + ); + assert.equal( + '
    ', + pug.render('div.foo.bar.baz') + ); + assert.equal( + '
    ', + pug.render('div(class="foo").bar.baz') + ); + assert.equal( + '
    ', + pug.render('div.foo(class="bar").baz') + ); + assert.equal( + '
    ', + pug.render('div.foo.bar(class="baz")') + ); + assert.equal('
    ', pug.render('div.a-b2')); + assert.equal('
    ', pug.render('div.a_b2')); + assert.equal('', pug.render('fb:user')); + assert.equal('', pug.render('fb:user:role')); + assert.equal( + '', + pug.render('colgroup\n col.test') + ); + }); + + it('should support nested tags', function() { + var str = [ + 'ul', + ' li a', + ' li b', + ' li', + ' ul', + ' li c', + ' li d', + ' li e', + ].join('\n'); + + var html = [ + '
      ', + '
    • a
    • ', + '
    • b
    • ', + '
      • c
      • d
    • ', + '
    • e
    • ', + '
    ', + ].join(''); + + assert.equal(html, pug.render(str)); + + var str = ['a(href="#")', ' | foo ', ' | bar ', ' | baz'].join('\n'); + + assert.equal('foo \nbar \nbaz', pug.render(str)); + + var str = ['ul', ' li one', ' ul', ' | two', ' li three'].join( + '\n' + ); + + var html = [ + '
      ', + '
    • one
    • ', + '
        two', + '
      • three
      • ', + '
      ', + '
    ', + ].join(''); + + assert.equal(html, pug.render(str)); + }); + + it('should support variable length newlines', function() { + var str = [ + 'ul', + ' li a', + ' ', + ' li b', + ' ', + ' ', + ' li', + ' ul', + ' li c', + '', + ' li d', + ' li e', + ].join('\n'); + + var html = [ + '
      ', + '
    • a
    • ', + '
    • b
    • ', + '
      • c
      • d
    • ', + '
    • e
    • ', + '
    ', + ].join(''); + + assert.equal(html, pug.render(str)); + }); + + it('should support tab conversion', function() { + var str = [ + 'ul', + '\tli a', + '\t', + '\tli b', + '\t\t', + '\t\t\t\t\t\t', + '\tli', + '\t\tul', + '\t\t\tli c', + '', + '\t\t\tli d', + '\tli e', + ].join('\n'); + + var html = [ + '
      ', + '
    • a
    • ', + '
    • b
    • ', + '
      • c
      • d
    • ', + '
    • e
    • ', + '
    ', + ].join(''); + + assert.equal(html, pug.render(str)); + }); + + it('should support newlines', function() { + var str = [ + 'ul', + ' li a', + ' ', + ' ', + '', + ' ', + ' li b', + ' li', + ' ', + ' ', + ' ', + ' ul', + ' ', + ' li c', + ' li d', + ' li e', + ].join('\n'); + + var html = [ + '
      ', + '
    • a
    • ', + '
    • b
    • ', + '
      • c
      • d
    • ', + '
    • e
    • ', + '
    ', + ].join(''); + + assert.equal(html, pug.render(str)); + + var str = [ + 'html', + ' ', + ' head', + ' != "test"', + ' ', + ' ', + ' ', + ' body', + ].join('\n'); + + var html = [ + '', + '', + 'test', + '', + '', + '', + ].join(''); + + assert.equal(html, pug.render(str)); + assert.equal( + 'something', + pug.render('foo\n= "something"\nbar') + ); + assert.equal( + 'somethingelse', + pug.render('foo\n= "something"\nbar\n= "else"') + ); + }); + + it('should support text', function() { + assert.equal('foo\nbar\nbaz', pug.render('| foo\n| bar\n| baz')); + assert.equal('foo \nbar \nbaz', pug.render('| foo \n| bar \n| baz')); + assert.equal('(hey)', pug.render('| (hey)')); + assert.equal('some random text', pug.render('| some random text')); + assert.equal(' foo', pug.render('| foo')); + assert.equal(' foo ', pug.render('| foo ')); + assert.equal(' foo \n bar ', pug.render('| foo \n| bar ')); + }); + + it('should support pipe-less text', function() { + assert.equal( + '
    ', + pug.render('pre\n code\n foo\n\n bar') + ); + assert.equal('

    foo\n\nbar

    ', pug.render('p.\n foo\n\n bar')); + assert.equal( + '

    foo\n\n\n\nbar

    ', + pug.render('p.\n foo\n\n\n\n bar') + ); + assert.equal( + '

    foo\n bar\nfoo

    ', + pug.render('p.\n foo\n bar\n foo') + ); + assert.equal( + '', + pug.render('script.\n s.parentNode.insertBefore(g,s)\n') + ); + assert.equal( + '', + pug.render('script.\n s.parentNode.insertBefore(g,s)') + ); + }); + + it('should support tag text', function() { + assert.equal('

    some random text

    ', pug.render('p some random text')); + assert.equal( + '

    clickGoogle.

    ', + pug.render('p\n | click\n a Google\n | .') + ); + assert.equal('

    (parens)

    ', pug.render('p (parens)')); + assert.equal( + '

    (parens)

    ', + pug.render('p(foo="bar") (parens)') + ); + assert.equal( + '', + pug.render('option(value="") -- (optional) foo --') + ); + }); + + it('should support tag text block', function() { + assert.equal( + '

    foo \nbar \nbaz

    ', + pug.render('p\n | foo \n | bar \n | baz') + ); + assert.equal( + '', + pug.render('label\n | Password:\n input') + ); + assert.equal( + '', + pug.render('label Password:\n input') + ); + }); + + it('should support tag text interpolation', function() { + assert.equal( + 'yo, pug is cool', + pug.render('| yo, #{name} is cool\n', {name: 'pug'}) + ); + assert.equal( + '

    yo, pug is cool

    ', + pug.render('p yo, #{name} is cool', {name: 'pug'}) + ); + assert.equal( + 'yo, pug is cool', + pug.render('| yo, #{name || "pug"} is cool', {name: null}) + ); + assert.equal( + "yo, 'pug' is cool", + pug.render('| yo, #{name || "\'pug\'"} is cool', {name: null}) + ); + assert.equal( + 'foo <script> bar', + pug.render('| foo #{code} bar', {code: '', + '', + '', + ].join(''); + + assert.equal(html, pug.render(str)); + }); + + it('should support comments', function() { + // Regular + var str = ['//foo', 'p bar'].join('\n'); + + var html = ['', '

    bar

    '].join(''); + + assert.equal(html, pug.render(str)); + + // Between tags + + var str = ['p foo', '// bar ', 'p baz'].join('\n'); + + var html = ['

    foo

    ', '', '

    baz

    '].join(''); + + assert.equal(html, pug.render(str)); + + // Quotes + + var str = "", + js = "// script(src: '/js/validate.js') "; + assert.equal(str, pug.render(js)); + }); + + it('should support unbuffered comments', function() { + var str = ['//- foo', 'p bar'].join('\n'); + + var html = ['

    bar

    '].join(''); + + assert.equal(html, pug.render(str)); + + var str = ['p foo', '//- bar ', 'p baz'].join('\n'); + + var html = ['

    foo

    ', '

    baz

    '].join(''); + + assert.equal(html, pug.render(str)); + }); + + it('should support literal html', function() { + assert.equal( + '', + pug.render('') + ); + }); + + it('should support code', function() { + assert.equal('test', pug.render('!= "test"')); + assert.equal('test', pug.render('= "test"')); + assert.equal('test', pug.render('- var foo = "test"\n=foo')); + assert.equal( + 'footestbar', + pug.render('- var foo = "test"\n| foo\nem= foo\n| bar') + ); + assert.equal( + 'test

    something

    ', + pug.render('!= "test"\nh2 something') + ); + + var str = ['- var foo = "', + pug.render(str, {filename: __dirname + '/pug.test.js'}) + ); + }); + + it('should not fail on js newlines', function() { + assert.equal('

    foo\u2028bar

    ', pug.render('p foo\u2028bar')); + assert.equal('

    foo\u2029bar

    ', pug.render('p foo\u2029bar')); + }); + + it('should display error line number correctly up to token level', function() { + var str = [ + 'p.', + ' Lorem ipsum dolor sit amet, consectetur', + ' adipisicing elit, sed do eiusmod tempor', + ' incididunt ut labore et dolore magna aliqua.', + 'p.', + ' Ut enim ad minim veniam, quis nostrud', + ' exercitation ullamco laboris nisi ut aliquip', + ' ex ea commodo consequat.', + 'p.', + ' Duis aute irure dolor in reprehenderit', + ' in voluptate velit esse cillum dolore eu', + ' fugiat nulla pariatur.', + 'a(href="#" Next', + ].join('\n'); + var errorLocation = function(str) { + try { + pug.render(str); + } catch (err) { + return err.message.split('\n')[0]; + } + }; + assert.equal(errorLocation(str), 'Pug:13:16'); + }); + }); + + describe('.compileFile()', function() { + it('does not produce warnings for issue-1593', function() { + pug.compileFile(__dirname + '/fixtures/issue-1593/index.pug'); + }); + it('should support caching (pass 1)', function() { + fs.writeFileSync(__dirname + '/temp/input-compileFile.pug', '.foo bar'); + var fn = pug.compileFile(__dirname + '/temp/input-compileFile.pug', { + cache: true, + }); + var expected = '
    bar
    '; + assert(fn() === expected); + }); + it('should support caching (pass 2)', function() { + // Poison the input file + fs.writeFileSync( + __dirname + '/temp/input-compileFile.pug', + '.big fat hen' + ); + var fn = pug.compileFile(__dirname + '/temp/input-compileFile.pug', { + cache: true, + }); + var expected = '
    bar
    '; + assert(fn() === expected); + }); + }); + + describe('.render()', function() { + it('should support .pug.render(str, fn)', function() { + pug.render('p foo bar', function(err, str) { + assert.ok(!err); + assert.equal('

    foo bar

    ', str); + }); + }); + + it('should support .pug.render(str, options, fn)', function() { + pug.render('p #{foo}', {foo: 'bar'}, function(err, str) { + assert.ok(!err); + assert.equal('

    bar

    ', str); + }); + }); + + it('should support .pug.render(str, options, fn) cache', function() { + pug.render('p bar', {cache: true}, function(err, str) { + assert.ok( + /the "filename" option is required for caching/.test(err.message) + ); + }); + + pug.render('p foo bar', {cache: true, filename: 'test'}, function( + err, + str + ) { + assert.ok(!err); + assert.equal('

    foo bar

    ', str); + }); + }); + }); + + describe('.compile()', function() { + it('should support .compile()', function() { + var fn = pug.compile('p foo'); + assert.equal('

    foo

    ', fn()); + }); + + it('should support .compile() locals', function() { + var fn = pug.compile('p= foo'); + assert.equal('

    bar

    ', fn({foo: 'bar'})); + }); + + it("should support .compile() locals in 'self' hash", function() { + var fn = pug.compile('p= self.foo', {self: true}); + assert.equal('

    bar

    ', fn({foo: 'bar'})); + }); + + it('should support .compile() no debug', function() { + var fn = pug.compile('p foo\np #{bar}', {compileDebug: false}); + assert.equal('

    foo

    baz

    ', fn({bar: 'baz'})); + }); + + it('should support .compile() no debug and global helpers', function() { + var fn = pug.compile('p foo\np #{bar}', { + compileDebug: false, + helpers: 'global', + }); + assert.equal('

    foo

    baz

    ', fn({bar: 'baz'})); + }); + + it('should be reasonably fast', function() { + pug.compile(perfTest, {}); + }); + it('allows trailing space (see #1586)', function() { + var res = pug.render('ul \n li An Item'); + assert.equal('
    • An Item
    ', res); + }); + }); + + describe('.compileClient()', function() { + it('should support pug.compileClient(str)', function() { + var src = fs.readFileSync(__dirname + '/cases/basic.pug'); + var expected = fs + .readFileSync(__dirname + '/cases/basic.html', 'utf8') + .replace(/\s/g, ''); + var fn = pug.compileClient(src); + fn = Function('pug', fn.toString() + '\nreturn template;')(pug.runtime); + var actual = fn({name: 'foo'}).replace(/\s/g, ''); + expect(actual).toBe(expected); + }); + it('should support pug.compileClient(str, options)', function() { + var src = '.bar= self.foo'; + var fn = pug.compileClient(src, {self: true}); + fn = Function('pug', fn.toString() + '\nreturn template;')(pug.runtime); + var actual = fn({foo: 'baz'}); + expect(actual).toBe('
    baz
    '); + }); + it('should support module syntax in pug.compileClient(str, options) when inlineRuntimeFunctions it true', function() { + var src = '.bar= self.foo'; + var fn = pug.compileClient(src, { + self: true, + module: true, + inlineRuntimeFunctions: true, + }); + expect(fn).toMatchSnapshot(); + fs.writeFileSync( + __dirname + '/temp/input-compileModuleFileClient.js', + fn + ); + var fn = require(__dirname + '/temp/input-compileModuleFileClient.js'); + expect(fn({foo: 'baz'})).toBe('
    baz
    '); + }); + it('should support module syntax in pug.compileClient(str, options) when inlineRuntimeFunctions it false', function() { + var src = '.bar= self.foo'; + var fn = pug.compileClient(src, { + self: true, + module: true, + inlineRuntimeFunctions: false, + }); + expect(fn).toMatchSnapshot(); + fs.writeFileSync( + __dirname + '/temp/input-compileModuleFileClient.js', + fn + ); + var fn = require(__dirname + '/temp/input-compileModuleFileClient.js'); + expect(fn({foo: 'baz'})).toBe('
    baz
    '); + }); + }); + + describe('.renderFile()', function() { + it('will synchronously return a string', function() { + var expected = fs + .readFileSync(__dirname + '/cases/basic.html', 'utf8') + .replace(/\s/g, ''); + var actual = pug + .renderFile(__dirname + '/cases/basic.pug', {name: 'foo'}) + .replace(/\s/g, ''); + assert(actual === expected); + }); + it('when given a callback, it calls that rather than returning', function(done) { + var expected = fs + .readFileSync(__dirname + '/cases/basic.html', 'utf8') + .replace(/\s/g, ''); + pug.renderFile(__dirname + '/cases/basic.pug', {name: 'foo'}, function( + err, + actual + ) { + if (err) return done(err); + assert(actual.replace(/\s/g, '') === expected); + done(); + }); + }); + it('when given a callback, it calls that rather than returning even if there are no options', function(done) { + var expected = fs + .readFileSync(__dirname + '/cases/basic.html', 'utf8') + .replace(/\s/g, ''); + pug.renderFile(__dirname + '/cases/basic.pug', function(err, actual) { + if (err) return done(err); + assert(actual.replace(/\s/g, '') === expected); + done(); + }); + }); + it('when given a callback, it calls that with any errors', function(done) { + pug.renderFile(__dirname + '/fixtures/runtime.error.pug', function( + err, + actual + ) { + assert.ok(err); + done(); + }); + }); + it('should support caching (pass 1)', function(done) { + fs.writeFileSync(__dirname + '/temp/input-renderFile.pug', '.foo bar'); + pug.renderFile( + __dirname + '/temp/input-renderFile.pug', + {cache: true}, + function(err, actual) { + if (err) return done(err); + assert.equal('
    bar
    ', actual); + done(); + } + ); + }); + it('should support caching (pass 2)', function(done) { + // Poison the input file + fs.writeFileSync( + __dirname + '/temp/input-renderFile.pug', + '.big fat hen' + ); + pug.renderFile( + __dirname + '/temp/input-renderFile.pug', + {cache: true}, + function(err, actual) { + if (err) return done(err); + assert.equal('
    bar
    ', actual); + done(); + } + ); + }); + }); + + describe('.compileFileClient(path, options)', function() { + it('returns a string form of a function called `template`', function() { + var src = pug.compileFileClient(__dirname + '/cases/basic.pug'); + var expected = fs + .readFileSync(__dirname + '/cases/basic.html', 'utf8') + .replace(/\s/g, ''); + var fn = Function('pug', src + '\nreturn template;')(pug.runtime); + var actual = fn({name: 'foo'}).replace(/\s/g, ''); + assert(actual === expected); + }); + it('accepts the `name` option to rename the resulting function', function() { + var src = pug.compileFileClient(__dirname + '/cases/basic.pug', { + name: 'myTemplateName', + }); + var expected = fs + .readFileSync(__dirname + '/cases/basic.html', 'utf8') + .replace(/\s/g, ''); + var fn = Function('pug', src + '\nreturn myTemplateName;')(pug.runtime); + var actual = fn({name: 'foo'}).replace(/\s/g, ''); + assert(actual === expected); + }); + it('should support caching (pass 1)', function() { + fs.writeFileSync( + __dirname + '/temp/input-compileFileClient.pug', + '.foo bar' + ); + var src = pug.compileFileClient( + __dirname + '/temp/input-compileFileClient.pug', + {name: 'myTemplateName', cache: true} + ); + var expected = '
    bar
    '; + var fn = Function('pug', src + '\nreturn myTemplateName;')(pug.runtime); + assert(fn() === expected); + }); + it('should support caching (pass 2)', function() { + // Poison the input file + fs.writeFileSync( + __dirname + '/temp/input-compileFileClient.pug', + '.big fat hen' + ); + var src = pug.compileFileClient( + __dirname + '/temp/input-compileFileClient.pug', + {name: 'myTemplateName', cache: true} + ); + var expected = '
    bar
    '; + var fn = Function('pug', src + '\nreturn myTemplateName;')(pug.runtime); + assert(fn() === expected); + }); + }); + + describe('.runtime', function() { + describe('.merge', function() { + it('merges two attribute objects, giving precedensce to the second object', function() { + assert.deepEqual( + pug.runtime.merge({}, {class: ['foo', 'bar'], foo: 'bar'}), + {class: ['foo', 'bar'], foo: 'bar'} + ); + assert.deepEqual( + pug.runtime.merge( + {class: ['foo'], foo: 'baz'}, + {class: ['bar'], foo: 'bar'} + ), + {class: ['foo', 'bar'], foo: 'bar'} + ); + assert.deepEqual( + pug.runtime.merge({class: ['foo', 'bar'], foo: 'bar'}, {}), + {class: ['foo', 'bar'], foo: 'bar'} + ); + }); + }); + describe('.attrs', function() { + it('Renders the given attributes object', function() { + assert.equal(pug.runtime.attrs({}), ''); + assert.equal(pug.runtime.attrs({class: []}), ''); + assert.equal(pug.runtime.attrs({class: ['foo']}), ' class="foo"'); + assert.equal( + pug.runtime.attrs({class: ['foo'], id: 'bar'}), + ' class="foo" id="bar"' + ); + }); + }); + }); + + describe('filter indentation', function() { + it('is maintained', function() { + var filters = { + indents: function(str) { + return str + .split(/\n/) + .map(function(line) { + return line.match(/^ */)[0].length; + }) + .join(','); + }, + }; + + var indents = [ + ':indents', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ' x', + ].join('\n'); + + assert.equal( + pug.render(indents, {filters: filters}), + '0,1,2,3,0,4,4,3,3,4,2,0,2,0,1' + ); + }); + }); + + describe('.compile().dependencies', function() { + it('should list the filename of the template referenced by extends', function() { + var filename = __dirname + '/dependencies/extends1.pug'; + var str = fs.readFileSync(filename, 'utf8'); + var info = pug.compile(str, {filename: filename}); + assert.deepEqual( + [path.resolve(__dirname + '/dependencies/dependency1.pug')], + info.dependencies + ); + }); + it('should list the filename of the template referenced by an include', function() { + var filename = __dirname + '/dependencies/include1.pug'; + var str = fs.readFileSync(filename, 'utf8'); + var info = pug.compile(str, {filename: filename}); + assert.deepEqual( + [path.resolve(__dirname + '/dependencies/dependency1.pug')], + info.dependencies + ); + }); + it('should list the dependencies of extends dependencies', function() { + var filename = __dirname + '/dependencies/extends2.pug'; + var str = fs.readFileSync(filename, 'utf8'); + var info = pug.compile(str, {filename: filename}); + assert.deepEqual( + [ + path.resolve(__dirname + '/dependencies/dependency2.pug'), + path.resolve(__dirname + '/dependencies/dependency3.pug'), + ], + info.dependencies + ); + }); + it('should list the dependencies of include dependencies', function() { + var filename = __dirname + '/dependencies/include2.pug'; + var str = fs.readFileSync(filename, 'utf8'); + var info = pug.compile(str, {filename: filename}); + assert.deepEqual( + [ + path.resolve(__dirname + '/dependencies/dependency2.pug'), + path.resolve(__dirname + '/dependencies/dependency3.pug'), + ], + info.dependencies + ); + }); + }); + + describe('.name', function() { + it('should have a name attribute', function() { + assert.strictEqual(pug.name, 'Pug'); + }); + }); +}); diff --git a/src/test-data/pug/test/regression-2436/__snapshots__/index.test.js.snap b/src/test-data/pug/test/regression-2436/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..142ec67 --- /dev/null +++ b/src/test-data/pug/test/regression-2436/__snapshots__/index.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#2436 - block with a same name extends from different layout in nesting 1`] = ` +" +

    layout

    +

    Main A

    +

    other layout

    +

    Other A

    " +`; + +exports[`#2436 - block with a same name extends from the same layout in nesting 1`] = ` +" +

    layout

    +

    Main A

    +

    layout

    +

    Other A

    " +`; diff --git a/src/test-data/pug/test/regression-2436/index.test.js b/src/test-data/pug/test/regression-2436/index.test.js new file mode 100644 index 0000000..d9647b8 --- /dev/null +++ b/src/test-data/pug/test/regression-2436/index.test.js @@ -0,0 +1,11 @@ +const pug = require('../../'); + +test('#2436 - block with a same name extends from the same layout in nesting', () => { + const output = pug.renderFile(__dirname + '/issue1.pug', {pretty: true}); + expect(output).toMatchSnapshot(); +}); + +test('#2436 - block with a same name extends from different layout in nesting', () => { + const output = pug.renderFile(__dirname + '/issue2.pug', {pretty: true}); + expect(output).toMatchSnapshot(); +}); diff --git a/src/test-data/pug/test/regression-2436/issue1.pug b/src/test-data/pug/test/regression-2436/issue1.pug new file mode 100644 index 0000000..f521c6e --- /dev/null +++ b/src/test-data/pug/test/regression-2436/issue1.pug @@ -0,0 +1,7 @@ +extends layout.pug + +block a + p Main A + +block b + include other1.pug diff --git a/src/test-data/pug/test/regression-2436/issue2.pug b/src/test-data/pug/test/regression-2436/issue2.pug new file mode 100644 index 0000000..7acba59 --- /dev/null +++ b/src/test-data/pug/test/regression-2436/issue2.pug @@ -0,0 +1,7 @@ +extends layout.pug + +block a + p Main A + +block b + include other2.pug diff --git a/src/test-data/pug/test/regression-2436/layout.pug b/src/test-data/pug/test/regression-2436/layout.pug new file mode 100644 index 0000000..71ad7eb --- /dev/null +++ b/src/test-data/pug/test/regression-2436/layout.pug @@ -0,0 +1,6 @@ +h1 layout + +block a + p block in layout + +block b diff --git a/src/test-data/pug/test/regression-2436/other1.pug b/src/test-data/pug/test/regression-2436/other1.pug new file mode 100644 index 0000000..206cb4f --- /dev/null +++ b/src/test-data/pug/test/regression-2436/other1.pug @@ -0,0 +1,4 @@ +extends layout.pug + +block a + p Other A diff --git a/src/test-data/pug/test/regression-2436/other2.pug b/src/test-data/pug/test/regression-2436/other2.pug new file mode 100644 index 0000000..6329aeb --- /dev/null +++ b/src/test-data/pug/test/regression-2436/other2.pug @@ -0,0 +1,4 @@ +extends other_layout.pug + +block a + p Other A diff --git a/src/test-data/pug/test/regression-2436/other_layout.pug b/src/test-data/pug/test/regression-2436/other_layout.pug new file mode 100644 index 0000000..edda161 --- /dev/null +++ b/src/test-data/pug/test/regression-2436/other_layout.pug @@ -0,0 +1,4 @@ +h1 other layout + +block a + p block in other layout diff --git a/src/test-data/pug/test/run-es2015.test.js b/src/test-data/pug/test/run-es2015.test.js new file mode 100644 index 0000000..0ee3d35 --- /dev/null +++ b/src/test-data/pug/test/run-es2015.test.js @@ -0,0 +1,21 @@ +'use strict'; + +const fs = require('fs'); +const assert = require('assert'); +const mkdirp = require('mkdirp').sync; +const runUtils = require('./run-utils'); +const pug = require('../'); + +var cases = runUtils.findCases(__dirname + '/cases'); +var es2015 = runUtils.findCases(__dirname + '/cases-es2015'); + +mkdirp(__dirname + '/output-es2015'); + +describe('test cases for ECMAScript 2015', function() { + try { + eval('``'); + es2015.forEach(runUtils.testSingle.bind(null, it, '-es2015')); + } catch (ex) { + es2015.forEach(runUtils.testSingle.bind(null, it.skip, '-es2015')); + } +}); diff --git a/src/test-data/pug/test/run-syntax-errors.test.js b/src/test-data/pug/test/run-syntax-errors.test.js new file mode 100644 index 0000000..379750c --- /dev/null +++ b/src/test-data/pug/test/run-syntax-errors.test.js @@ -0,0 +1,43 @@ +const assert = require('assert'); +const fs = require('fs'); +const runUtils = require('./run-utils'); +const pug = require('../'); + +const anti = runUtils.findCases(__dirname + '/anti-cases'); + +describe('certain syntax is not allowed and will throw a compile time error', function() { + anti.forEach(function(test) { + var name = test.replace(/[-.]/g, ' '); + it(name, function() { + var path = __dirname.replace(/\\/g, '/') + '/anti-cases/' + test + '.pug'; + var str = fs.readFileSync(path, 'utf8'); + try { + var fn = pug.compile(str, { + filename: path, + pretty: true, + basedir: __dirname + '/anti-cases', + filters: runUtils.filters, + }); + } catch (ex) { + if (!ex.code) { + throw ex; + } + assert(ex instanceof Error, 'Should throw a real Error'); + assert( + ex.code.indexOf('PUG:') === 0, + 'It should have a code of "PUG:SOMETHING"' + ); + assert( + ex.message.replace(/\\/g, '/').indexOf(path) === 0, + 'it should start with the path' + ); + assert( + /:\d+$/m.test(ex.message.replace(/\\/g, '/')), + 'it should include a line number.' + ); + return; + } + throw new Error(test + ' should have thrown an error'); + }); + }); +}); diff --git a/src/test-data/pug/test/run-utils.js b/src/test-data/pug/test/run-utils.js new file mode 100644 index 0000000..2d1eccf --- /dev/null +++ b/src/test-data/pug/test/run-utils.js @@ -0,0 +1,154 @@ +var fs = require('fs'); +var assert = require('assert'); +var pug = require('../'); +var uglify = require('uglify-js'); +var mkdirp = require('mkdirp').sync; + +var filters = { + custom: function(str, options) { + assert(options.opt === 'val'); + assert(options.num === 2); + return 'BEGIN' + str + 'END'; + }, +}; + +// test cases + +function writeFileSync(filename, data) { + try { + if (fs.readFileSync(filename, 'utf8') === data.toString('utf8')) { + return; + } + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + } + fs.writeFileSync(filename, data); +} + +function findCases(dir) { + return fs + .readdirSync(dir) + .filter(function(file) { + return ~file.indexOf('.pug'); + }) + .map(function(file) { + return file.replace('.pug', ''); + }); +} + +function testSingle(it, suffix, test) { + var name = test.replace(/[-.]/g, ' '); + it(name, function() { + var path = __dirname + '/cases' + suffix + '/' + test + '.pug'; + var str = fs.readFileSync(path, 'utf8'); + var fn = pug.compile(str, { + filename: path, + pretty: true, + basedir: __dirname + '/cases' + suffix, + filters: filters, + filterAliases: {markdown: 'markdown-it'}, + }); + var actual = fn({title: 'Pug'}); + + writeFileSync( + __dirname + '/output' + suffix + '/' + test + '.html', + actual + ); + + var html = fs + .readFileSync( + __dirname + '/cases' + suffix + '/' + test + '.html', + 'utf8' + ) + .trim() + .replace(/\r/g, ''); + var clientCode = uglify.minify( + pug.compileClient(str, { + filename: path, + pretty: true, + compileDebug: false, + basedir: __dirname + '/cases' + suffix, + filters: filters, + filterAliases: {markdown: 'markdown-it'}, + }), + { + output: {beautify: true}, + mangle: false, + compress: false, + fromString: true, + } + ).code; + var clientCodeDebug = uglify.minify( + pug.compileClient(str, { + filename: path, + pretty: true, + compileDebug: true, + basedir: __dirname + '/cases' + suffix, + filters: filters, + filterAliases: {markdown: 'markdown-it'}, + }), + { + output: {beautify: true}, + mangle: false, + compress: false, + fromString: true, + } + ).code; + writeFileSync( + __dirname + '/output' + suffix + '/' + test + '.js', + uglify.minify( + pug.compileClient(str, { + filename: path, + pretty: false, + compileDebug: false, + basedir: __dirname + '/cases' + suffix, + filters: filters, + filterAliases: {markdown: 'markdown-it'}, + }), + { + output: {beautify: true}, + mangle: false, + compress: false, + fromString: true, + } + ).code + ); + if (/filter/.test(test)) { + actual = actual.replace(/\n| /g, ''); + html = html.replace(/\n| /g, ''); + } + if (/mixins-unused/.test(test)) { + assert( + /never-called/.test(str), + 'never-called is in the pug file for mixins-unused' + ); + assert( + !/never-called/.test(clientCode), + 'never-called should be removed from the code' + ); + } + expect(actual.trim()).toEqual(html); + actual = Function('pug', clientCode + '\nreturn template;')()({ + title: 'Pug', + }); + if (/filter/.test(test)) { + actual = actual.replace(/\n| /g, ''); + } + expect(actual.trim()).toEqual(html); + actual = Function('pug', clientCodeDebug + '\nreturn template;')()({ + title: 'Pug', + }); + if (/filter/.test(test)) { + actual = actual.replace(/\n| /g, ''); + } + expect(actual.trim()).toEqual(html); + }); +} + +module.exports = { + filters, + findCases, + testSingle, +}; diff --git a/src/test-data/pug/test/run.test.js b/src/test-data/pug/test/run.test.js new file mode 100644 index 0000000..b5c0270 --- /dev/null +++ b/src/test-data/pug/test/run.test.js @@ -0,0 +1,20 @@ +'use strict'; + +// even and odd tests are arbitrarily split because jest is faster that way + +const fs = require('fs'); +const assert = require('assert'); +const mkdirp = require('mkdirp').sync; +const runUtils = require('./run-utils'); +const pug = require('../'); + +var cases = runUtils.findCases(__dirname + '/cases'); +var es2015 = runUtils.findCases(__dirname + '/cases-es2015'); + +mkdirp(__dirname + '/output'); + +describe('test cases', function() { + cases.forEach((test, i) => { + runUtils.testSingle(it, '', test); + }); +}); diff --git a/src/test-data/pug/test/shadowed-block/__snapshots__/index.test.js.snap b/src/test-data/pug/test/shadowed-block/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..43a917e --- /dev/null +++ b/src/test-data/pug/test/shadowed-block/__snapshots__/index.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`layout with shadowed block 1`] = `""`; + +exports[`layout with shadowed block 2`] = `""`; diff --git a/src/test-data/pug/test/shadowed-block/base.pug b/src/test-data/pug/test/shadowed-block/base.pug new file mode 100644 index 0000000..7148cac --- /dev/null +++ b/src/test-data/pug/test/shadowed-block/base.pug @@ -0,0 +1,4 @@ +block root + // base.pug: root + block shadowed + // base.pug: shadowed diff --git a/src/test-data/pug/test/shadowed-block/index.pug b/src/test-data/pug/test/shadowed-block/index.pug new file mode 100644 index 0000000..dabca48 --- /dev/null +++ b/src/test-data/pug/test/shadowed-block/index.pug @@ -0,0 +1,4 @@ +extends ./layout.pug + +block shadowed + // index.pug: shadowed diff --git a/src/test-data/pug/test/shadowed-block/index.test.js b/src/test-data/pug/test/shadowed-block/index.test.js new file mode 100644 index 0000000..97999a8 --- /dev/null +++ b/src/test-data/pug/test/shadowed-block/index.test.js @@ -0,0 +1,10 @@ +const pug = require('../../'); + +test('layout with shadowed block', () => { + const outputWithAjax = pug.renderFile(__dirname + '/index.pug', {ajax: true}); + const outputWithoutAjax = pug.renderFile(__dirname + '/index.pug', { + ajax: false, + }); + expect(outputWithAjax).toMatchSnapshot(); + expect(outputWithoutAjax).toMatchSnapshot(); +}); diff --git a/src/test-data/pug/test/shadowed-block/layout.pug b/src/test-data/pug/test/shadowed-block/layout.pug new file mode 100644 index 0000000..fa27d25 --- /dev/null +++ b/src/test-data/pug/test/shadowed-block/layout.pug @@ -0,0 +1,6 @@ +extends ./base.pug + +block root + // layout.pug: root + block shadowed + // layout.pug: shadowed diff --git a/src/test-data/pug/test/temp/input-compileFile.pug b/src/test-data/pug/test/temp/input-compileFile.pug new file mode 100644 index 0000000..7a3caea --- /dev/null +++ b/src/test-data/pug/test/temp/input-compileFile.pug @@ -0,0 +1 @@ +.big fat hen \ No newline at end of file diff --git a/src/test-data/pug/test/temp/input-compileFileClient.pug b/src/test-data/pug/test/temp/input-compileFileClient.pug new file mode 100644 index 0000000..7a3caea --- /dev/null +++ b/src/test-data/pug/test/temp/input-compileFileClient.pug @@ -0,0 +1 @@ +.big fat hen \ No newline at end of file diff --git a/src/test-data/pug/test/temp/input-renderFile.pug b/src/test-data/pug/test/temp/input-renderFile.pug new file mode 100644 index 0000000..7a3caea --- /dev/null +++ b/src/test-data/pug/test/temp/input-renderFile.pug @@ -0,0 +1 @@ +.big fat hen \ No newline at end of file diff --git a/src/tests/check_list/source.html b/src/tests/check_list/source.html index 1881c0f..4248ef7 100644 --- a/src/tests/check_list/source.html +++ b/src/tests/check_list/source.html @@ -1,6 +1,6 @@ -