<|start_filename|>lib/config.js<|end_filename|>
'use strict';

/**
 * Configurations for Virtual DOM libraries.
 * You are expected to install runtimes yourself.
 */
const VDOM_CONFIG = {
  snabbdom: {
    propsWrapper: `props`,
    rawProps: true, // don't translate Jade attrs to HTML props
    marshalDataset: false,
    runtime: `var h = require('snabbdom').h;`,
    serializeAttrsObjects: true,
  },

  'virtual-dom': {
    rawProps: false,
    marshalDataset: true,
    runtime: `var h = require('virtual-dom/h');`,
  },
};

module.exports = {VDOM_CONFIG};


<|start_filename|>test/test.js<|end_filename|>
/* eslint-env mocha */

'use strict';

const debug = require(`debug`)(`test`);
const expect = require(`expect.js`);
const fs = require(`fs`);
const parse5 = require(`parse5-utils`);
const path = require(`path`);
const vdomToHTML = require(`vdom-to-html`);
const snabbdomToHTML = require(`snabbdom-to-html`);

const render = require(`..`);

// expected output from literal imports
const SINGLE_ROOT_IMPORT_HTML = `<div class="test">test</div>`;
const MULTI_ROOT_IMPORT_HTML = `<div>child 1</div><div>child 2</div>`;

function fixtureFilename(name) {
  return path.resolve(__dirname, `fixtures/` + name + `.jade`);
}

function fixture(name) {
  return fs.readFileSync(fixtureFilename(name), `utf8`);
}

function fixtureToHTML(fixtureName, locals, options) {
  options = Object.assign({filename: fixtureFilename(fixtureName)}, options);

  const snabb = options.vdom === `snabbdom`;
  if (snabb && !options.snabbArgs) {
    // putting all 'raw' attrs into the props object allows
    // the same template fixtures to work with both virtual-dom
    // and snabbdom, otherwise snabb expects special keys
    // props: {}, attrs: {}
    options.propsWrapper = `props`;
    options.rawProps = false;
  }

  const compiled = render(fixture(fixtureName), options);
  debug(compiled);

  const fn = eval(`(${compiled})`);
  const root = fn.call({class: `asdf`}, locals);
  const toHTML = snabb ? snabbdomToHTML : vdomToHTML;
  const html = toHTML(root);
  debug(html);

  parse5.parse(html, true);
  return html;
}

describe(`configuration`, function() {
  it(`throws when an invalid vdom config is supplied`, function() {
    expect(render).withArgs(fixture(`simple`), {
      filename: fixtureFilename(`simple`),
      vdom: `foobar`,
    }).to.throwException(`No virtual-jade config found for vdom type: foobar`);
  });

  it(`throws a parser error with filename and line number when rendering a broken template`, function() {
    const file = `break-parser`;
    const filename = fixtureFilename(file);

    expect(render).withArgs(fixture(file), {filename}).to.throwException(function(err) {
      const filenameAndLineNo = new RegExp(filename.replace(`/`, `\\/`) + `:2`);
      expect(err.path).to.be(filename);
      expect(err.message).to.match(filenameAndLineNo);
      expect(err.message).to.match(/Unexpected identifier/);
    });
  });

  it(`throws a compiler error with filename and line number when rendering a broken template`, function() {
    const file = `break-compiler`;
    const filename = fixtureFilename(file);
    expect(render).withArgs(fixture(file), {filename}).to.throwException(function(err) {
      const filenameAndLineNo = new RegExp(filename.replace(`/`, `\\/`) + `:2`);
      expect(err.path).to.be(filename);
      expect(err.message).to.match(filenameAndLineNo);
      expect(err.message).to.match(/You can only have one top-level tag!/);
    });
  });

  it(`renders a template without options`, function() {
    const compiled = render(fixture(`attributes`));
    expect(compiled).to.contain(`class1`);
    expect(compiled).to.contain(`foo`);
    expect(compiled).to.contain(`doge`);
  });
});

describe(`rendering`, function() {

  // run rendering tests against each supported Virtual DOM library

  const VDOM_LIBS = [
    `virtual-dom`,
    `snabbdom`,
  ];

  for (let vdom of VDOM_LIBS) {
    const renderFixture = function(fixtureName, locals) {
      return fixtureToHTML(fixtureName, locals, {vdom, pretty: true});
    };

    context(`using library "${vdom}"`, function() {
      it(`renders a template`, function() {
        const html = renderFixture(`item`, {
          item: {
            id: `1234`,
            title: `some title`,
            description: `some description`,
            active: true,
          },
        });

        expect(html).to.contain(`item active`);
        expect(html).to.contain(`class="title"`);
        expect(html).to.contain(`<h3`);
        expect(html).to.contain(`</h3>`);
        expect(html).to.contain(`some title`);
        expect(html).to.contain(`some description`);
      });

      it(`beautifies when option is set`, function() {
        const fn = render(fixture(`item`), {pretty: true});
        const lines = fn.split(`\n`);
        expect(lines.length).to.be.greaterThan(15);
        expect(lines[lines.length - 1].trim()).to.be(`}`);
      });

      it(`inserts dynamic tag names`, function() {
        let html = renderFixture(`dynamic-tag`, {myTag: `input`});
        expect(html).to.contain(`<input class="llamas">`);
        html = renderFixture(`dynamic-tag`, {myTag: `textarea`});
        expect(html).to.contain(`<textarea class="llamas">`);
      });

      it(`renders a simple template`, function() {
        const html = renderFixture(`simple`);
        expect(html).to.be(`<div>This is text</div>`);
      });

      it(`translates basic class notation`, function() {
        const html = renderFixture(`class`);
        expect(html).to.contain(`div class="foo"`);
        expect(html).to.contain(`div class="baz llamas"`);
      });

      it(`renders falsey numeric values`, function() {
        const html = renderFixture(`falsey-content`);
        expect(html).to.contain(`<span class="zero-a">0</span>`);
        expect(html).to.contain(`<span class="zero-b">0</span>`);
        expect(html).to.contain(`<span class="zero-c">0</span>`);
        expect(html).to.contain(`<span class="zero-d">0</span>`);
        expect(html).to.contain(`<span class="one">1</span>`);
        expect(html).to.contain(`<span class="null">null</span>`);
      });

      describe(`iteration`, function() {
        it(`runs "each" loops correctly`, function() {
          const html = renderFixture(`each-expression`, {values: [`foo`, `bar`]});
          expect(html).to.contain(`<li>foo</li><li>bar</li>`);
        });

        it(`runs "while" loops correctly`, function() {
          const html = renderFixture(`while`);
          expect(html.match(/<div class="item">/g)).to.have.length(5);
        });

        it(`runs "else" block if no iteration occurred`, function() {
          let html = renderFixture(`each-else`, {items: [`foo`, `bar`]});
          expect(html).to.contain(`<li>foo</li><li>bar</li>`);
          expect(html).not.to.contain(`No items found`);

          html = renderFixture(`each-else`, {items: []});
          expect(html).not.to.contain(`<li>foo</li><li>bar</li>`);
          expect(html).to.contain(`No items found`);
        });

        context(`over object key/value pairs`, function() {
          let html;

          beforeEach(function() {
            html = renderFixture(`each-object`, {obj: {
              llama: `a`,
              wombat: `b`,
            }});
          });

          it(`works with object literals`, function() {
            expect(html).to.contain(`<div class="literal-entry">Literal key: foo / val: bar</div>`);
            expect(html.match(/literal-entry/g)).to.have.length(1);
          });

          it(`works with object variables`, function() {
            expect(html).to.contain(`<div class="obj-entry">Value of llama is a</div>`);
            expect(html).to.contain(`<div class="obj-entry">Value of wombat is b</div>`);
            expect(html.match(/obj-entry/g)).to.have.length(2);
          });

          it(`runs "else" block if no iteration occurred`, function() {
            expect(html).not.to.contain(`entry-present`);
            expect(html).to.contain(`No entries in object`);
          });
        });
      });

      describe(`multiple files`, function() {
        it(`inserts included files`, function() {
          const html = renderFixture(`include`);
          expect(html).to.contain(`<p>Hello</p>`);
          expect(html).to.contain(`<div class="included-content">llamas!!!</div>`);
          expect(html).to.contain(`<p>world</p>`);
        });

        it(`makes included mixins available`, function() {
          const html = renderFixture(`include-with-mixin`);
          expect(html).to.contain(`<div class="foo">`);
          expect(html).to.contain(`<div class="hello">insert me</div>`);
        });

        it(`inserts extended files`, function() {
          const html = renderFixture(`extends`);
          expect(html).to.contain(`<div class="foo">`);
          expect(html).to.contain(`capybara`);
          expect(html).to.contain(`default content`);
        });
      });

      describe(`attributes`, function() {
        it(`adds arbitrary attributes`, function() {
          const html = renderFixture(`attributes`);
          expect(html).to.contain(`required`);
          expect(!html.includes(`something`) || html.includes(`something="false"`)).to.be.ok();
        });

        it(`adds class attributes in array notation`, function() {
          let html = renderFixture(`attributes`);
          expect(html).to.contain(`1 2`);
          html = renderFixture(`attributes`, {variable: `meow`});
          expect(html).to.contain(`1 2 meow`);
        });

        it(`adds class attributes in object notation`, function() {
          let html = renderFixture(`attributes`);
          expect(html).to.contain(`obj1`);
          expect(html).not.to.contain(`obj2`);

          html = renderFixture(`attributes`, {variable: `doge`});
          expect(html).to.contain(`obj1`);
          expect(html).to.contain(`obj2`);
        });

        it(`combines class attributes in different notations gracefully`, function() {
          let html = renderFixture(`attributes`);
          expect(html).to.contain(`mixed`);
          expect(html).to.contain(`mixedArray1`);
          expect(html).not.to.contain(`mixedObj1`);

          html = renderFixture(`attributes`, {var2: `doge`});
          expect(html).to.contain(`mixed`);
          expect(html).to.contain(`mixedArray1`);
          expect(html).to.contain(`mixedObj1`);
          expect(html).to.contain(`doge`);
        });

        it(`renders data attributes`, function() {
          const html = renderFixture(`attributes`, {variable: `capybara`});
          expect(html).to.contain(`data-foo-id="42"`);
          expect(html).to.contain(`data-var="capybara"`);
        });

        it(`converts attributes to correct property names`, function() {
          const html = renderFixture(`attributes`);
          expect(html).to.contain(`autocomplete="on"`);
          expect(html).to.contain(`tabindex="5"`);
          expect(html).to.contain(`for="special-attr"`);
        });
      });

      describe(`case statements`, function() {
        it(`does not execute any cases when none match`, function() {
          const html = renderFixture(`case`);
          expect(html).not.to.contain(`foo`);
          expect(html).not.to.contain(`bar`);
        });

        it(`executes default when no others match`, function() {
          const html = renderFixture(`case`);
          expect(html).to.contain(`llama`);
        });

        it(`executes only one match`, function() {
          const html = renderFixture(`case`, {variable: 1});
          expect(html).to.contain(`span`);
          expect(html).not.to.contain(`input`);
          expect(html).not.to.contain(`textarea`);
        });

        it(`falls through from the first case in a chain`, function() {
          const html = renderFixture(`case`, {variable2: `d`});
          expect(html).to.contain(`bar`);
          expect(html).not.to.contain(`foo`);
        });

        it(`falls through from the middle case in a chain`, function() {
          const html = renderFixture(`case`, {variable2: `e`});
          expect(html).to.contain(`bar`);
          expect(html).not.to.contain(`foo`);
        });

        it(`falls through from the last case in a chain`, function() {
          const html = renderFixture(`case`, {variable2: `f`});
          expect(html).to.contain(`bar`);
          expect(html).not.to.contain(`foo`);
        });
      });

      describe(`code blocks`, function() {
        let html;

        beforeEach(function() {
          html = renderFixture(`code`);
        });

        it(`runs unbuffered code correctly`, function() {
          expect(html).to.contain(`<div class="example bar">`);
        });

        it(`uses locals when evaluating`, function() {
          html = renderFixture(`code`, {x: 0});
          expect(html).to.contain(`<div class="baz">`);

          html = renderFixture(`code`, {x: -1});
          expect(html).not.to.contain(`<div class="baz">`);
        });

        it(`outputs inline buffered code`, function() {
          expect(html).to.contain(`<div class="inline-script"><div class="raw-inline">within another div`);
        });

        it(`outputs standalone buffered code`, function() {
          expect(html).to.contain(`<div class="raw-buffered">raw so raw`);
        });

        it(`does not output unbuffered code`, function() {
          expect(html).not.to.contain(`should not be output`);
        });
      });

      describe(`multiple files`, function() {
        it(`inserts included literal (non-jade) files`, function() {
          const html = renderFixture(`literal-import`);
          expect(html).to.be(
            `<div class="raw">` +
              `<div class="single-root">` +
                `<div>` + SINGLE_ROOT_IMPORT_HTML + `</div>` +
              `</div>` +
              `<div class="multi-root">` +
                `<div>` + MULTI_ROOT_IMPORT_HTML + `</div>` +
              `</div>` +
            `</div>`
          );
        });
      });

      describe(`mixins`, function() {
        it(`renders mixins with args`, function() {
          const html = renderFixture(`mixin-args`);
          expect(html).to.be(
            `<div class="items">` +
              `<div class="item">` +
                  `<div class="more-tree">6</div>` +
              `</div>` +
              `<div class="item">` +
                  `<div class="more-tree">7</div>` +
              `</div>` +
            `</div>`
          );
        });

        it(`renders mixins exactly the same when calling mixin functions from code blocks`, function() {
          const htmlMixin = renderFixture(`mixin-args`);
          const html$Mixin = renderFixture(`mixin-args-$mixins`);
          expect(htmlMixin).to.be(html$Mixin);
        });
      });

      describe(`plain text`, function() {
        let html;

        beforeEach(function() {
          html = renderFixture(`text`);
        });

        it(`renders inlined text`, function() {
          expect(html).to.contain(`<div class="inline-in-tag">Inlined within tag</div>`);
        });

        it(`renders piped text with spaces between lines`, function() {
          expect(html).to.contain(`<div class="piped">Hello world</div>`);
        });

        it(`does not insert extraneous whitespace between tags`, function() {
          expect(html).to.contain(`<div class="tags-whitespace">You put the em<em>pha</em>sis on the wrong syl<em>la</em>ble.</div>`);
        });

        it(`supports tag interpolation syntax`, function() {
          expect(html).to.contain(`<div class="tag-interpolation">Sweet <blink>flashy</blink> tags</div>`);
        });

        it(`supports block-in-tag trailing dot syntax`, function() {
          expect(html).to.contain(`<div class="block-in-tag">Arbitrary text and more arbitrary text</div>`);
        });
      });
    });
  }
});

describe(`snabbdom-specific rendering`, function() {
  let html;

  beforeEach(function() {
    html = fixtureToHTML(`snabb`, {}, {
      vdom: `snabbdom`,
      snabbArgs: true,
      pretty: true,
    });
  });

  it(`renders plain objects`, function() {
    expect(html).to.contain(`<div>Hello</div>`);
  });

  it(`renders arbitrary attributes`, function() {
    expect(html).to.contain(`bar="baz"`);
  });

  it(`serializes object attributes`, function() {
    expect(html).to.contain(`obj="{&quot;hello&quot;:&quot;world&quot;}"`);
  });

  it(`renders properties`, function() {
    expect(html).to.contain(`id="xxx"`);
  });

  it(`translates id-selector notation`, function() {
    expect(html).to.contain(`id="id-example1"`);
  });

  it(`combines id-selector notation with props object`, function() {
    expect(html).to.contain(`id="id-example2"`);
    expect(html).to.contain(`href="link2"`);
  });

  it(`combines classes from jade notation and object notation`, function() {
    expect(html).to.match(/class=".+"/);
    const classes = html.match(/class="(.+?)"/)[1].split(` `);
    expect(classes.sort()).to.eql([`jade-class`, `foo`, `obj-class`].sort());
  });

  it(`renders jade-only class notation`, function() {
    expect(html).to.contain(`<div class="single-class"`);
  });

  it(`renders object-only class notation`, function() {
    expect(html).to.contain(`<a class="aclass"`);
  });

  it(`renders literal imports`, function() {
    expect(html).to.contain(MULTI_ROOT_IMPORT_HTML);
  });
});


<|start_filename|>package.json<|end_filename|>
{
  "name": "virtual-jade",
  "description": "Compile Jade templates to virtual-dom functions",
  "version": "1.0.0",
  "author": "<NAME> <<EMAIL>>",
  "contributors": [
    "<NAME> <<EMAIL>> (http://jongleberry.com)",
    "<NAME> <<EMAIL>>"
  ],
  "license": "MIT",
  "repository": "tdumitrescu/virtual-jade",
  "bugs": {
    "url": "https://github.com/tdumitrescu/virtual-jade/issues"
  },
  "homepage": "https://github.com/tdumitrescu/virtual-jade",
  "dependencies": {
    "debug": "^2.1.3",
    "jade": "^1.9.2",
    "lazyrequire": "^1.2.1",
    "with": "^5.0.0"
  },
  "devDependencies": {
    "coveralls": "3.1.1",
    "eslint": "^5.10.0",
    "expect.js": "^0.3.1",
    "istanbul": "^0.4.2",
    "js-beautify": "^1.5.5",
    "mocha": "^5.2.0",
    "parse5-utils": "^1.3.1",
    "snabbdom": "^3.0.0",
    "snabbdom-to-html": "^3.0.0-beta.1",
    "vdom-to-html": "^2.3.0",
    "virtual-dom": "^2.0.1"
  },
  "scripts": {
    "lint": "eslint lib test",
    "test": "mocha",
    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot",
    "test-ci": "npm run lint && istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot",
    "test-g": "mocha -g"
  },
  "keywords": [
    "virtual",
    "dom",
    "jade",
    "html",
    "maquette",
    "pug",
    "reactive",
    "snabbdom",
    "template",
    "templating",
    "virtual-dom"
  ],
  "files": [
    "lib"
  ],
  "main": "lib"
}


<|start_filename|>lib/compiler.js<|end_filename|>
'use strict';

const VDOM_CONFIG = require(`./config`).VDOM_CONFIG;

const OVERRIDABLE_CONFIG_OPTIONS = [
  `marshalDataset`,
  `propsWrapper`,
  `rawProps`,
  `serializeAttrsObjects`,
];

/**
 * Compilation style.
 *
 * We rely on minification, so don't bother with styling.
 * For example, trailling commas on object and arrays are _preferred_.
 *
 * We rely on beautification for "pretty" mode,
 * so don't bother handling indentations and so on.
 */

const debug = require(`debug`)(`virtual-jade:compiler`);

function assertWithDetailedError(node, assertion, message) {
  if (!assertion) {
    const err = new Error(message);
    err.line = node.line;
    err.filename = node.filename;
    throw err;
  }
}

function Compiler(node, options) {
  this.node = node;
  this.options = options;

  // set options from vdom config + overrides
  this.vdom = VDOM_CONFIG[options.vdom || `virtual-dom`];
  for (let optionName of OVERRIDABLE_CONFIG_OPTIONS) {
    const optionSource = this.options.hasOwnProperty(optionName) ? this.options : this.vdom;
    this[optionName] = optionSource[optionName];
  }
}

/**
 * Actually compile the tokens.
 * We want to match the Jade API as much as possible here.
 */

Compiler.prototype.compile = function() {
  let tagged = null;
  let js = ``;

  this.hasObjAttrs = false;
  this.hasAttrsProp = false;
  this.mixins = [];

  for (const node of this.node.nodes) {
    switch (node.type) {
      case `Block`:
        this.visitBlock(node);
        break;
      case `Code`:
        assertWithDetailedError(node, !tagged, `Code must exist before the tag.`);
        js += node.val + `;\n`;
        break;
      case `Mixin`:
        this.visitMixin(node);
        break;
      case `Tag`:
        assertWithDetailedError(node, !tagged, `You can only have one top-level tag!`);
        tagged = true;
        js += `return ` + this.visitTag(node);
        break;
    }
  }

  assertWithDetailedError(this.node, tagged, `Exactly a single root element is required!`);

  // probably this stuff should all be extracted/extractable to a single
  // virtual-jade runtime to reduce bundle weight when lots of templates
  // are combined in one bundle
  if (this.hasCode) {
    js = `
      var __vjadeSafeCode = function(code) {
        return code || String(code);
      };
      ${js}
    `;
  }
  if (this.hasObjAttrs) {
    js = `
      var __vjadeObjToAttrs = function(o) {
        return Object.keys(o).map(function(k) { return o[k] ? k : false});
      };
      ${js}
    `;
  }

  if (this.hasAttrsProp) {
    js = `
      var __vjadeStringifyAttrsIfObj = function(attrs) {
        if (typeof attrs !== 'object') return attrs;
        const copy = {};
        for (const [k, v] of Object.entries(attrs)) {
          copy[k] = typeof attrs[k] === 'object' ? JSON.stringify(v) : v;
        }
        return copy;
      }
      ${js}
    `;
  }

  if (this.mixins.length > 0) {
    js = `var jade_mixins = {}, $mixins = jade_mixins;${this.mixins.join(``)}${js}`;
  }

  return js;
};

/**
 * Visit a node, though this is really just meant for the first tag.
 */

Compiler.prototype.visit = function(node) {
  const method = `visit` + node.type;
  assertWithDetailedError(node,
    typeof this[method] === `function`,
    `Node type "${node.type}" is not implemented!`);
  return this[method](node);
};

/**
 * TODO
 */

function toVal(x) {
  return x.val;
}

Compiler.prototype.visitComment =
Compiler.prototype.visitBlockComment = function(node) {
  if (!node.block) return `/* ${node.val} */\n`;
  return `/* ` + node.block.nodes.map(toVal).join(`\n`) + `*/\n`;
};

/**
 * Create basic text nodes.
 */

Compiler.prototype.visitText = function(node, prefix=``) {
  return JSON.stringify(prefix + node.val)
    .replace(/#\{(.+?)\}/g, `" + ($1) + "`);
};

/**
 * Wrap props dictionary if necessary
 * e.g. {foo: 'bar'} => {props: {foo: 'bar'}}
 */
Compiler.prototype.wrapProps = function(propsOut, propsWrapper) {
  if (propsOut.length) {
    if (arguments.length < 2 && !this.rawProps) {
      propsWrapper = this.propsWrapper;
    }
    if (propsWrapper) {
      propsOut = `{${propsWrapper}:${propsOut}}`;
    }
  }

  return propsOut;
};

/**
 * Handle a "Literal" include such as an SVG or HTML file
 */
Compiler.prototype.visitLiteral = function(node) {
  const escapedSource = node.str.replace(/'/g, `\\'`).replace(/\n/g, ``);
  const propsOut = this.wrapProps(`{innerHTML: '${escapedSource}'}`, this.propsWrapper);
  return `h('div', ${propsOut})`;
};

/**
 * Build a single HTML element.
 */

Compiler.prototype.visitTag = function(tag) {
  let buf = `h(` + (tag.buffer ? `(${tag.name})` : JSON.stringify(tag.name));
  let propsOut = this.wrapProps(this.visitAttributes(tag.attrs, tag.attributeBlocks));
  if (propsOut.length) {
    buf += `,${propsOut}`;
  }
  // TODO: handle cases when there is both code and blocks
  if (tag.code) {
    this.hasCode = true;
    buf += `, [__vjadeSafeCode(${tag.code.val})]`;
  } else {
    let buf2 = this.visitBlock(tag.block);
    if (buf2) {
      buf += `, ${buf2.trim()}.filter(Boolean)`;
    }
  }
  buf += `)`;
  return buf;
};

/**
 * Visit each attribute in a tag.
 * Note that each property of the attribute is already JS-ready,
 * so don't just JSON.stringify() the entire thing.
 * Also, `virtual-dom` handles properties differently.
 */

// attribute -> property transformations
const ATTRS_TO_PROPS = {
  'class': `className`,
  'for': `htmlFor`,
  'http-equiv': `httpEquiv`,
};
const LOWERCASE_ATTRS = new Set([
  `acceptCharset`,
  `accessKey`,
  `allowFullScreen`,
  `allowTransparency`,
  `cellPadding`,
  `cellSpacing`,
  `colSpan`,
  `contentEditable`,
  `contextMenu`,
  `crossOrigin`,
  `dateTime`,
  `formAction`,
  `formEncType`,
  `formMethod`,
  `formNoValidate`,
  `formTarget`,
  `frameBorder`,
  `marginHeight`,
  `marginWidth`,
  `maxLength`,
  `mediaGroup`,
  `noValidate`,
  `readOnly`,
  `rowSpan`,
  `tabIndex`,
  `useMap`,
]);
for (const a of LOWERCASE_ATTRS) {
  ATTRS_TO_PROPS[a.toLowerCase()] = a;
}

// convert this-type-of-string to thisTypeOfString
function dashedToCamel(s) {
  return s.replace(/-([a-z])/g, group => group[1].toUpperCase());
}

// JSON-stringify keys but not vals, for JS evaluation in output
function outputObj(obj) {
  let buf = ``;
  for (const key of Object.keys(obj)) {
    buf += JSON.stringify(key) + `:` + obj[key] + `,`;
  }
  return buf ? `{${buf}}` : ``;
}

Compiler.prototype.visitAttributes = function(attrs) {
  // first, we need to format the attributes
  const props = Object.create(null);

  if (this.rawProps) {

    // easy-peasy, pass through everything directly except class/id

    const jadeClasses = [];
    let rawClassObj = ``;
    let hasClasses = false;
    let id = ``;
    for (const attr of attrs) {
      if (attr.name === `class`) {
        hasClasses = true;
        if (attr.val[0] === `'`) {
          jadeClasses.push(attr.val);
        } else {
          rawClassObj = attr.val;
        }
      } else if (attr.name === `id` && attr.val[0] === `'`) {
        id = attr.val;
      } else if (this.serializeAttrsObjects && attr.name === `attrs`) {
        // special case for snabbdom's 'attrs' object notation for specifying HTML attributes
        // since attributes are always strings, automatically JSON-serialize any object values
        // e.g.: in attrs={foo: {bar: 'baz'}} the {bar: 'baz'} should be auto-serialized
        this.hasAttrsProp = true;
        props[attr.name] = `__vjadeStringifyAttrsIfObj(${attr.val})`;
      } else {
        props[attr.name] = attr.val;
      }
    }

    // generate class code
    if (hasClasses) {
      const jadeClassObj = `{${jadeClasses.map(cls => `${cls}:true`).join(`,`)}}`;
      if (rawClassObj) {
        if (jadeClasses.length) {
          props.class = `Object.assign({}, ${rawClassObj}, ${jadeClassObj})`;
        } else {
          props.class = rawClassObj;
        }
      } else {
        props.class = jadeClassObj;
      }
    }

    // generate id code
    if (id) {
      const idObj = `{id: ${id}}`;
      const propsObj = props[this.propsWrapper];
      props[this.propsWrapper] = propsObj ? `Object.assign(${idObj}, ${propsObj})` : idObj;
    }

  } else {

    // translate jade attrs to the appropriate vdom props

    const classExprs = [];
    const dataset = {};
    for (const attr of attrs) {
      let name = ATTRS_TO_PROPS[attr.name] || attr.name;
      let val = attr.val;

      if (name === `className`) {
        /**
         * We need to handle `class` separately because
         * it can be defined multiple times.
         */
        const isObj = !!val.match(/^\s*\{(.|[\r\n])+\}\s*$/);
        if (isObj) {
          this.hasObjAttrs = true;
          val = `__vjadeObjToAttrs(${val})`;
        }
        classExprs.push(val);
      } else if (this.marshalDataset !== false && name.startsWith(`data-`)) {
        // strip data- and camelcase remainder for dataset
        dataset[dashedToCamel(name.slice(5))] = val;
      } else {
        props[name] = val;
      }
    }
    if (classExprs.length) {
      const classConcat = classExprs.map(e => `.concat(${e})`).join(``);
      props.className = `[]${classConcat}.filter(Boolean).join(' ')`;
    }
    if (Object.keys(dataset).length) {
      props.dataset = outputObj(dataset);
    }
  }

  debug(`properties: %o`, props);

  return outputObj(props);
};

Compiler.prototype.visitBlock = function(block) {
  const nodes = block.nodes;
  const length = nodes.length;
  if (!length) return ``;

  let buf = ``;

  for (let i = 0; i < nodes.length; i++) {
    let curNodeBuf = ``;
    const node = nodes[i];
    switch (node.type) {
      case `Code`:
        assertWithDetailedError(node, !/^\s*else/.test(node.val), `Hanging else statement!`);

        if (/^\s*if\s+/.test(node.val)) {
          // handle if statements
          let ended = false;

          curNodeBuf += node.val.replace(/^\s*if\s+/, ``);
          curNodeBuf += ` ? (${this.visitBlock(node.block)})`;

          for (let j = i + 1; j < nodes.length; j++) {
            const next = nodes[j];
            if (/^\s*else\s+if\s+/.test(next.val)) {
              i++;
              curNodeBuf += ` : ` + next.val.replace(/^\s*else\s+if\s+/, ``);
              curNodeBuf += ` ? (${this.visitBlock(next.block)})`;
            } else if (/^\s*else\s*$/.test(next.val)) {
              i++;
              ended = true;
              curNodeBuf += ` : (${this.visitBlock(next.block)})`;
              break;
            } else {
              break;
            }
          }

          if (!ended) curNodeBuf += ` : undefined`;
          buf += `__jade_nodes = __jade_nodes.concat(${curNodeBuf});`;
        } else if (/^\s*while/.test(node.val)) {
          // handle while loops
          curNodeBuf += `(function(){
            var buf = [];
            ${node.val} {
              buf = buf.concat(${this.visitBlock(node.block)})
            }
            return buf
          }).call(this)`;
          buf += `__jade_nodes = __jade_nodes.concat(${curNodeBuf});`;
        } else if (node.val) {
          // arbitrary code
          node.val = node.val.trim();
          buf += node.buffer ? `__jade_nodes = __jade_nodes.concat(${node.val});` : `${node.val};`;
        }

        break;
      case `Comment`:
      case `BlockComment`:
        buf += this.visitComment(node);
        break;
      case `Each`:
        curNodeBuf = this.visitEach(node);
        buf += `__jade_nodes = __jade_nodes.concat(${curNodeBuf});`;
        break;
      case `Text`: {
        // contiguous text nodes need to be rendered with whitespace in between
        const prefix = i > 0 && nodes[i - 1].type === `Text` ? ` ` : ``;
        curNodeBuf = this.visitText(node, prefix);
        buf += `__jade_nodes = __jade_nodes.concat(${curNodeBuf});`;
        break;
      }
      default: {
        curNodeBuf = this.visit(node);
        if (curNodeBuf) {
          buf += `__jade_nodes = __jade_nodes.concat(${curNodeBuf});`;
        }
      }
    }
  }

  return buf ? `(function() {var __jade_nodes = [];${buf};return __jade_nodes}).call(this)` : ``;
};

/**
 * Handles jade's special case statements,
 * which breaks it down into a bunch of `if` statements.
 *
 * TODO: maybe memoize the main expression
 */

Compiler.prototype.visitCase = function(code) {
  let str = ``;
  let defaulted = false;
  let conditions = [];

  for (const node of code.block.nodes) {
    if (node.expr === `default`) {
      defaulted = true;
      str += `(${this.visitBlock(node.block)})`;
      break;
    }

    conditions.push(`((${code.expr}) == (${node.expr}))`);
    if (node.block) {
      str += `(${conditions.join(` || `)})
        ? (${this.visitBlock(node.block)})
        : `;
      conditions = [];
    }
  }

  if (!defaulted) str += `undefined`;
  return str;
};

/**
 * Handle each statements
 * - uses Array.concat() with result of each iteration to ensure flattening
 * - closes over k/v vars in each iteration block so they can be used later
 *   (e.g. in event handler callbacks)
 */

Compiler.prototype.visitEach = function(code) {
  let out = `(function() {
    var $$obj = (${code.obj});
    var $$eachNodes;
    var $$iterated = false;
    if (Array.isArray($$obj)) {
      $$eachNodes = $$obj.reduce(function($$eachNodes, ${code.val}, ${code.key}) {
        $$iterated = true;
        return $$eachNodes.concat(${this.visitBlock(code.block)});
      }, []);
    } else {
      $$eachNodes = [];
      for (var $$objKey in $$obj) {
        (function() {
          var ${code.key} = $$objKey;
          if ($$obj.hasOwnProperty(${code.key})) {
            $$iterated = true;
            var ${code.val} = $$obj[${code.key}];
            $$eachNodes = $$eachNodes.concat(${this.visitBlock(code.block)});
          }
        })();
      }
    }
  `;

  if (code.alternative) {
    out += `
    if (!$$iterated) {
      return ${this.visitBlock(code.alternative)};
    }
    `;
  }

  out += `
    return $$eachNodes;
  })()`;

  return out;
};

/**
 * Handle mixin declarations and calls
 */

Compiler.prototype.visitMixin = function(mixin) {
  let str = ``;
  let args = mixin.args || ``;
  let rest = ``;
  let initRest = ``;

  if (mixin.call) {

    // mixin call

    let ctx;
    const ctxItems = [];
    if (mixin.block) {
      ctxItems.push(`block: function() { return ${this.visitBlock(mixin.block)}; }`);
    }
    if (mixin.attrs.length) {
      ctxItems.push(`attributes: ` + this.visitAttributes(mixin.attrs));
    }
    if (ctxItems.length) {
      ctx = `{${ctxItems.join(`,`)}}`;
    } else {
      ctx = `this`;
    }
    if (args) {
      args = `, ${args}`;
    }
    str = `jade_mixins['${mixin.name}'].call(${ctx}${args})`;

  } else {

    // mixin declaration

    args = args ? args.split(`,`) : [];
    if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) {
      rest = args.pop().trim().replace(/^\.\.\./, ``);
    }
    if (rest) {
      initRest = `
        var ${rest} = [];
        for (var jade_interp = ${args.length}; jade_interp < arguments.length; jade_interp++) {
          ${rest}.push(arguments[jade_interp]);
        }
      `;
    }
    this.mixins.push(`
      jade_mixins['${mixin.name}'] = function(${args.join(`,`)}) {
        var block = (this && this.block), attributes = (this && this.attributes) || {};
        ${initRest}
        return ${this.visitBlock(mixin.block)};
      };
    `);

  }

  return str;
};

/**
 * Handle mixin's `block` keyword
 */

Compiler.prototype.visitMixinBlock = function() {
  return `block && block()`;
};


module.exports = Compiler;
