") != 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, "");
- } 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("");
- try self.write(tag.tag);
- 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 hhhh; 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 hhhh;
- i += 1;
- if (i >= str.len) return false;
-
- if (str[i] == 'x' or str[i] == 'X') {
- // Hex: hhhh;
- 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, "foo
Hello, 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, "");
+ try output.appendSlice(allocator, name);
+ 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, "");
+ try output.appendSlice(allocator, name);
+ 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 hex;
+ 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("", 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 .
\\\\nWith 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 @@
+contact save
+
+ Foo
+ Bar
+
+contact save
+
+ Foo
+ Bar
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
\ 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 @@
+
\ 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/code.escape.pug b/src/test-data/pug/test/cases/code.escape.pug
new file mode 100644
index 0000000..762c089
--- /dev/null
+++ b/src/test-data/pug/test/cases/code.escape.pug
@@ -0,0 +1,2 @@
+p= '
\ 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 @@
+
+
+
+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
+
+
+
+
+
+
+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 @@
+
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.
+
+
+
+
+
+
+
+
+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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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 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 @@
+
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 @@
+
\ 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 @@
+Username:
+
+
+Password:
+
+
\ 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 @@
+
+-- (selected) --
+
+
+
+ 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 @@
+
\ 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)
+
\ 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(
+ '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(
+ '',
+ 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 ',
+ ' ',
+ '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 = [
+ '',
+ ].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 ',
+ ' ',
+ '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 ',
+ ' ',
+ '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 ',
+ ' ',
+ '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(
+ ' something else',
+ 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(
+ '-- (optional) foo -- ',
+ 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(
+ 'Password: ',
+ pug.render('label\n | Password:\n input')
+ );
+ assert.equal(
+ 'Password: ',
+ 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(
+ 'footest bar',
+ pug.render('- var foo = "test"\n| foo\nem= foo\n| bar')
+ );
+ assert.equal(
+ 'testsomething ',
+ 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('', 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 @@
-
-
-
+
+
+
-
\ No newline at end of file
+
This
+is regular, javascript
+