<commit_before>"use strict";
var fs = require("fs");
var gulp = require("gulp");
var sourcemaps = require('gulp-sourcemaps');
var concat = require('gulp-concat');
var ts = require("gulp-typescript");
var newer = require('gulp-newer');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var clean = require('gulp-clean');
var gulpPrint = require('gulp-print');
var osPath = require('path');
var typescript = null;
typescript = require("typescript/lib/typescript");

var locally = false;
var compilerOptions = loadValidCompilerOptions("tsconfig.gulp.json");
var manifest = readJSON("manifest.json", true);
var DEST = compilerOptions.outDir;
if (!DEST || DEST === ".") {
  DEST = compilerOptions.outDir = "dist";
}
var enableSourceMap = !!compilerOptions.sourceMap;
var willListFiles   = !!compilerOptions.listFiles;
var removeComments  = !!compilerOptions.removeComments;
var JSDEST = osPath.join(DEST, ".build");
var globalForceUpdate = false;
var disableErrors = !globalForceUpdate;

if (compilerOptions.noImplicitUseStrict) {
  compilerOptions.alwaysStrict = false;
}

var Tasks = {
  background: "background/*.ts",
  content: ["./content/*.ts", "./lib/*.ts"],
  front: "front/*.ts",
  lib: "lib/*.ts",
  "pages/others": ["pages/loader.ts", "pages/chrome_ui.ts"],
  show: "pages/show.ts",
  options: "pages/options*.ts",
  pages: ["pages/others", "show", "options"],
  "static/special": function() {
    return copyByPath(["pages/newtab.js", "lib/math_parser*", "lib/*.min.js"]);
  },
  static: ["static/special", function() {
    return copyByPath(["front/*", "pages/*", "icons/*", "lib/*.css"
        , "settings_template.json", "*.txt", "*.md"
        , "!**/manifest.json"
        , '!**/*.ts', "!**/*.js", "!**/tsconfig*.json"
        , "!front/vimium.css", "!todo*"
      ]);
  }],

  scripts: ["background", "content", "front"],
  ts: ["scripts", "pages"],
  js: ["ts"],

  "min/bg": function() {
    var sources = manifest.background.scripts;
    sources = ("\n" + sources.join("\n")).replace(/\n\//g, "\n").split("\n");
    var body = sources.splice(0, sources.indexOf("background/main.js") + 1, "background/body.js");
    var stream1 = uglifyJSFiles(body, sources[0]);
    var index = sources.indexOf("background/tools.js");
    var tail = sources.splice(index, sources.length - index, "background/tail.js");
    uglifyJSFiles(tail, sources[index]);
    uglifyJSFiles(sources.slice(1, index), "background/", "");
    manifest.background.scripts = sources;
  },
  "min/content": function() {
    var cs = manifest.content_scripts[0], sources = cs.js;
    if (sources.length <= 2) {
      return;
    }
    var lastFile = sources.pop();
    cs.js = ["content/body.js", lastFile];
    return uglifyJSFiles(sources, cs.js[0]);
  },
  "min/others": function() {
    var oriManifest = readJSON("manifest.json", true);
    var res = ["**/*.js"];
    for (var arr = oriManifest.background.scripts, i = 0, len = arr.length; i < len; i++) {
      res.push("!" + arr[i]);
    }
    for (var arr = oriManifest.content_scripts[0].js, i = 0, len = arr.length - 1; i < len; i++) {
      res.push("!" + arr[i]);
    }
    return uglifyJSFiles(res, ".", "");
  },
  manifest: [["min/bg", "min/content"], function() {
    var file = osPath.join(DEST, "manifest.json")
      , data = JSON.stringify(manifest, null, "  ");
    if (fs.existsSync(file) && fs.statSync(file).isFile()) {
      var oldData = readFile(file);
      if (data === oldData) {
        if (willListFiles) {
          print('skip', file);
        }
        return;
      }
    }
    fs.writeFileSync(file, data);
  }],
  minjs: ["min/bg", "min/content", "min/others"],
  dist: [["static", "js"], willStart("manifest", "min/others")],

  build: ["dist"],
  default: ["dist"],
  rebuild: ["clean", willStart("dist")],
  all: ["rebuild"],
  "clean/temp": function() {
    disableErrors = false;
    return cleanByPath([JSDEST + "**/*.js"
      , JSDEST + "**/*.js.map"
      , "!" + JSDEST + "/pages/newtab.js"]);
  },
  clean: ["clean/temp", function() {
    return cleanByPath(DEST + "/*");
  }],

  locally: function() {
    if (locally) { return; }
    enableSourceMap = false;
    willListFiles = true;
    globalForceUpdate = false;
    disableErrors = !globalForceUpdate;
    JSDEST = ".";
    compilerOptions = loadValidCompilerOptions("tsconfig.json", true);
    locally = true;
  },
  "local/scripts": ["locally", function() {
    return compile(["background/*.ts", "content/*.ts", "lib/*.ts"
        , "!lib/polyfill.ts"]);
  }],
  "local/others": ["locally", function() {
    return compile(["front/*.ts", "lib/polyfill.ts"
        , "pages/chrome_ui.ts", "pages/loader.ts"]);
  }],
  "local/pages": ["locally", function() {
    compile(["pages/*.ts", "pages/show.ts"]);
    return compile(["pages/options*.ts", "pages/show.ts"]);
  }],
  local: ["local/scripts", "local/others", "local/others"],
  test: ["local"]
}

makeTasks();

function makeCompileTask(src) {
  if (typeof src === "function") { return src; }
  return function() {
    return compile(src);
  };
}

function makeTasks() {
  var hasOwn = Object.prototype.hasOwnProperty;
  for (var key in Tasks) {
    if (!hasOwn.call(Tasks, key)) { continue; }
    var task = Tasks[key];
    if (typeof task === "function") {
      gulp.task(key, task);
    } else if (typeof task === "string") {
      gulp.task(key, Tasks[key] = makeCompileTask(task));
    } else if (typeof task[1] === "function") {
      gulp.task(key, task[0] instanceof Array ? task[0] : [task[0]], task[1]);
    } else if (!/\.ts\b/i.test(task[0])) {
      gulp.task(key, task);
    } else {
      gulp.task(key, Tasks[key] = makeCompileTask(task));
    }
    if (key.slice(-1) !== "/" && fs.existsSync(key) && fs.statSync(key).isDirectory()) {
      gulp.task(key + "/", [key]);
    }
  }
}

function willStart() {
  var taskNames = [].slice.call(arguments, 0);
  return function() {
    // try to run it as a `sync` task if possible
    if (taskNames.length === 1) {
      var task = Tasks[taskNames[0]];
      if (typeof task === "function") {
        return task();
      }
    }
    return gulp.start.apply(gulp, taskNames);
  };
}

function tsProject() {
  return disableErrors ? ts(compilerOptions, ts.reporter.nullReporter()) : ts(compilerOptions);
}

function compile(pathOrStream, skipOutput, forceUpdate) {
  if (typeof pathOrStream === "string") {
    pathOrStream = [pathOrStream];
  }
  if (pathOrStream instanceof Array) {
    pathOrStream.push("!**/*.d.ts");
  }
  if (forceUpdate == null) {
    forceUpdate = globalForceUpdate;
  }
  var stream = pathOrStream instanceof Array ? gulp.src(pathOrStream, { base: "." }) : pathOrStream;
  if (!skipOutput && !forceUpdate) {
    stream = stream.pipe(newer({ dest: JSDEST, ext: '.js', extra: "**/*.d.ts" }));
  }
  if (willListFiles) {
    stream = stream.pipe(gulpPrint());
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.init());
  }
  var project = tsProject();
  var tsResult = stream.pipe(project);
  if (skipOutput) {
    return tsResult;
  }
  return outputJSResult(tsResult.js);
}

function outputJSResult(stream, concatedFile) {
  if (concatedFile) {
    stream = stream.pipe(concat(concatedFile));
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.write("."));
  }
  return stream.pipe(gulp.dest(JSDEST));
}

function uglifyJSFiles(path, output, new_suffix, forceUpdate) {
  if (typeof path === "string") {
    path = [path];
  }
  for (var i = 0; i < path.length; i++) {
    var p = path[i];
    path[i] = p[0] !== "!" ? osPath.join(JSDEST, p) : "!" + osPath.join(JSDEST, p.substring(1));
  }
  path.push("!**/*.min.js");
  output = output || ".";
  new_suffix = new_suffix !== "" ? (new_suffix || ".min") : "";

  var stream = gulp.src(path, { base: JSDEST });
  var is_file = output.indexOf(".js", Math.max(0, output.length - 3)) > 0;
  if (forceUpdate) {
  } else if (is_file) {
    stream = stream.pipe(newer({ dest: osPath.join(DEST, output) }));
  } else {
    stream = stream.pipe(newer({ dest: DEST, ext: new_suffix + '.js' }));
  }
  if (willListFiles) {
    stream = stream.pipe(gulpPrint());
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.init({ loadMaps: true }));
  }
  if (is_file) {
     stream = stream.pipe(concat(output));
  }
  stream = stream.pipe(uglify({
    preserveComments: removeComments ? undefined : "all"
  }));
  if (!is_file && new_suffix !== "") {
     stream = stream.pipe(rename({ suffix: new_suffix }));
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.write("."));
  }
  return stream.pipe(gulp.dest(DEST));
}

function copyByPath(path) {
  var stream = gulp.src(path, { base: "." })
    .pipe(newer(DEST));
  if (willListFiles) {
    stream = stream.pipe(gulpPrint());
  }
  return stream.pipe(gulp.dest(DEST));
}

function cleanByPath(path) {
  return gulp.src(path, {read: false}).pipe(clean());
}

function convertToStream(pathOrStream) {
  return typeof pathOrStream === "string" || pathOrStream instanceof Array
    ? gulp.src(pathOrStream, { base: "." }) : pathOrStream;
}

function readFile(fileName, info) {
  info == null && (info = {});
  var buffer = fs.readFileSync(fileName);
  var len = buffer.length;
  if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) {
      // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js,
      // flip all byte pairs and treat as little endian.
      len &= ~1;
      for (var i = 0; i < len; i += 2) {
          const temp = buffer[i];
          buffer[i] = buffer[i + 1];
          buffer[i + 1] = temp;
      }
      info.bom = "\uFFFE";
      return buffer.toString("utf16le", 2);
  }
  if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
      // Little endian UTF-16 byte order mark detected
      info.bom = "\uFEFF";
      return buffer.toString("utf16le", 2);
  }
  if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
      // UTF-8 byte order mark detected
      info.bom = "\uFEFF";
      return buffer.toString("utf8", 3);
  }
  info.bom = "";
  // Default is UTF-8 with no byte order mark
  return buffer.toString("utf8");
}

function _makeJSONReader() {
  var stringOrComment = /"(?:\\[\\\"]|[^"])*"|'(?:\\[\\\']|[^'])*'|\/\/[^\r\n]*|\/\*.*?\*\//g
    , lf = /\r\n|\r(?!\n)/g, notLF = /[^\r\n]+/g, notWhiteSpace = /\S/;
  function spaceN(str) {
    return ' '.repeat(str.length);
  }
  function onReplace(str) {
    switch (str[0]) {
    case '/': case '#':
      if (str.startsWith("/*")) {
        // replace comments with whitespace to preserve original character positions
        return str.replace(notLF, spaceN);
      }
      return spaceN(str);
    case '"': case "'": // falls through
    default:
      return str;
    }
  }
  function readJSON(fileName, throwError) {
    var text = readFile(fileName);
    // text = text.replace(lf, "\n");
    text = text.replace(stringOrComment, onReplace);
    try {
      return notWhiteSpace.test(text) ? JSON.parse(text) : {};
    } catch (e) {
      if (throwError === true) {
        throw e;
      }
      var err = "Failed to parse file '" + fileName + "': " + e + ".";
      console.warn(err);
      return {};
    }
  }
  global._readJSON = readJSON;
}

function readJSON(fileName, throwError) {
  if (!global._readJSON) {
    _makeJSONReader();
  }
  return _readJSON(fileName, throwError);
}

function readCompilerOptions(tsConfigFile, throwError) {
  if (tsConfigFile.lastIndexOf(".json") !== tsConfigFile.length - 5) {
    tsConfigFile += ".json";
  }
  var config = readJSON(tsConfigFile);
  var compilerOptions = config ? config.compilerOptions || {} : null;
  if (compilerOptions && config.extends) {
    var baseFile = osPath.join(osPath.dirname(tsConfigFile), config.extends);
    var baseOptions = readCompilerOptions(baseFile, throwError);
    if (baseOptions) {
      for (var key in baseOptions) {
        if (baseOptions.hasOwnProperty(key) && !(key in compilerOptions)) {
          compilerOptions[key] = baseOptions[key];
        }
      }
    }
  }
  return compilerOptions;
}

function loadValidCompilerOptions(tsConfigFile, keepCustomOptions) {
  var compilerOptions = readCompilerOptions(tsConfigFile, true);
  if (!keepCustomOptions) {
    delete compilerOptions.inferThisForObjectLiterals;
    delete compilerOptions.narrowFormat;
  }
  if (typescript) {
    compilerOptions.typescript = typescript;
  }
  return compilerOptions;
}

function print() {
  return console.log.apply(console, arguments);
}
<commit_msg>gulp: change loading of typescript compiler<commit_after>"use strict";
var fs = require("fs");
var gulp = require("gulp");
var sourcemaps = require('gulp-sourcemaps');
var concat = require('gulp-concat');
var ts = require("gulp-typescript");
var newer = require('gulp-newer');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var clean = require('gulp-clean');
var gulpPrint = require('gulp-print');
var osPath = require('path');

var locally = false;
var compilerOptions = loadValidCompilerOptions("tsconfig.gulp.json");
var typescript = null;
var manifest = readJSON("manifest.json", true);
var DEST = compilerOptions.outDir;
if (!DEST || DEST === ".") {
  DEST = compilerOptions.outDir = "dist";
}
var enableSourceMap = !!compilerOptions.sourceMap;
var willListFiles   = !!compilerOptions.listFiles;
var removeComments  = !!compilerOptions.removeComments;
var JSDEST = osPath.join(DEST, ".build");
var globalForceUpdate = false;
var disableErrors = !globalForceUpdate;

if (compilerOptions.noImplicitUseStrict) {
  compilerOptions.alwaysStrict = false;
}

var Tasks = {
  background: "background/*.ts",
  content: ["./content/*.ts", "./lib/*.ts"],
  front: "front/*.ts",
  lib: "lib/*.ts",
  "pages/others": ["pages/loader.ts", "pages/chrome_ui.ts"],
  show: "pages/show.ts",
  options: "pages/options*.ts",
  pages: ["pages/others", "show", "options"],
  "static/special": function() {
    return copyByPath(["pages/newtab.js", "lib/math_parser*", "lib/*.min.js"]);
  },
  static: ["static/special", function() {
    return copyByPath(["front/*", "pages/*", "icons/*", "lib/*.css"
        , "settings_template.json", "*.txt", "*.md"
        , "!**/manifest.json"
        , '!**/*.ts', "!**/*.js", "!**/tsconfig*.json"
        , "!front/vimium.css", "!todo*"
      ]);
  }],

  scripts: ["background", "content", "front"],
  ts: ["scripts", "pages"],
  js: ["ts"],

  "min/bg": function() {
    var sources = manifest.background.scripts;
    sources = ("\n" + sources.join("\n")).replace(/\n\//g, "\n").split("\n");
    var body = sources.splice(0, sources.indexOf("background/main.js") + 1, "background/body.js");
    var stream1 = uglifyJSFiles(body, sources[0]);
    var index = sources.indexOf("background/tools.js");
    var tail = sources.splice(index, sources.length - index, "background/tail.js");
    uglifyJSFiles(tail, sources[index]);
    uglifyJSFiles(sources.slice(1, index), "background/", "");
    manifest.background.scripts = sources;
  },
  "min/content": function() {
    var cs = manifest.content_scripts[0], sources = cs.js;
    if (sources.length <= 2) {
      return;
    }
    var lastFile = sources.pop();
    cs.js = ["content/body.js", lastFile];
    return uglifyJSFiles(sources, cs.js[0]);
  },
  "min/others": function() {
    var oriManifest = readJSON("manifest.json", true);
    var res = ["**/*.js"];
    for (var arr = oriManifest.background.scripts, i = 0, len = arr.length; i < len; i++) {
      res.push("!" + arr[i]);
    }
    for (var arr = oriManifest.content_scripts[0].js, i = 0, len = arr.length - 1; i < len; i++) {
      res.push("!" + arr[i]);
    }
    return uglifyJSFiles(res, ".", "");
  },
  manifest: [["min/bg", "min/content"], function() {
    var file = osPath.join(DEST, "manifest.json")
      , data = JSON.stringify(manifest, null, "  ");
    if (fs.existsSync(file) && fs.statSync(file).isFile()) {
      var oldData = readFile(file);
      if (data === oldData) {
        if (willListFiles) {
          print('skip', file);
        }
        return;
      }
    }
    fs.writeFileSync(file, data);
  }],
  minjs: ["min/bg", "min/content", "min/others"],
  dist: [["static", "js"], willStart("manifest", "min/others")],

  build: ["dist"],
  default: ["dist"],
  rebuild: ["clean", willStart("dist")],
  all: ["rebuild"],
  "clean/temp": function() {
    disableErrors = false;
    return cleanByPath([JSDEST + "**/*.js"
      , JSDEST + "**/*.js.map"
      , "!" + JSDEST + "/pages/newtab.js"]);
  },
  clean: ["clean/temp", function() {
    return cleanByPath(DEST + "/*");
  }],

  locally: function() {
    if (locally) { return; }
    enableSourceMap = false;
    willListFiles = true;
    globalForceUpdate = false;
    disableErrors = !globalForceUpdate;
    JSDEST = ".";
    compilerOptions = loadValidCompilerOptions("tsconfig.json", true);
    locally = true;
  },
  "local/scripts": ["locally", function() {
    return compile(["background/*.ts", "content/*.ts", "lib/*.ts"
        , "!lib/polyfill.ts"]);
  }],
  "local/others": ["locally", function() {
    return compile(["front/*.ts", "lib/polyfill.ts"
        , "pages/chrome_ui.ts", "pages/loader.ts"]);
  }],
  "local/pages": ["locally", function() {
    compile(["pages/*.ts", "pages/show.ts"]);
    return compile(["pages/options*.ts", "pages/show.ts"]);
  }],
  local: ["local/scripts", "local/others", "local/others"],
  test: ["local"]
}

typescript = compilerOptions.typescript = loadTypeScriptCompiler();
makeTasks();

function makeCompileTask(src) {
  if (typeof src === "function") { return src; }
  return function() {
    return compile(src);
  };
}

function makeTasks() {
  var hasOwn = Object.prototype.hasOwnProperty;
  for (var key in Tasks) {
    if (!hasOwn.call(Tasks, key)) { continue; }
    var task = Tasks[key];
    if (typeof task === "function") {
      gulp.task(key, task);
    } else if (typeof task === "string") {
      gulp.task(key, Tasks[key] = makeCompileTask(task));
    } else if (typeof task[1] === "function") {
      gulp.task(key, task[0] instanceof Array ? task[0] : [task[0]], task[1]);
    } else if (!/\.ts\b/i.test(task[0])) {
      gulp.task(key, task);
    } else {
      gulp.task(key, Tasks[key] = makeCompileTask(task));
    }
    if (key.slice(-1) !== "/" && fs.existsSync(key) && fs.statSync(key).isDirectory()) {
      gulp.task(key + "/", [key]);
    }
  }
}

function willStart() {
  var taskNames = [].slice.call(arguments, 0);
  return function() {
    // try to run it as a `sync` task if possible
    if (taskNames.length === 1) {
      var task = Tasks[taskNames[0]];
      if (typeof task === "function") {
        return task();
      }
    }
    return gulp.start.apply(gulp, taskNames);
  };
}

function tsProject() {
  return disableErrors ? ts(compilerOptions, ts.reporter.nullReporter()) : ts(compilerOptions);
}

function compile(pathOrStream, skipOutput, forceUpdate) {
  if (typeof pathOrStream === "string") {
    pathOrStream = [pathOrStream];
  }
  if (pathOrStream instanceof Array) {
    pathOrStream.push("!**/*.d.ts");
  }
  if (forceUpdate == null) {
    forceUpdate = globalForceUpdate;
  }
  var stream = pathOrStream instanceof Array ? gulp.src(pathOrStream, { base: "." }) : pathOrStream;
  if (!skipOutput && !forceUpdate) {
    stream = stream.pipe(newer({ dest: JSDEST, ext: '.js', extra: "**/*.d.ts" }));
  }
  if (willListFiles) {
    stream = stream.pipe(gulpPrint());
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.init());
  }
  var project = tsProject();
  var tsResult = stream.pipe(project);
  if (skipOutput) {
    return tsResult;
  }
  return outputJSResult(tsResult.js);
}

function outputJSResult(stream, concatedFile) {
  if (concatedFile) {
    stream = stream.pipe(concat(concatedFile));
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.write("."));
  }
  return stream.pipe(gulp.dest(JSDEST));
}

function uglifyJSFiles(path, output, new_suffix, forceUpdate) {
  if (typeof path === "string") {
    path = [path];
  }
  for (var i = 0; i < path.length; i++) {
    var p = path[i];
    path[i] = p[0] !== "!" ? osPath.join(JSDEST, p) : "!" + osPath.join(JSDEST, p.substring(1));
  }
  path.push("!**/*.min.js");
  output = output || ".";
  new_suffix = new_suffix !== "" ? (new_suffix || ".min") : "";

  var stream = gulp.src(path, { base: JSDEST });
  var is_file = output.indexOf(".js", Math.max(0, output.length - 3)) > 0;
  if (forceUpdate) {
  } else if (is_file) {
    stream = stream.pipe(newer({ dest: osPath.join(DEST, output) }));
  } else {
    stream = stream.pipe(newer({ dest: DEST, ext: new_suffix + '.js' }));
  }
  if (willListFiles) {
    stream = stream.pipe(gulpPrint());
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.init({ loadMaps: true }));
  }
  if (is_file) {
     stream = stream.pipe(concat(output));
  }
  stream = stream.pipe(uglify({
    preserveComments: removeComments ? undefined : "all"
  }));
  if (!is_file && new_suffix !== "") {
     stream = stream.pipe(rename({ suffix: new_suffix }));
  }
  if (enableSourceMap) {
    stream = stream.pipe(sourcemaps.write("."));
  }
  return stream.pipe(gulp.dest(DEST));
}

function copyByPath(path) {
  var stream = gulp.src(path, { base: "." })
    .pipe(newer(DEST));
  if (willListFiles) {
    stream = stream.pipe(gulpPrint());
  }
  return stream.pipe(gulp.dest(DEST));
}

function cleanByPath(path) {
  return gulp.src(path, {read: false}).pipe(clean());
}

function convertToStream(pathOrStream) {
  return typeof pathOrStream === "string" || pathOrStream instanceof Array
    ? gulp.src(pathOrStream, { base: "." }) : pathOrStream;
}

function readFile(fileName, info) {
  info == null && (info = {});
  var buffer = fs.readFileSync(fileName);
  var len = buffer.length;
  if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) {
      // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js,
      // flip all byte pairs and treat as little endian.
      len &= ~1;
      for (var i = 0; i < len; i += 2) {
          const temp = buffer[i];
          buffer[i] = buffer[i + 1];
          buffer[i + 1] = temp;
      }
      info.bom = "\uFFFE";
      return buffer.toString("utf16le", 2);
  }
  if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
      // Little endian UTF-16 byte order mark detected
      info.bom = "\uFEFF";
      return buffer.toString("utf16le", 2);
  }
  if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
      // UTF-8 byte order mark detected
      info.bom = "\uFEFF";
      return buffer.toString("utf8", 3);
  }
  info.bom = "";
  // Default is UTF-8 with no byte order mark
  return buffer.toString("utf8");
}

function _makeJSONReader() {
  var stringOrComment = /"(?:\\[\\\"]|[^"])*"|'(?:\\[\\\']|[^'])*'|\/\/[^\r\n]*|\/\*.*?\*\//g
    , lf = /\r\n|\r(?!\n)/g, notLF = /[^\r\n]+/g, notWhiteSpace = /\S/;
  function spaceN(str) {
    return ' '.repeat(str.length);
  }
  function onReplace(str) {
    switch (str[0]) {
    case '/': case '#':
      if (str.startsWith("/*")) {
        // replace comments with whitespace to preserve original character positions
        return str.replace(notLF, spaceN);
      }
      return spaceN(str);
    case '"': case "'": // falls through
    default:
      return str;
    }
  }
  function readJSON(fileName, throwError) {
    var text = readFile(fileName);
    // text = text.replace(lf, "\n");
    text = text.replace(stringOrComment, onReplace);
    try {
      return notWhiteSpace.test(text) ? JSON.parse(text) : {};
    } catch (e) {
      if (throwError === true) {
        throw e;
      }
      var err = "Failed to parse file '" + fileName + "': " + e + ".";
      console.warn(err);
      return {};
    }
  }
  global._readJSON = readJSON;
}

function readJSON(fileName, throwError) {
  if (!global._readJSON) {
    _makeJSONReader();
  }
  return _readJSON(fileName, throwError);
}

function readCompilerOptions(tsConfigFile, throwError) {
  if (tsConfigFile.lastIndexOf(".json") !== tsConfigFile.length - 5) {
    tsConfigFile += ".json";
  }
  var config = readJSON(tsConfigFile);
  var compilerOptions = config ? config.compilerOptions || {} : null;
  if (compilerOptions && config.extends) {
    var baseFile = osPath.join(osPath.dirname(tsConfigFile), config.extends);
    var baseOptions = readCompilerOptions(baseFile, throwError);
    if (baseOptions) {
      for (var key in baseOptions) {
        if (baseOptions.hasOwnProperty(key) && !(key in compilerOptions)) {
          compilerOptions[key] = baseOptions[key];
        }
      }
    }
  }
  return compilerOptions;
}

function loadValidCompilerOptions(tsConfigFile, keepCustomOptions) {
  var compilerOptions = readCompilerOptions(tsConfigFile, true);
  if (!keepCustomOptions && (keepCustomOptions === false || !compilerOptions.typescript)) {
    delete compilerOptions.inferThisForObjectLiterals;
    delete compilerOptions.narrowFormat;
  }
  if (typescript) {
    compilerOptions.typescript = typescript;
  }
  return compilerOptions;
}

function loadTypeScriptCompiler(path) {
  var typescript;
  path = path || compilerOptions.typescript || null;
  if (typeof path === "string") {
    if (fs.existsSync(path)) {
      if (fs.statSync(path).isDirectory()) {
        path = osPath.join(path, "typescript");
      }
      try {
        typescript = require(path);
      } catch (e) {}
    }
    console.log('Load customized TypeScript compiler:', typescript != null);
  }
  if (typescript == null) {
    typescript = require("typescript/lib/typescript");
  }
  return typescript;
}

function print() {
  return console.log.apply(console, arguments);
}
<|endoftext|>


<commit_before>(function() {
  'use strict';

  var gulp = require('gulp');
  var fs   = require('fs');
  var git  = require('git-rev');
  var rjs  = require('requirejs');

  var clean  = require('gulp-clean');
  var less   = require('gulp-less');
  var mincss = require('gulp-minify-css');
  var rename = require('gulp-rename');

  /**
   * Append '.min' to the base of a filename; use with `gulp-rename`
   */
  var dotmin = function(dir, base, ext) { return base + '.min' + ext; };

  /**
   * Clean existing build files
   */
  gulp.task('clean', function() {
    return gulp.src([
      './build.json',
      './public/css',
      './public/js/*.min.js',
      './frame/css/*.min.css'
    ], { read: false })
    .pipe(clean());
  });

  /**
   * Optimize scripts from AMD modules and build a single compressed JS file
   */
  gulp.task('js', function() {
    rjs.optimize({
      baseUrl: './public/src/js',
      mainConfigFile: './public/src/js/require.config.js',

      name: 'main',
      out: './public/js/debuggerio.min.js',

      pragmas: { bustCache: true },
      findNestedDependencies: true,
      preserveLicenseComments: true,
      useStrict: true,

      exclude: ['promise', 'inflection']
    });
  });

  /**
   * Build LESS files for each theme into a single compressed CSS file
   */
  gulp.task('less', function() {
    gulp.src('./public/src/less/debuggerio.*.less')
      .pipe(less())
      .pipe(mincss())
      .pipe(rename(dotmin))
      .pipe(gulp.dest('./public/css'));
  });

  /**
   * Compress static CSS files
   */
  gulp.task('css', function() {
    gulp.src(['./frame/css/*.css', '!./**/*.min.css'])
      .pipe(mincss())
      .pipe(rename(dotmin))
      .pipe(gulp.dest('./frame/css'));
  });

  /**
   * Write a `build.json` file with build information
   */
  gulp.task('rev', function() {
    git.short(function(rev) {
      fs.writeFile('./build.json', JSON.stringify({
        date: (new Date()).toISOString(),
        rev: rev
      }));
    });
  });

  /**
   * Watch for changes in JS and LESS files
   */
  gulp.task('watch', ['default'], function() {
    gulp.watch('./public/src/js/*.js', ['js']);
    gulp.watch('./public/src/less/*.less', ['less']);
  });

  /**
   * Default task
   */
  gulp.task('default', ['clean'], function() {
    gulp.start('js', 'less', 'css', 'rev');
  });

})();
<commit_msg>Update for gulp-rename<commit_after>(function() {
  'use strict';

  var gulp = require('gulp');
  var fs   = require('fs');
  var git  = require('git-rev');
  var rjs  = require('requirejs');

  var clean  = require('gulp-clean');
  var less   = require('gulp-less');
  var mincss = require('gulp-minify-css');
  var rename = require('gulp-rename');

  /**
   * Append '.min' to the base of a filename; use with `gulp-rename`
   */
  var dotmin = function(path) { path.basename += '.min'; };

  /**
   * Clean existing build files
   */
  gulp.task('clean', function() {
    return gulp.src([
      './build.json',
      './public/css',
      './public/js/*.min.js',
      './frame/css/*.min.css'
    ], { read: false })
    .pipe(clean());
  });

  /**
   * Optimize scripts from AMD modules and build a single compressed JS file
   */
  gulp.task('js', function() {
    rjs.optimize({
      baseUrl: './public/src/js',
      mainConfigFile: './public/src/js/require.config.js',

      name: 'main',
      out: './public/js/debuggerio.min.js',

      pragmas: { bustCache: true },
      findNestedDependencies: true,
      preserveLicenseComments: true,
      useStrict: true,

      exclude: ['promise', 'inflection']
    });
  });

  /**
   * Build LESS files for each theme into a single compressed CSS file
   */
  gulp.task('less', function() {
    gulp.src('./public/src/less/debuggerio.*.less')
      .pipe(less())
      .pipe(mincss())
      .pipe(rename(dotmin))
      .pipe(gulp.dest('./public/css'));
  });

  /**
   * Compress static CSS files
   */
  gulp.task('css', function() {
    gulp.src(['./frame/css/*.css', '!./**/*.min.css'])
      .pipe(mincss())
      .pipe(rename(dotmin))
      .pipe(gulp.dest('./frame/css'));
  });

  /**
   * Write a `build.json` file with build information
   */
  gulp.task('rev', function() {
    git.short(function(rev) {
      fs.writeFile('./build.json', JSON.stringify({
        date: (new Date()).toISOString(),
        rev: rev
      }));
    });
  });

  /**
   * Watch for changes in JS and LESS files
   */
  gulp.task('watch', ['default'], function() {
    gulp.watch('./public/src/js/*.js', ['js']);
    gulp.watch('./public/src/less/*.less', ['less']);
  });

  /**
   * Default task
   */
  gulp.task('default', ['clean'], function() {
    gulp.start('js', 'less', 'css', 'rev');
  });

})();
<|endoftext|>


<commit_before>//
// A View into Space, implemented in HTML DOM and CSS
// This module gives a starting point for implementing
// views in other tech such as Canvas or WebGL
//
// Notes:
//
// [1]
// View has handler for 'added' event.
// View's target space has handler for 'contentAdded' event.
// When a view is parented on a space, the two handlers cause
// duplicate rendering of the view's SpaceNode.
// To go in details, calling _renderElementFor for the space
// attaches 'contentAdded' handler to the space. The handler
// will be fired after execution of the view's 'added'
// handler because the view has been just added as a child
// of the space. To prevent a double call of _renderElementFor
// for the view, the 'contentAdded' handler of the space must
// check if the added child is a view or not. If it is the view,
// then there is no need to render it because the view is
// already rendered by the view's 'added' handler.
//
var createElementFor = require('./createElementFor')
var Vector = require('../Vector')
var SpaceRectangle = require('../SpaceRectangle')
var Space = require('../Space')
var extend = require('extend')
var move = require('move-js')

// Disable animations by default.
move.defaults = { duration: 0 }

var DOM_SPACENODE_PROPERTY = '_taaspace_node'

// Constructor

var SpaceViewHTML = function (space) {
  // Parameters
  //   space

  // Test if valid space
  if (!(space instanceof Space)) {
    throw new Error('Parent of a View must be a Space.')
  }

  SpaceRectangle.call(this)

  // This is the DOM container. Populated in mount().
  this._el = null

  // Mapping from SpaceNode#id to HTMLElement
  // Every HTMLElement created by the view is stored here.
  this._elements = {}
  // Mapping from SpaceNode#id to a map from an event name to a handler fn.
  // Event handlers of every rendered SpaceNode are stored here.
  this._handlers = {}
  // Mapping from an event name to a handler fn.
  // Handlers for the view's events like 'added'. Populated in mount().
  this._viewHandlers = {}

  // View is now ready to be added onto the space.
  // Note that this implicitly sets
  //   this._parent = space
  // Therefore, to access the space, call this.getParent() or this._parent
  this.setParent(space)
}

// Prototype mixin

var p = extend({}, SpaceRectangle.prototype)
SpaceViewHTML.prototype = p

// Public methods

p.getElementBySpaceNode = function (spaceNode) {
  // Get HTML element representation of the space node.
  // If spaceNode is the view itself, return the container.
  // TODO or should return the 0x0 element of the view and require
  // TODO that the container is requested via getHtmlContainer?
  // Return null if not found.
  //
  // if (spaceNode === this) {
  //   return this._el
  // }

  var n = this._elements[spaceNode.id]

  // n will be undefined if not found.
  if (n) {
    return n
  }
  return null
}

p.getHtmlContainer = function () {
  // Return the DOM root element of the space view.
  // Return null if not yet mounted.
  return this._el
}

p.getSpaceNodeByElementId = function (id) {
  // Get SpaceNode by HTML element id
  // Return null if no node for such id.
  var el = document.getElementById(id)

  if (el && el.hasOwnProperty(DOM_SPACENODE_PROPERTY)) {
    return el[DOM_SPACENODE_PROPERTY]
  }

  return null
}

p.isMounted = function () {
  return this._el !== null
}

p.mount = function (htmlContainer) {
  // Begin to use htmlContainer as the DOM root for the space.
  // Design decision:
  //   In v3 the HTML container was provided via the constructor.
  //   This led to a poor coding style where 'new SpaceViewHTML'
  //   was called just for its rendering side-effect. In v4
  //   we wanted the side-effect to be more explicit and made
  //   it a separate method 'mount'.
  //

  // Test if valid dom element
  if (!(htmlContainer && 'tagName' in htmlContainer)) {
    throw new Error('Container should be a DOM Element')
  }

  // If same container, no reason to do anything
  if (this._el === htmlContainer) {
    return
  }

  // Clear the old container
  this.unmount()
  this._el = htmlContainer

  // Init style.
  // Note:
  // "position: relative" is needed to enable "overflow: hidden".
  // Without "overflow: hidden", if transformed elements get outside
  // the initial viewport, Chrome on Android increases
  // the dimensions of the document.
  this._el.style.position = 'relative'
  this._el.style.overflow = 'hidden'
  this._el.style.display = 'block'

  // Init size in space.
  this.setLocalSize(new Vector(this._el.clientWidth, this._el.clientHeight))

  // Render the space.
  // Note that this renders also an element for the SpaceViewHTML.
  // Although the element will be 0x0 and meant for the children of the view.
  this._renderElementFor(this._parent)

  // A HTMLElement was rendered for the space itself.
  // This element is a bit special, because it should not react
  // to transformations of the Space (how could it, Space does not transform)
  // but to transformations of the SpaceViewHTML.

  var view = this
  this._viewHandlers = {
    added: function () {
      // If the view becomes attached to a new parent,
      // render the content of the new parent.
      // See [1] for interaction with the 'contentAdded' handler of the parent.
      view._renderElementFor(view._parent)
    },
    transformed: function () {
      // View becomes transformed.
      // => inverse transform HTMLElement of the Space
      //   => Browser retransforms HTMLElements of the descendants.
      // =>
      var el, vel, tr, prec, s, r, tx, ty
      el = view._elements[view._parent.id]
      vel = view._elements[view.id]
      tr = view._T.inverse()
      vtr = view._T

      prec = 8
      s = tr.s.toFixed(prec)
      r = tr.r.toFixed(prec)
      tx = tr.tx.toFixed(prec)
      ty = tr.ty.toFixed(prec)
      move(el).matrix(s, r, -r, s, tx, ty).end()

      // This undoes the transformation for the view's children
      s = vtr.s.toFixed(prec)
      r = vtr.r.toFixed(prec)
      tx = vtr.tx.toFixed(prec)
      ty = vtr.ty.toFixed(prec)
      move(vel).matrix(s, r, -r, s, tx, ty).end()
    }
  }

  this.on('added', this._viewHandlers.added)
  this.on('transformed', this._viewHandlers.transformed)
}

var setParentSuperProto = p.setParent
p.setParent = function (newParent) {
  // Override the SpaceNode#setParent so that only a Space
  // is allowed to become the parent of a SpaceView.
  if (!(newParent instanceof Space)) {
    throw new Error('A View can only be a child of a Space')
  }

  // Remove elements of the oldParent if the parent is about to change.
  // It is easier to remove the elements before _parent changes. That
  // would be the case if removal happens as a reaction to 'removed' event.
  if (this._parent === newParent) {
    return
  }
  if (this._parent !== null) {
    this._removeElementOf(this._parent)
  }

  setParentSuperProto.call(this, newParent)
}

p.unmount = function () {
  // Detach the view from HTML DOM

  if (!this.isMounted()) {
    // No need to remove anything.
    // Unnecessary unmount call can be a programming error
    // but sometimes we just want to be sure the view is unmounted.
    return
  }

  // No need to react to events in the space.
  this.off('added', this._viewHandlers.added)
  this.off('transformed', this._viewHandlers.transformed)
  this._viewHandlers = {}

  // Detach all rendered elements.
  this._removeElementOf(this._parent)
}

// Private(ish) methods

p._getViewSpecificId = function (spaceNodeId) {
  // Each rendered element has own ID. The ID differs from
  // the id of space nodes because a space node can become
  // visualized through multiple views.
  return this.id + '-' + spaceNodeId
}

p._removeElementOf = function (spaceNode) {
  // Removes the HTMLElement of spaceNode.
  // Does not remove the spaceNode from the view,
  // only from DOM.
  //
  // Note that the give spaceNode is probably already detached from
  // the Space. Therefore spaceNode.isRoot() probably returns true and
  // cannot be used to determine if the spaceNode is space.
  var el, on
  var view = this
  var n = spaceNode  // alias

  if (!this.isMounted()) {
    throw new Error('Cannot remove element when view is not mounted.')
  }

  // Recursively remove the children first
  n.getChildren().forEach(function (child) {
    view._removeElementOf(child)
  })

  // Remove event handlers and the element.
  on = this._handlers[n.id]

  if (n === this || n === this._parent) {
    // When SpaceNode is the Space or SpaceViewHTML itself,
    // only a handler for contentAdded has been created.
    n.off('contentAdded', on.contentAdded)
  } else {
    n.off('contentAdded', on.contentAdded)
    n.off('removed', on.removed)
    n.off('resized', on.resized)
    n.off('transformed', on.transformed)
  }

  el = this._elements[n.id]
  delete el[DOM_SPACENODE_PROPERTY]
  delete this._elements[n.id]
  delete this._handlers[n.id]

  // Remove from DOM
  el.parentElement.removeChild(el)
}

p._renderElementFor = function (spaceNode) {
  // Creates the element for spaceNode and renders it
  // to the view. Renders also the children of the node.
  var el, parentEl, on
  var view = this
  var n = spaceNode

  // Ensure that the view if mounted to DOM. Otherwise
  // asking for the render is a bug.
  if (!this.isMounted()) {
    throw new Error('Do not render elements before mounting the view')
  }

  // Prevent bugs that double-render elements
  if (this._elements.hasOwnProperty(n.id)) {
    throw new Error('An element should not be added twice to the same view.')
  }

  // Create HTMLElement for the node.
  // If the node is Space, SpaceGroup, or SpaceViewHTML,
  // a special 0x0 div is created.
  // Provide SpaceViewHTML constructor to avoid a circular dependency.
  el = createElementFor(n, SpaceViewHTML)
  // Each must have unique ID so we can reference to them.
  el.id = this._getViewSpecificId(n.id)

  // Allow reference from the element to the spaceNode and vice versa.
  // It becomes important to carefully undo the reference
  // to prevent memory leaks.
  el[DOM_SPACENODE_PROPERTY] = n
  this._elements[n.id] = el

  if (n === this._parent) {
    // SpaceNode is Space. Space has a special handling for its HTMLElement.
    // The element is transformed only when the view transforms.
    // This is the way how the transformation of the view affects to
    // the CSS3 transforms of HTMLElements of the children
    // of the space. The browser takes a product of the CSS3 transforms
    // of HTMLElement and its parents.

    // Add the element to the view's container.
    this._el.appendChild(el)

    // Define how view should react to changes in Space.
    // Note that Space cannot emit 'removed', 'resized', or 'transformed'
    on = {
      contentAdded: function (childNode) {
        // Child added to parent, therefore render it to view.
        // However, if the child is the view itself, do not render
        // because the view has been already rendered in the view's
        // handler for the 'added' event. See note [1] for details.
        if (childNode !== view) {
          view._renderElementFor(childNode)
        }
      }
    }
    n.on('contentAdded', on.contentAdded)
  } else {
    // Is a descendant of Space

    // Add the new element to the parent element.
    parentEl = this._elements[n.getParent().id]
    parentEl.appendChild(el)

    // Define how view should react to changes in SpaceNode
    // by setting up handlers.

    if (n === this) {
      // View itself is a descendant of Space and therefore _renderElementFor
      // is called also for the view's SpaceNode.
      //
      // If the view's SpaceNode becomes removed, the view has
      // a special handling for that, defined in unmount().
      //
      // If the view's SpaceNode becomes resized, the view has
      // a special handler for that, defined in mount() TODO.
      // The HTMLElement for the view is a 0x0 and should remain so
      // and therefore we cannot use the default _resizeElementOf.
      //
      // If the view becomes transformed, there is no need to
      // transform it's HTMLElement because the element should stay still.
      // By avoiding unnecessary retransformation we also avoid possible
      // rounding errors.
      //
      // Note that we bound has contentAdded handler also for the view.
      // This way we can fix elements to the view, like a health bar, or
      // dropdown menu.
      //
      on = {
        contentAdded: function (childNode) {
          // Child added
          view._renderElementFor(childNode)
        }
      }
      n.on('contentAdded', on.contentAdded)
    } else {
      on = {
        contentAdded: function (child) {
          view._renderElementFor(child)
        },
        removed: function () {
          view._removeElementOf(n)
        },
        resized: function () {
          view._resizeElementOf(n)
        },
        transformed: function () {
          view._transformElementOf(n)
        }
      }

      n.on('contentAdded', on.contentAdded)
      n.on('removed', on.removed)
      n.on('resized', on.resized)
      n.on('transformed', on.transformed)

      // Initial size and transformation
      if (n instanceof SpaceRectangle) {
        on.resized()
      }
      on.transformed()
    }
  }

  // Store for _removeElementOf
  this._handlers[n.id] = on

  // Repeat for children to render everything.
  n.getChildren().forEach(function (child) {
    view._renderElementFor(child)
  })
}

p._resizeElementOf = function (spaceRectangle) {
  var el, wh
  wh = spaceRectangle.getLocalSize()
  el = this._elements[spaceRectangle.id]
  el.style.width = wh.x + 'px'
  el.style.height = wh.y + 'px'
}

p._transformElementOf = function (spaceNode) {
  // Update transformation of a HTMLElement.
  // They are HTMLElements parented on view's container, not nodes on space.
  //
  var el, tr, prec, s, r, tx, ty

  el = this._elements[spaceNode.id]

  // Node's local transformation.
  tr = spaceNode._T

  // Current move.js does not prevent scientific notation reaching CSS
  // which leads to problems with Safari and Opera. Therefore we must
  // prevent the notation here.
  // Of course this will cause a small deviation in the presentation.
  // However the deviation is only in the presentation and thus not a problem.
  prec = 8
  s = tr.s.toFixed(prec)
  r = tr.r.toFixed(prec)
  tx = tr.tx.toFixed(prec)
  ty = tr.ty.toFixed(prec)
  move(el).matrix(s, r, -r, s, tx, ty).end()

  // TODO el.style.transform = 'matrix(' +
}

module.exports = SpaceViewHTML
<commit_msg>Fix lint issue<commit_after>//
// A View into Space, implemented in HTML DOM and CSS
// This module gives a starting point for implementing
// views in other tech such as Canvas or WebGL
//
// Notes:
//
// [1]
// View has handler for 'added' event.
// View's target space has handler for 'contentAdded' event.
// When a view is parented on a space, the two handlers cause
// duplicate rendering of the view's SpaceNode.
// To go in details, calling _renderElementFor for the space
// attaches 'contentAdded' handler to the space. The handler
// will be fired after execution of the view's 'added'
// handler because the view has been just added as a child
// of the space. To prevent a double call of _renderElementFor
// for the view, the 'contentAdded' handler of the space must
// check if the added child is a view or not. If it is the view,
// then there is no need to render it because the view is
// already rendered by the view's 'added' handler.
//
var createElementFor = require('./createElementFor')
var Vector = require('../Vector')
var SpaceRectangle = require('../SpaceRectangle')
var Space = require('../Space')
var extend = require('extend')
var move = require('move-js')

// Disable animations by default.
move.defaults = { duration: 0 }

var DOM_SPACENODE_PROPERTY = '_taaspace_node'

// Constructor

var SpaceViewHTML = function (space) {
  // Parameters
  //   space

  // Test if valid space
  if (!(space instanceof Space)) {
    throw new Error('Parent of a View must be a Space.')
  }

  SpaceRectangle.call(this)

  // This is the DOM container. Populated in mount().
  this._el = null

  // Mapping from SpaceNode#id to HTMLElement
  // Every HTMLElement created by the view is stored here.
  this._elements = {}
  // Mapping from SpaceNode#id to a map from an event name to a handler fn.
  // Event handlers of every rendered SpaceNode are stored here.
  this._handlers = {}
  // Mapping from an event name to a handler fn.
  // Handlers for the view's events like 'added'. Populated in mount().
  this._viewHandlers = {}

  // View is now ready to be added onto the space.
  // Note that this implicitly sets
  //   this._parent = space
  // Therefore, to access the space, call this.getParent() or this._parent
  this.setParent(space)
}

// Prototype mixin

var p = extend({}, SpaceRectangle.prototype)
SpaceViewHTML.prototype = p

// Public methods

p.getElementBySpaceNode = function (spaceNode) {
  // Get HTML element representation of the space node.
  // If spaceNode is the view itself, return the container.
  // TODO or should return the 0x0 element of the view and require
  // TODO that the container is requested via getHtmlContainer?
  // Return null if not found.
  //
  // if (spaceNode === this) {
  //   return this._el
  // }

  var n = this._elements[spaceNode.id]

  // n will be undefined if not found.
  if (n) {
    return n
  }
  return null
}

p.getHtmlContainer = function () {
  // Return the DOM root element of the space view.
  // Return null if not yet mounted.
  return this._el
}

p.getSpaceNodeByElementId = function (id) {
  // Get SpaceNode by HTML element id
  // Return null if no node for such id.
  var el = document.getElementById(id)

  if (el && el.hasOwnProperty(DOM_SPACENODE_PROPERTY)) {
    return el[DOM_SPACENODE_PROPERTY]
  }

  return null
}

p.isMounted = function () {
  return this._el !== null
}

p.mount = function (htmlContainer) {
  // Begin to use htmlContainer as the DOM root for the space.
  // Design decision:
  //   In v3 the HTML container was provided via the constructor.
  //   This led to a poor coding style where 'new SpaceViewHTML'
  //   was called just for its rendering side-effect. In v4
  //   we wanted the side-effect to be more explicit and made
  //   it a separate method 'mount'.
  //

  // Test if valid dom element
  if (!(htmlContainer && 'tagName' in htmlContainer)) {
    throw new Error('Container should be a DOM Element')
  }

  // If same container, no reason to do anything
  if (this._el === htmlContainer) {
    return
  }

  // Clear the old container
  this.unmount()
  this._el = htmlContainer

  // Init style.
  // Note:
  // "position: relative" is needed to enable "overflow: hidden".
  // Without "overflow: hidden", if transformed elements get outside
  // the initial viewport, Chrome on Android increases
  // the dimensions of the document.
  this._el.style.position = 'relative'
  this._el.style.overflow = 'hidden'
  this._el.style.display = 'block'

  // Init size in space.
  this.setLocalSize(new Vector(this._el.clientWidth, this._el.clientHeight))

  // Render the space.
  // Note that this renders also an element for the SpaceViewHTML.
  // Although the element will be 0x0 and meant for the children of the view.
  this._renderElementFor(this._parent)

  // A HTMLElement was rendered for the space itself.
  // This element is a bit special, because it should not react
  // to transformations of the Space (how could it, Space does not transform)
  // but to transformations of the SpaceViewHTML.

  var view = this
  this._viewHandlers = {
    added: function () {
      // If the view becomes attached to a new parent,
      // render the content of the new parent.
      // See [1] for interaction with the 'contentAdded' handler of the parent.
      view._renderElementFor(view._parent)
    },
    transformed: function () {
      // View becomes transformed.
      // => inverse transform HTMLElement of the Space
      //   => Browser retransforms HTMLElements of the descendants.
      // =>
      var el, vel, tr, vtr, prec, s, r, tx, ty
      el = view._elements[view._parent.id]
      vel = view._elements[view.id]
      tr = view._T.inverse()
      vtr = view._T

      prec = 8
      s = tr.s.toFixed(prec)
      r = tr.r.toFixed(prec)
      tx = tr.tx.toFixed(prec)
      ty = tr.ty.toFixed(prec)
      move(el).matrix(s, r, -r, s, tx, ty).end()

      // This undoes the transformation for the view's children
      s = vtr.s.toFixed(prec)
      r = vtr.r.toFixed(prec)
      tx = vtr.tx.toFixed(prec)
      ty = vtr.ty.toFixed(prec)
      move(vel).matrix(s, r, -r, s, tx, ty).end()
    }
  }

  this.on('added', this._viewHandlers.added)
  this.on('transformed', this._viewHandlers.transformed)
}

var setParentSuperProto = p.setParent
p.setParent = function (newParent) {
  // Override the SpaceNode#setParent so that only a Space
  // is allowed to become the parent of a SpaceView.
  if (!(newParent instanceof Space)) {
    throw new Error('A View can only be a child of a Space')
  }

  // Remove elements of the oldParent if the parent is about to change.
  // It is easier to remove the elements before _parent changes. That
  // would be the case if removal happens as a reaction to 'removed' event.
  if (this._parent === newParent) {
    return
  }
  if (this._parent !== null) {
    this._removeElementOf(this._parent)
  }

  setParentSuperProto.call(this, newParent)
}

p.unmount = function () {
  // Detach the view from HTML DOM

  if (!this.isMounted()) {
    // No need to remove anything.
    // Unnecessary unmount call can be a programming error
    // but sometimes we just want to be sure the view is unmounted.
    return
  }

  // No need to react to events in the space.
  this.off('added', this._viewHandlers.added)
  this.off('transformed', this._viewHandlers.transformed)
  this._viewHandlers = {}

  // Detach all rendered elements.
  this._removeElementOf(this._parent)
}

// Private(ish) methods

p._getViewSpecificId = function (spaceNodeId) {
  // Each rendered element has own ID. The ID differs from
  // the id of space nodes because a space node can become
  // visualized through multiple views.
  return this.id + '-' + spaceNodeId
}

p._removeElementOf = function (spaceNode) {
  // Removes the HTMLElement of spaceNode.
  // Does not remove the spaceNode from the view,
  // only from DOM.
  //
  // Note that the give spaceNode is probably already detached from
  // the Space. Therefore spaceNode.isRoot() probably returns true and
  // cannot be used to determine if the spaceNode is space.
  var el, on
  var view = this
  var n = spaceNode  // alias

  if (!this.isMounted()) {
    throw new Error('Cannot remove element when view is not mounted.')
  }

  // Recursively remove the children first
  n.getChildren().forEach(function (child) {
    view._removeElementOf(child)
  })

  // Remove event handlers and the element.
  on = this._handlers[n.id]

  if (n === this || n === this._parent) {
    // When SpaceNode is the Space or SpaceViewHTML itself,
    // only a handler for contentAdded has been created.
    n.off('contentAdded', on.contentAdded)
  } else {
    n.off('contentAdded', on.contentAdded)
    n.off('removed', on.removed)
    n.off('resized', on.resized)
    n.off('transformed', on.transformed)
  }

  el = this._elements[n.id]
  delete el[DOM_SPACENODE_PROPERTY]
  delete this._elements[n.id]
  delete this._handlers[n.id]

  // Remove from DOM
  el.parentElement.removeChild(el)
}

p._renderElementFor = function (spaceNode) {
  // Creates the element for spaceNode and renders it
  // to the view. Renders also the children of the node.
  var el, parentEl, on
  var view = this
  var n = spaceNode

  // Ensure that the view if mounted to DOM. Otherwise
  // asking for the render is a bug.
  if (!this.isMounted()) {
    throw new Error('Do not render elements before mounting the view')
  }

  // Prevent bugs that double-render elements
  if (this._elements.hasOwnProperty(n.id)) {
    throw new Error('An element should not be added twice to the same view.')
  }

  // Create HTMLElement for the node.
  // If the node is Space, SpaceGroup, or SpaceViewHTML,
  // a special 0x0 div is created.
  // Provide SpaceViewHTML constructor to avoid a circular dependency.
  el = createElementFor(n, SpaceViewHTML)
  // Each must have unique ID so we can reference to them.
  el.id = this._getViewSpecificId(n.id)

  // Allow reference from the element to the spaceNode and vice versa.
  // It becomes important to carefully undo the reference
  // to prevent memory leaks.
  el[DOM_SPACENODE_PROPERTY] = n
  this._elements[n.id] = el

  if (n === this._parent) {
    // SpaceNode is Space. Space has a special handling for its HTMLElement.
    // The element is transformed only when the view transforms.
    // This is the way how the transformation of the view affects to
    // the CSS3 transforms of HTMLElements of the children
    // of the space. The browser takes a product of the CSS3 transforms
    // of HTMLElement and its parents.

    // Add the element to the view's container.
    this._el.appendChild(el)

    // Define how view should react to changes in Space.
    // Note that Space cannot emit 'removed', 'resized', or 'transformed'
    on = {
      contentAdded: function (childNode) {
        // Child added to parent, therefore render it to view.
        // However, if the child is the view itself, do not render
        // because the view has been already rendered in the view's
        // handler for the 'added' event. See note [1] for details.
        if (childNode !== view) {
          view._renderElementFor(childNode)
        }
      }
    }
    n.on('contentAdded', on.contentAdded)
  } else {
    // Is a descendant of Space

    // Add the new element to the parent element.
    parentEl = this._elements[n.getParent().id]
    parentEl.appendChild(el)

    // Define how view should react to changes in SpaceNode
    // by setting up handlers.

    if (n === this) {
      // View itself is a descendant of Space and therefore _renderElementFor
      // is called also for the view's SpaceNode.
      //
      // If the view's SpaceNode becomes removed, the view has
      // a special handling for that, defined in unmount().
      //
      // If the view's SpaceNode becomes resized, the view has
      // a special handler for that, defined in mount() TODO.
      // The HTMLElement for the view is a 0x0 and should remain so
      // and therefore we cannot use the default _resizeElementOf.
      //
      // If the view becomes transformed, there is no need to
      // transform it's HTMLElement because the element should stay still.
      // By avoiding unnecessary retransformation we also avoid possible
      // rounding errors.
      //
      // Note that we bound has contentAdded handler also for the view.
      // This way we can fix elements to the view, like a health bar, or
      // dropdown menu.
      //
      on = {
        contentAdded: function (childNode) {
          // Child added
          view._renderElementFor(childNode)
        }
      }
      n.on('contentAdded', on.contentAdded)
    } else {
      on = {
        contentAdded: function (child) {
          view._renderElementFor(child)
        },
        removed: function () {
          view._removeElementOf(n)
        },
        resized: function () {
          view._resizeElementOf(n)
        },
        transformed: function () {
          view._transformElementOf(n)
        }
      }

      n.on('contentAdded', on.contentAdded)
      n.on('removed', on.removed)
      n.on('resized', on.resized)
      n.on('transformed', on.transformed)

      // Initial size and transformation
      if (n instanceof SpaceRectangle) {
        on.resized()
      }
      on.transformed()
    }
  }

  // Store for _removeElementOf
  this._handlers[n.id] = on

  // Repeat for children to render everything.
  n.getChildren().forEach(function (child) {
    view._renderElementFor(child)
  })
}

p._resizeElementOf = function (spaceRectangle) {
  var el, wh
  wh = spaceRectangle.getLocalSize()
  el = this._elements[spaceRectangle.id]
  el.style.width = wh.x + 'px'
  el.style.height = wh.y + 'px'
}

p._transformElementOf = function (spaceNode) {
  // Update transformation of a HTMLElement.
  // They are HTMLElements parented on view's container, not nodes on space.
  //
  var el, tr, prec, s, r, tx, ty

  el = this._elements[spaceNode.id]

  // Node's local transformation.
  tr = spaceNode._T

  // Current move.js does not prevent scientific notation reaching CSS
  // which leads to problems with Safari and Opera. Therefore we must
  // prevent the notation here.
  // Of course this will cause a small deviation in the presentation.
  // However the deviation is only in the presentation and thus not a problem.
  prec = 8
  s = tr.s.toFixed(prec)
  r = tr.r.toFixed(prec)
  tx = tr.tx.toFixed(prec)
  ty = tr.ty.toFixed(prec)
  move(el).matrix(s, r, -r, s, tx, ty).end()

  // TODO el.style.transform = 'matrix(' +
}

module.exports = SpaceViewHTML
<|endoftext|>


<commit_before>/* eslint-disable no-underscore-dangle */

jest.mock('fs-promise');
jest.mock('chokidar');
jest.mock('nunjucks');

jest.mock('../utils/glob');
jest.mock('../utils/createServer');
jest.mock('../utils/log');
jest.mock('../utils/getConfig');
jest.mock('../plugins/addTOC');
jest.mock('../plugins/load');
jest.mock('../utils/getList');

const chokidar = require('chokidar');
const Wikic = require('../index');
const glob = require('../utils/glob');
const logger = require('../utils/log');
const fsp = require('fs-promise');
const path = require('path');

glob.__setPath('**/*', ['404.md', 'styles/main.css']);
glob.__setPath('**/*.md', ['css/flexbox.md', 'html/hi.md']);
glob.__setPath('**/', ['css/', 'html/']);

describe('wikic build', () => {
  describe('afterRead & beforeWrite', () => {
    it('beforeWrite works', async () => {
      const wikic = new Wikic();
      const callback = jest.fn(context => context);
      const length = wikic._beforeWriteTasks.length;

      expect(wikic.beforeWrite(callback)).toBe(wikic);
      expect(wikic._beforeWriteTasks.length).toBe(length + 1);
      await wikic.build();
      expect(callback.mock.calls.length).not.toBeLessThan(1);
    });

    it('beforeWrite throws if not pass a function', () => {
      const wikic = new Wikic();
      try {
        wikic.beforeWrite();
      } catch (e) {
        expect(e.message).toBe('should pass a function');
      }
    });

    it('afterRead works', async () => {
      const wikic = new Wikic();
      const callback = jest.fn(context => context);
      const length = wikic._afterReadTasks.length;

      expect(wikic.afterRead(callback)).toBe(wikic);
      expect(wikic._afterReadTasks.length).toBe(length + 1);
      await wikic.build();
      expect(callback.mock.calls.length).not.toBeLessThan(1);
    });

    it('afterRead throws if not pass a function', () => {
      const wikic = new Wikic();
      try {
        wikic.afterRead();
      } catch (e) {
        expect(e.message).toBe('should pass a function');
      }
    });
  });

  describe('build', () => {
    it('call build() will call once only', () => {
      logger.error.mockClear();
      const wikic = new Wikic();
      wikic.build();
      wikic.build();
      expect(logger.error.mock.calls.length).toBe(1);
    });
  });

  describe('readMD', () => {
    it('works if src not in context', async () => {
      const wikic = new Wikic();
      await wikic.readMD({
        data: '',
        site: {},
      });
    });
  });

  describe('nunjucks', () => {
    it('receive funcs', () => {
      const wikic = new Wikic();
      expect(wikic.renderer.addFilter).toHaveBeenCalledTimes(3);
      wikic.config.baseurl = 'base';
      expect(wikic.renderer.addFilter.mock.calls[0][0]).toBe('baseurl');
      expect(wikic.renderer.addFilter.mock.calls[0][1]('a/b/c')).toBe(wikic.getURL('a/b/c'));

      expect(wikic.renderer.addFilter.mock.calls[1][0]).toBe('typeMap');
      expect(wikic.renderer.addFilter.mock.calls[1][1]('css')).toBe(wikic.typeMap('css'));

      expect(wikic.renderer.addFilter.mock.calls[2][0]).toBe('typeMaps');
      expect(wikic.renderer.addFilter.mock.calls[2][1](['css', 'html'])).toEqual(['CSS', 'HTML']);
    });

    it('relative', () => {
      const wikic = new Wikic();
      wikic.publicPath = path.resolve('a', 'b');
      wikic.config.baseurl = 'base';
      wikic.renderer.addFilter.mockClear();
      wikic.renderMD({ data: '', page: {}, renderContext: {}, dist: path.resolve('a', 'b', 'c', 'd.html') });
      expect(wikic.renderer.addFilter).toHaveBeenCalledTimes(1);
      expect(wikic.renderer.addFilter.mock.calls[0][0]).toBe('relative');
      expect(wikic.renderer.addFilter.mock.calls[0][1]('/base/c/f.html')).toBe('f.html');
    });
  });
});

describe('watch', () => {
  it('works', () => {
    logger.error.mockClear();
    const wikic = new Wikic();
    wikic.watch();
    const setup = jest.spyOn(wikic, 'setup');
    const build = jest.spyOn(wikic, 'build');

    chokidar.__emiter.emit('change', '/path/to/');
    expect(setup).toHaveBeenCalled();
    expect(build).toHaveBeenCalled();
    setup.mockReset();
    setup.mockRestore();
    build.mockReset();
    build.mockRestore();

    chokidar.__emiter.emit('unlink', 'text.txt');
    expect(fsp.removeSync.mock.calls[0][0]).toMatch(path.join('docs', 'text.txt'));

    chokidar.__emiter.emit('error', Error('oops'));
    expect(logger.error.mock.calls.length).toBe(1);
    expect(logger.error.mock.calls[0][0]).toMatch('Error: oops');
  });

  test('ignored', () => {
    const wikic = new Wikic();

    chokidar.watch.mockClear();
    wikic.config.layoutPath = '_layout';
    wikic.config.docsPath = '_doc';
    wikic.watch();
    expect(chokidar.watch.mock.calls[0][1].ignored.pop()).toBe('_!(layout|doc)/**');

    chokidar.watch.mockClear();
    wikic.config.layoutPath = 'layout';
    wikic.config.docsPath = 'doc';
    wikic.watch();
    expect(chokidar.watch.mock.calls[0][1].ignored.pop()).toBe('_*/**');
  });
});

describe('clean', async () => {
  const wikic = new Wikic();
  expect(await wikic.clean()).toBe(wikic);
  expect(fsp.emptyDir.mock.calls[0][0]).toBe(wikic.publicPath);
});
<commit_msg>test(filters): change test descriptions<commit_after>/* eslint-disable no-underscore-dangle */

jest.mock('fs-promise');
jest.mock('chokidar');
jest.mock('nunjucks');

jest.mock('../utils/glob');
jest.mock('../utils/createServer');
jest.mock('../utils/log');
jest.mock('../utils/getConfig');
jest.mock('../plugins/addTOC');
jest.mock('../plugins/load');
jest.mock('../utils/getList');

const chokidar = require('chokidar');
const Wikic = require('../index');
const glob = require('../utils/glob');
const logger = require('../utils/log');
const fsp = require('fs-promise');
const path = require('path');

glob.__setPath('**/*', ['404.md', 'styles/main.css']);
glob.__setPath('**/*.md', ['css/flexbox.md', 'html/hi.md']);
glob.__setPath('**/', ['css/', 'html/']);

describe('wikic build', () => {
  describe('afterRead & beforeWrite', () => {
    it('beforeWrite works', async () => {
      const wikic = new Wikic();
      const callback = jest.fn(context => context);
      const length = wikic._beforeWriteTasks.length;

      expect(wikic.beforeWrite(callback)).toBe(wikic);
      expect(wikic._beforeWriteTasks.length).toBe(length + 1);
      await wikic.build();
      expect(callback.mock.calls.length).not.toBeLessThan(1);
    });

    it('beforeWrite throws if not pass a function', () => {
      const wikic = new Wikic();
      try {
        wikic.beforeWrite();
      } catch (e) {
        expect(e.message).toBe('should pass a function');
      }
    });

    it('afterRead works', async () => {
      const wikic = new Wikic();
      const callback = jest.fn(context => context);
      const length = wikic._afterReadTasks.length;

      expect(wikic.afterRead(callback)).toBe(wikic);
      expect(wikic._afterReadTasks.length).toBe(length + 1);
      await wikic.build();
      expect(callback.mock.calls.length).not.toBeLessThan(1);
    });

    it('afterRead throws if not pass a function', () => {
      const wikic = new Wikic();
      try {
        wikic.afterRead();
      } catch (e) {
        expect(e.message).toBe('should pass a function');
      }
    });
  });

  describe('build', () => {
    it('call build() will call once only', () => {
      logger.error.mockClear();
      const wikic = new Wikic();
      wikic.build();
      wikic.build();
      expect(logger.error.mock.calls.length).toBe(1);
    });
  });

  describe('readMD', () => {
    it('works if src not in context', async () => {
      const wikic = new Wikic();
      await wikic.readMD({
        data: '',
        site: {},
      });
    });
  });

  describe('nunjucks', () => {
    it('baseurl, typeMap, typeMaps Filter', () => {
      const wikic = new Wikic();
      expect(wikic.renderer.addFilter).toHaveBeenCalledTimes(3);
      wikic.config.baseurl = 'base';
      expect(wikic.renderer.addFilter.mock.calls[0][0]).toBe('baseurl');
      expect(wikic.renderer.addFilter.mock.calls[0][1]('a/b/c')).toBe(wikic.getURL('a/b/c'));

      expect(wikic.renderer.addFilter.mock.calls[1][0]).toBe('typeMap');
      expect(wikic.renderer.addFilter.mock.calls[1][1]('css')).toBe(wikic.typeMap('css'));

      expect(wikic.renderer.addFilter.mock.calls[2][0]).toBe('typeMaps');
      expect(wikic.renderer.addFilter.mock.calls[2][1](['css', 'html'])).toEqual(['CSS', 'HTML']);
    });

    it('relative Filter', () => {
      const wikic = new Wikic();
      wikic.publicPath = path.resolve('a', 'b');
      wikic.config.baseurl = 'base';
      wikic.renderer.addFilter.mockClear();
      wikic.renderMD({ data: '', page: {}, renderContext: {}, dist: path.resolve('a', 'b', 'c', 'd.html') });
      expect(wikic.renderer.addFilter).toHaveBeenCalledTimes(1);
      expect(wikic.renderer.addFilter.mock.calls[0][0]).toBe('relative');
      expect(wikic.renderer.addFilter.mock.calls[0][1]('/base/c/f.html')).toBe('f.html');
    });
  });
});

describe('watch', () => {
  it('works', () => {
    logger.error.mockClear();
    const wikic = new Wikic();
    wikic.watch();
    const setup = jest.spyOn(wikic, 'setup');
    const build = jest.spyOn(wikic, 'build');

    chokidar.__emiter.emit('change', '/path/to/');
    expect(setup).toHaveBeenCalled();
    expect(build).toHaveBeenCalled();
    setup.mockReset();
    setup.mockRestore();
    build.mockReset();
    build.mockRestore();

    chokidar.__emiter.emit('unlink', 'text.txt');
    expect(fsp.removeSync.mock.calls[0][0]).toMatch(path.join('docs', 'text.txt'));

    chokidar.__emiter.emit('error', Error('oops'));
    expect(logger.error.mock.calls.length).toBe(1);
    expect(logger.error.mock.calls[0][0]).toMatch('Error: oops');
  });

  test('ignored', () => {
    const wikic = new Wikic();

    chokidar.watch.mockClear();
    wikic.config.layoutPath = '_layout';
    wikic.config.docsPath = '_doc';
    wikic.watch();
    expect(chokidar.watch.mock.calls[0][1].ignored.pop()).toBe('_!(layout|doc)/**');

    chokidar.watch.mockClear();
    wikic.config.layoutPath = 'layout';
    wikic.config.docsPath = 'doc';
    wikic.watch();
    expect(chokidar.watch.mock.calls[0][1].ignored.pop()).toBe('_*/**');
  });
});

describe('clean', async () => {
  const wikic = new Wikic();
  expect(await wikic.clean()).toBe(wikic);
  expect(fsp.emptyDir.mock.calls[0][0]).toBe(wikic.publicPath);
});
<|endoftext|>


<commit_before>//
// A View into Space, implemented in HTML DOM and CSS
// This module gives a starting point for implementing
// views in other tech such as Canvas or WebGL
//
// Notes:
//
// [1]
// View has handler for 'added' event.
// View's target space has handler for 'contentAdded' event.
// When a view is parented on a space, the two handlers cause
// duplicate rendering of the view's SpaceNode.
// To go in details, calling _renderElementFor for the space
// attaches 'contentAdded' handler to the space. The handler
// will be fired after execution of the view's 'added'
// handler because the view has been just added as a child
// of the space. To prevent a double call of _renderElementFor
// for the view, the 'contentAdded' handler of the space must
// check if the added child is a view or not. If it is the view,
// then there is no need to render it because the view is
// already rendered by the view's 'added' handler.
//
var createElementFor = require('./createElementFor')
var Vector = require('../Vector')
var SpaceRectangle = require('../SpaceRectangle')
var Space = require('../Space')
var extend = require('extend')
var move = require('move-js')

// Disable animations by default.
move.defaults = { duration: 0 }

var DOM_SPACENODE_PROPERTY = '_taaspace_node'

// Constructor

var SpaceViewHTML = function (space) {
  // Parameters
  //   space

  // Test if valid space
  if (!(space instanceof Space)) {
    throw new Error('Parent of a View must be a Space.')
  }

  SpaceRectangle.call(this)

  // This is the DOM container. Populated in mount().
  this._el = null

  // Mapping from SpaceNode#id to HTMLElement
  // Every HTMLElement created by the view is stored here.
  this._elements = {}
  // Mapping from SpaceNode#id to a map from an event name to a handler fn.
  // Event handlers of every rendered SpaceNode are stored here.
  this._handlers = {}
  // Mapping from an event name to a handler fn.
  // Handlers for the view's events like 'added'. Populated in mount().
  this._viewHandlers = {}

  // View is now ready to be added onto the space.
  // Note that this implicitly sets
  //   this._parent = space
  // Therefore, to access the space, call this.getParent() or this._parent
  this.setParent(space)
}

// Prototype mixin

var p = extend({}, SpaceRectangle.prototype)
SpaceViewHTML.prototype = p

// Public methods

p.getElementBySpaceNode = function (spaceNode) {
  // Get HTML element representation of the space node.
  // If spaceNode is the view itself, return the container.
  // TODO or should return the 0x0 element of the view and require
  // TODO that the container is requested via getHtmlContainer?
  // Return null if not found.
  //
  // if (spaceNode === this) {
  //   return this._el
  // }

  var n = this._elements[spaceNode.id]

  // n will be undefined if not found.
  if (n) {
    return n
  }
  return null
}

p.getHtmlContainer = function () {
  // Return the DOM root element of the space view.
  // Return null if not yet mounted.
  return this._el
}

p.getSpaceNodeByElementId = function (id) {
  // Get SpaceNode by HTML element id
  // Return null if no node for such id.
  var el = document.getElementById(id)

  if (el && el.hasOwnProperty(DOM_SPACENODE_PROPERTY)) {
    return el[DOM_SPACENODE_PROPERTY]
  }

  return null
}

p.isMounted = function () {
  return this._el !== null
}

p.mount = function (htmlContainer) {
  // Begin to use htmlContainer as the DOM root for the space.
  // Design decision:
  //   In v3 the HTML container was provided via the constructor.
  //   This led to a poor coding style where 'new SpaceViewHTML'
  //   was called just for its rendering side-effect. In v4
  //   we wanted the side-effect to be more explicit and made
  //   it a separate method 'mount'.
  //

  // Test if valid dom element
  if (!(htmlContainer && 'tagName' in htmlContainer)) {
    throw new Error('Container should be a DOM Element')
  }

  // If same container, no reason to do anything
  if (this._el === htmlContainer) {
    return
  }

  // Clear the old container
  this.unmount()
  this._el = htmlContainer

  // Init style.
  // Note:
  // "position: relative" is needed to enable "overflow: hidden".
  // Without "overflow: hidden", if transformed elements get outside
  // the initial viewport, Chrome on Android increases
  // the dimensions of the document.
  this._el.style.position = 'relative'
  this._el.style.overflow = 'hidden'
  this._el.style.display = 'block'

  // Init size in space.
  this.setLocalSize(new Vector(this._el.clientWidth, this._el.clientHeight))

  // Render the space.
  // Note that this renders also an element for the SpaceViewHTML.
  // Although the element will be 0x0 and meant for the children of the view.
  this._renderElementFor(this._parent)

  // A HTMLElement was rendered for the space itself.
  // This element is a bit special, because it should not react
  // to transformations of the Space (how could it, Space does not transform)
  // but to transformations of the SpaceViewHTML.

  var view = this
  this._viewHandlers = {
    added: function () {
      // If the view becomes attached to a new parent,
      // render the content of the new parent.
      // See [1] for interaction with the 'contentAdded' handler of the parent.
      view._renderElementFor(view._parent)
    },
    transformed: function () {
      // View becomes transformed.
      // => inverse transform HTMLElement of the Space
      //   => Browser retransforms HTMLElements of the descendants.
      // =>
      var el, vel, tr, vtr, prec, s, r, tx, ty
      el = view._elements[view._parent.id]
      vel = view._elements[view.id]
      tr = view._T.inverse()
      vtr = view._T

      prec = 8
      s = tr.s.toFixed(prec)
      r = tr.r.toFixed(prec)
      tx = tr.tx.toFixed(prec)
      ty = tr.ty.toFixed(prec)
      move(el).matrix(s, r, -r, s, tx, ty).end()

      // This undoes the transformation for the view's children
      s = vtr.s.toFixed(prec)
      r = vtr.r.toFixed(prec)
      tx = vtr.tx.toFixed(prec)
      ty = vtr.ty.toFixed(prec)
      move(vel).matrix(s, r, -r, s, tx, ty).end()
    }
  }

  this.on('added', this._viewHandlers.added)
  this.on('transformed', this._viewHandlers.transformed)
}

var setParentSuperProto = p.setParent
p.setParent = function (newParent) {
  // Override the SpaceNode#setParent so that only a Space
  // is allowed to become the parent of a SpaceView.
  if (!(newParent instanceof Space)) {
    throw new Error('A View can only be a child of a Space')
  }

  // Remove elements of the oldParent if the parent is about to change.
  // It is easier to remove the elements before _parent changes. That
  // would be the case if removal happens as a reaction to 'removed' event.
  if (this._parent === newParent) {
    return
  }
  if (this._parent !== null) {
    this._removeElementOf(this._parent)
  }

  setParentSuperProto.call(this, newParent)
}

p.unmount = function () {
  // Detach the view from HTML DOM

  if (!this.isMounted()) {
    // No need to remove anything.
    // Unnecessary unmount call can be a programming error
    // but sometimes we just want to be sure the view is unmounted.
    return
  }

  // No need to react to events in the space.
  this.off('added', this._viewHandlers.added)
  this.off('transformed', this._viewHandlers.transformed)
  this._viewHandlers = {}

  // Detach all rendered elements.
  this._removeElementOf(this._parent)
}

// Private(ish) methods

p._getViewSpecificId = function (spaceNodeId) {
  // Each rendered element has own ID. The ID differs from
  // the id of space nodes because a space node can become
  // visualized through multiple views.
  return this.id + '-' + spaceNodeId
}

p._removeElementOf = function (spaceNode) {
  // Removes the HTMLElement of spaceNode.
  // Does not remove the spaceNode from the view,
  // only from DOM.
  //
  // Note that the give spaceNode is probably already detached from
  // the Space. Therefore spaceNode.isRoot() probably returns true and
  // cannot be used to determine if the spaceNode is space.
  var el, on
  var view = this
  var n = spaceNode  // alias

  if (!this.isMounted()) {
    throw new Error('Cannot remove element when view is not mounted.')
  }

  // Recursively remove the children first
  n.getChildren().forEach(function (child) {
    view._removeElementOf(child)
  })

  // Remove event handlers and the element.
  on = this._handlers[n.id]

  if (n === this || n === this._parent) {
    // When SpaceNode is the Space or SpaceViewHTML itself,
    // only a handler for contentAdded has been created.
    n.off('contentAdded', on.contentAdded)
  } else {
    n.off('contentAdded', on.contentAdded)
    n.off('removed', on.removed)
    n.off('resized', on.resized)
    n.off('transformed', on.transformed)
  }

  el = this._elements[n.id]
  delete el[DOM_SPACENODE_PROPERTY]
  delete this._elements[n.id]
  delete this._handlers[n.id]

  // Remove from DOM
  el.parentElement.removeChild(el)
}

p._renderElementFor = function (spaceNode) {
  // Creates the element for spaceNode and renders it
  // to the view. Renders also the children of the node.
  var el, parentEl, on
  var view = this
  var n = spaceNode

  // Ensure that the view if mounted to DOM. Otherwise
  // asking for the render is a bug.
  if (!this.isMounted()) {
    throw new Error('Do not render elements before mounting the view')
  }

  // Prevent bugs that double-render elements
  if (this._elements.hasOwnProperty(n.id)) {
    throw new Error('An element should not be added twice to the same view.')
  }

  // Create HTMLElement for the node.
  // If the node is Space, SpaceGroup, or SpaceViewHTML,
  // a special 0x0 div is created.
  // Provide SpaceViewHTML constructor to avoid a circular dependency.
  el = createElementFor(n, SpaceViewHTML)
  // Each must have unique ID so we can reference to them.
  el.id = this._getViewSpecificId(n.id)

  // Allow reference from the element to the spaceNode and vice versa.
  // It becomes important to carefully undo the reference
  // to prevent memory leaks.
  el[DOM_SPACENODE_PROPERTY] = n
  this._elements[n.id] = el

  if (n === this._parent) {
    // SpaceNode is Space. Space has a special handling for its HTMLElement.
    // The element is transformed only when the view transforms.
    // This is the way how the transformation of the view affects to
    // the CSS3 transforms of HTMLElements of the children
    // of the space. The browser takes a product of the CSS3 transforms
    // of HTMLElement and its parents.

    // Add the element to the view's container.
    this._el.appendChild(el)

    // Define how view should react to changes in Space.
    // Note that Space cannot emit 'removed', 'resized', or 'transformed'
    on = {
      contentAdded: function (childNode) {
        // Child added to parent, therefore render it to view.
        // However, if the child is the view itself, do not render
        // because the view has been already rendered in the view's
        // handler for the 'added' event. See note [1] for details.
        if (childNode !== view) {
          view._renderElementFor(childNode)
        }
      }
    }
    n.on('contentAdded', on.contentAdded)
  } else {
    // Is a descendant of Space

    // Add the new element to the parent element.
    parentEl = this._elements[n.getParent().id]
    parentEl.appendChild(el)

    // Define how view should react to changes in SpaceNode
    // by setting up handlers.

    if (n === this) {
      // View itself is a descendant of Space and therefore _renderElementFor
      // is called also for the view's SpaceNode.
      //
      // If the view's SpaceNode becomes removed, the view has
      // a special handling for that, defined in unmount().
      //
      // If the view's SpaceNode becomes resized, the view has
      // a special handler for that, defined in mount() TODO.
      // The HTMLElement for the view is a 0x0 and should remain so
      // and therefore we cannot use the default _resizeElementOf.
      //
      // If the view becomes transformed, there is no need to
      // transform it's HTMLElement because the element should stay still.
      // By avoiding unnecessary retransformation we also avoid possible
      // rounding errors.
      //
      // Note that we bound has contentAdded handler also for the view.
      // This way we can fix elements to the view, like a health bar, or
      // dropdown menu.
      //
      on = {
        contentAdded: function (childNode) {
          // Child added
          view._renderElementFor(childNode)
        }
      }
      n.on('contentAdded', on.contentAdded)
    } else {
      on = {
        contentAdded: function (child) {
          view._renderElementFor(child)
        },
        removed: function () {
          view._removeElementOf(n)
        },
        resized: function () {
          view._resizeElementOf(n)
        },
        transformed: function () {
          view._transformElementOf(n)
        }
      }

      n.on('contentAdded', on.contentAdded)
      n.on('removed', on.removed)
      n.on('resized', on.resized)
      n.on('transformed', on.transformed)

      // Initial size and transformation
      if (n instanceof SpaceRectangle) {
        on.resized()
      }
      on.transformed()
    }
  }

  // Store for _removeElementOf
  this._handlers[n.id] = on

  // Repeat for children to render everything.
  n.getChildren().forEach(function (child) {
    view._renderElementFor(child)
  })
}

p._resizeElementOf = function (spaceRectangle) {
  var el, wh
  wh = spaceRectangle.getLocalSize()
  el = this._elements[spaceRectangle.id]
  el.style.width = wh.x + 'px'
  el.style.height = wh.y + 'px'
}

p._transformElementOf = function (spaceNode) {
  // Update transformation of a HTMLElement.
  // They are HTMLElements parented on view's container, not nodes on space.
  //
  var el, tr, prec, s, r, tx, ty

  el = this._elements[spaceNode.id]

  // Node's local transformation.
  tr = spaceNode._T

  // Current move.js does not prevent scientific notation reaching CSS
  // which leads to problems with Safari and Opera. Therefore we must
  // prevent the notation here.
  // Of course this will cause a small deviation in the presentation.
  // However the deviation is only in the presentation and thus not a problem.
  prec = 8
  s = tr.s.toFixed(prec)
  r = tr.r.toFixed(prec)
  tx = tr.tx.toFixed(prec)
  ty = tr.ty.toFixed(prec)
  move(el).matrix(s, r, -r, s, tx, ty).end()

  // TODO el.style.transform = 'matrix(' +
}

module.exports = SpaceViewHTML
<commit_msg>Overwrite fitScale to throw error if view not mounted<commit_after>//
// A View into Space, implemented in HTML DOM and CSS
// This module gives a starting point for implementing
// views in other tech such as Canvas or WebGL
//
// Notes:
//
// [1]
// View has handler for 'added' event.
// View's target space has handler for 'contentAdded' event.
// When a view is parented on a space, the two handlers cause
// duplicate rendering of the view's SpaceNode.
// To go in details, calling _renderElementFor for the space
// attaches 'contentAdded' handler to the space. The handler
// will be fired after execution of the view's 'added'
// handler because the view has been just added as a child
// of the space. To prevent a double call of _renderElementFor
// for the view, the 'contentAdded' handler of the space must
// check if the added child is a view or not. If it is the view,
// then there is no need to render it because the view is
// already rendered by the view's 'added' handler.
//
var createElementFor = require('./createElementFor')
var Vector = require('../Vector')
var SpaceRectangle = require('../SpaceRectangle')
var Space = require('../Space')
var extend = require('extend')
var move = require('move-js')

// Disable animations by default.
move.defaults = { duration: 0 }

var DOM_SPACENODE_PROPERTY = '_taaspace_node'

// Constructor

var SpaceViewHTML = function (space) {
  // Parameters
  //   space

  // Test if valid space
  if (!(space instanceof Space)) {
    throw new Error('Parent of a View must be a Space.')
  }

  SpaceRectangle.call(this)

  // This is the DOM container. Populated in mount().
  this._el = null

  // Mapping from SpaceNode#id to HTMLElement
  // Every HTMLElement created by the view is stored here.
  this._elements = {}
  // Mapping from SpaceNode#id to a map from an event name to a handler fn.
  // Event handlers of every rendered SpaceNode are stored here.
  this._handlers = {}
  // Mapping from an event name to a handler fn.
  // Handlers for the view's events like 'added'. Populated in mount().
  this._viewHandlers = {}

  // View is now ready to be added onto the space.
  // Note that this implicitly sets
  //   this._parent = space
  // Therefore, to access the space, call this.getParent() or this._parent
  this.setParent(space)
}

// Prototype mixin

var p = extend({}, SpaceRectangle.prototype)
SpaceViewHTML.prototype = p

// Public methods

p.fitScale = function (ipath) {
  // Overwrite SpaceRectangle#fitScale() to throw an error
  // when the view is not yet mounted and thus has no proper
  // dimensions with to fit.
  //
  if (!this.isMounted()) {
    throw new Error('View is not yet mounted and thus has no proper size ' +
      'for fitting. Call mount() before fitScale().')
  }

  return SpaceRectangle.prototype.fitScale.call(this, ipath)
}

p.getElementBySpaceNode = function (spaceNode) {
  // Get HTML element representation of the space node.
  // If spaceNode is the view itself, return the container.
  // TODO or should return the 0x0 element of the view and require
  // TODO that the container is requested via getHtmlContainer?
  // Return null if not found.
  //
  // if (spaceNode === this) {
  //   return this._el
  // }

  var n = this._elements[spaceNode.id]

  // n will be undefined if not found.
  if (n) {
    return n
  }
  return null
}

p.getHtmlContainer = function () {
  // Return the DOM root element of the space view.
  // Return null if not yet mounted.
  return this._el
}

p.getSpaceNodeByElementId = function (id) {
  // Get SpaceNode by HTML element id
  // Return null if no node for such id.
  var el = document.getElementById(id)

  if (el && el.hasOwnProperty(DOM_SPACENODE_PROPERTY)) {
    return el[DOM_SPACENODE_PROPERTY]
  }

  return null
}

p.isMounted = function () {
  return this._el !== null
}

p.mount = function (htmlContainer) {
  // Begin to use htmlContainer as the DOM root for the space.
  // Design decision:
  //   In v3 the HTML container was provided via the constructor.
  //   This led to a poor coding style where 'new SpaceViewHTML'
  //   was called just for its rendering side-effect. In v4
  //   we wanted the side-effect to be more explicit and made
  //   it a separate method 'mount'.
  //

  // Test if valid dom element
  if (!(htmlContainer && 'tagName' in htmlContainer)) {
    throw new Error('Container should be a DOM Element')
  }

  // If same container, no reason to do anything
  if (this._el === htmlContainer) {
    return
  }

  // Clear the old container
  this.unmount()
  this._el = htmlContainer

  // Init style.
  // Note:
  // "position: relative" is needed to enable "overflow: hidden".
  // Without "overflow: hidden", if transformed elements get outside
  // the initial viewport, Chrome on Android increases
  // the dimensions of the document.
  this._el.style.position = 'relative'
  this._el.style.overflow = 'hidden'
  this._el.style.display = 'block'

  // Init size in space.
  this.setLocalSize(new Vector(this._el.clientWidth, this._el.clientHeight))

  // Render the space.
  // Note that this renders also an element for the SpaceViewHTML.
  // Although the element will be 0x0 and meant for the children of the view.
  this._renderElementFor(this._parent)

  // A HTMLElement was rendered for the space itself.
  // This element is a bit special, because it should not react
  // to transformations of the Space (how could it, Space does not transform)
  // but to transformations of the SpaceViewHTML.

  var view = this
  this._viewHandlers = {
    added: function () {
      // If the view becomes attached to a new parent,
      // render the content of the new parent.
      // See [1] for interaction with the 'contentAdded' handler of the parent.
      view._renderElementFor(view._parent)
    },
    transformed: function () {
      // View becomes transformed.
      // => inverse transform HTMLElement of the Space
      //   => Browser retransforms HTMLElements of the descendants.
      // =>
      var el, vel, tr, vtr, prec, s, r, tx, ty
      el = view._elements[view._parent.id]
      vel = view._elements[view.id]
      tr = view._T.inverse()
      vtr = view._T

      prec = 8
      s = tr.s.toFixed(prec)
      r = tr.r.toFixed(prec)
      tx = tr.tx.toFixed(prec)
      ty = tr.ty.toFixed(prec)
      move(el).matrix(s, r, -r, s, tx, ty).end()

      // This undoes the transformation for the view's children
      s = vtr.s.toFixed(prec)
      r = vtr.r.toFixed(prec)
      tx = vtr.tx.toFixed(prec)
      ty = vtr.ty.toFixed(prec)
      move(vel).matrix(s, r, -r, s, tx, ty).end()
    }
  }

  this.on('added', this._viewHandlers.added)
  this.on('transformed', this._viewHandlers.transformed)
}

var setParentSuperProto = p.setParent
p.setParent = function (newParent) {
  // Override the SpaceNode#setParent so that only a Space
  // is allowed to become the parent of a SpaceView.
  if (!(newParent instanceof Space)) {
    throw new Error('A View can only be a child of a Space')
  }

  // Remove elements of the oldParent if the parent is about to change.
  // It is easier to remove the elements before _parent changes. That
  // would be the case if removal happens as a reaction to 'removed' event.
  if (this._parent === newParent) {
    return
  }
  if (this._parent !== null) {
    this._removeElementOf(this._parent)
  }

  setParentSuperProto.call(this, newParent)
}

p.unmount = function () {
  // Detach the view from HTML DOM

  if (!this.isMounted()) {
    // No need to remove anything.
    // Unnecessary unmount call can be a programming error
    // but sometimes we just want to be sure the view is unmounted.
    return
  }

  // No need to react to events in the space.
  this.off('added', this._viewHandlers.added)
  this.off('transformed', this._viewHandlers.transformed)
  this._viewHandlers = {}

  // Detach all rendered elements.
  this._removeElementOf(this._parent)
}

// Private(ish) methods

p._getViewSpecificId = function (spaceNodeId) {
  // Each rendered element has own ID. The ID differs from
  // the id of space nodes because a space node can become
  // visualized through multiple views.
  return this.id + '-' + spaceNodeId
}

p._removeElementOf = function (spaceNode) {
  // Removes the HTMLElement of spaceNode.
  // Does not remove the spaceNode from the view,
  // only from DOM.
  //
  // Note that the give spaceNode is probably already detached from
  // the Space. Therefore spaceNode.isRoot() probably returns true and
  // cannot be used to determine if the spaceNode is space.
  var el, on
  var view = this
  var n = spaceNode  // alias

  if (!this.isMounted()) {
    throw new Error('Cannot remove element when view is not mounted.')
  }

  // Recursively remove the children first
  n.getChildren().forEach(function (child) {
    view._removeElementOf(child)
  })

  // Remove event handlers and the element.
  on = this._handlers[n.id]

  if (n === this || n === this._parent) {
    // When SpaceNode is the Space or SpaceViewHTML itself,
    // only a handler for contentAdded has been created.
    n.off('contentAdded', on.contentAdded)
  } else {
    n.off('contentAdded', on.contentAdded)
    n.off('removed', on.removed)
    n.off('resized', on.resized)
    n.off('transformed', on.transformed)
  }

  el = this._elements[n.id]
  delete el[DOM_SPACENODE_PROPERTY]
  delete this._elements[n.id]
  delete this._handlers[n.id]

  // Remove from DOM
  el.parentElement.removeChild(el)
}

p._renderElementFor = function (spaceNode) {
  // Creates the element for spaceNode and renders it
  // to the view. Renders also the children of the node.
  var el, parentEl, on
  var view = this
  var n = spaceNode

  // Ensure that the view if mounted to DOM. Otherwise
  // asking for the render is a bug.
  if (!this.isMounted()) {
    throw new Error('Do not render elements before mounting the view')
  }

  // Prevent bugs that double-render elements
  if (this._elements.hasOwnProperty(n.id)) {
    throw new Error('An element should not be added twice to the same view.')
  }

  // Create HTMLElement for the node.
  // If the node is Space, SpaceGroup, or SpaceViewHTML,
  // a special 0x0 div is created.
  // Provide SpaceViewHTML constructor to avoid a circular dependency.
  el = createElementFor(n, SpaceViewHTML)
  // Each must have unique ID so we can reference to them.
  el.id = this._getViewSpecificId(n.id)

  // Allow reference from the element to the spaceNode and vice versa.
  // It becomes important to carefully undo the reference
  // to prevent memory leaks.
  el[DOM_SPACENODE_PROPERTY] = n
  this._elements[n.id] = el

  if (n === this._parent) {
    // SpaceNode is Space. Space has a special handling for its HTMLElement.
    // The element is transformed only when the view transforms.
    // This is the way how the transformation of the view affects to
    // the CSS3 transforms of HTMLElements of the children
    // of the space. The browser takes a product of the CSS3 transforms
    // of HTMLElement and its parents.

    // Add the element to the view's container.
    this._el.appendChild(el)

    // Define how view should react to changes in Space.
    // Note that Space cannot emit 'removed', 'resized', or 'transformed'
    on = {
      contentAdded: function (childNode) {
        // Child added to parent, therefore render it to view.
        // However, if the child is the view itself, do not render
        // because the view has been already rendered in the view's
        // handler for the 'added' event. See note [1] for details.
        if (childNode !== view) {
          view._renderElementFor(childNode)
        }
      }
    }
    n.on('contentAdded', on.contentAdded)
  } else {
    // Is a descendant of Space

    // Add the new element to the parent element.
    parentEl = this._elements[n.getParent().id]
    parentEl.appendChild(el)

    // Define how view should react to changes in SpaceNode
    // by setting up handlers.

    if (n === this) {
      // View itself is a descendant of Space and therefore _renderElementFor
      // is called also for the view's SpaceNode.
      //
      // If the view's SpaceNode becomes removed, the view has
      // a special handling for that, defined in unmount().
      //
      // If the view's SpaceNode becomes resized, the view has
      // a special handler for that, defined in mount() TODO.
      // The HTMLElement for the view is a 0x0 and should remain so
      // and therefore we cannot use the default _resizeElementOf.
      //
      // If the view becomes transformed, there is no need to
      // transform it's HTMLElement because the element should stay still.
      // By avoiding unnecessary retransformation we also avoid possible
      // rounding errors.
      //
      // Note that we bound has contentAdded handler also for the view.
      // This way we can fix elements to the view, like a health bar, or
      // dropdown menu.
      //
      on = {
        contentAdded: function (childNode) {
          // Child added
          view._renderElementFor(childNode)
        }
      }
      n.on('contentAdded', on.contentAdded)
    } else {
      on = {
        contentAdded: function (child) {
          view._renderElementFor(child)
        },
        removed: function () {
          view._removeElementOf(n)
        },
        resized: function () {
          view._resizeElementOf(n)
        },
        transformed: function () {
          view._transformElementOf(n)
        }
      }

      n.on('contentAdded', on.contentAdded)
      n.on('removed', on.removed)
      n.on('resized', on.resized)
      n.on('transformed', on.transformed)

      // Initial size and transformation
      if (n instanceof SpaceRectangle) {
        on.resized()
      }
      on.transformed()
    }
  }

  // Store for _removeElementOf
  this._handlers[n.id] = on

  // Repeat for children to render everything.
  n.getChildren().forEach(function (child) {
    view._renderElementFor(child)
  })
}

p._resizeElementOf = function (spaceRectangle) {
  var el, wh
  wh = spaceRectangle.getLocalSize()
  el = this._elements[spaceRectangle.id]
  el.style.width = wh.x + 'px'
  el.style.height = wh.y + 'px'
}

p._transformElementOf = function (spaceNode) {
  // Update transformation of a HTMLElement.
  // They are HTMLElements parented on view's container, not nodes on space.
  //
  var el, tr, prec, s, r, tx, ty

  el = this._elements[spaceNode.id]

  // Node's local transformation.
  tr = spaceNode._T

  // Current move.js does not prevent scientific notation reaching CSS
  // which leads to problems with Safari and Opera. Therefore we must
  // prevent the notation here.
  // Of course this will cause a small deviation in the presentation.
  // However the deviation is only in the presentation and thus not a problem.
  prec = 8
  s = tr.s.toFixed(prec)
  r = tr.r.toFixed(prec)
  tx = tr.tx.toFixed(prec)
  ty = tr.ty.toFixed(prec)
  move(el).matrix(s, r, -r, s, tx, ty).end()

  // TODO el.style.transform = 'matrix(' +
}

module.exports = SpaceViewHTML
<|endoftext|>