<commit_before>var gulp = require('gulp'),
	stylus = require('gulp-stylus'),
	concat = require('gulp-concat'),
	uglify = require('gulp-uglify'),
	sourcemaps = require('gulp-sourcemaps'),
	rename = require('gulp-rename'),
	imagemin = require('gulp-imagemin');

/**
 * Sauvegarde et compresse les fichiers stylus dans un unique fichier style.css
 * Garde le comportement @require de stylus
 * Crée un fichier sourcemap dans /maps/
 **/
gulp.task('stylus', function() {
	return gulp.src('stylus/style.styl')
		.pipe(sourcemaps.init())
			.pipe(stylus({
				compress: true
			}))
		.pipe(sourcemaps.write('../maps'))
		.pipe(gulp.dest(''));
});

/**
 * Sauvegarde et compresse les fichiers JS dans un fichier minifié
 * Crée un fichier sourcemap dans /maps/
 **/
gulp.task('javascript', function() {
	return gulp.src(['js/vendors/*.js', 'js/plugins/*.js', 'js/main.js'])
		.pipe(sourcemaps.init())
			.pipe(uglify({
				preserveComments: 'some'
			}))
			.pipe(concat('main.min.js'))
		.pipe(sourcemaps.write('../maps'))
		.pipe(gulp.dest('js/'));
});

/**
 * Compresse les images
 **/
gulp.task('images', function() {
	return gulp.src('images/**/*')
		.pipe(imagemin())
		.pipe(gulp.dest('images'));
});

gulp.task('default', ['stylus', 'javascript', 'images'], function() {
});

gulp.task('watch', function() {
	gulp.watch('stylus/*.styl', ['stylus']);
	gulp.watch('js/*.js', ['javascript']);
});
<commit_msg>order<commit_after>var gulp = require('gulp'),
	stylus = require('gulp-stylus'),
	concat = require('gulp-concat'),
	uglify = require('gulp-uglify'),
	rename = require('gulp-rename'),
	sourcemaps = require('gulp-sourcemaps'),
	imagemin = require('gulp-imagemin');

/**
 * Sauvegarde et compresse les fichiers stylus dans un unique fichier style.css
 * Garde le comportement @require de stylus
 * Crée un fichier sourcemap dans /maps/
 **/
gulp.task('stylus', function() {
	return gulp.src('stylus/style.styl')
		.pipe(sourcemaps.init())
			.pipe(stylus({
				compress: true
			}))
		.pipe(sourcemaps.write('../maps'))
		.pipe(gulp.dest(''));
});

/**
 * Sauvegarde et compresse les fichiers JS dans un fichier minifié
 * Crée un fichier sourcemap dans /maps/
 **/
gulp.task('javascript', function() {
	return gulp.src(['js/vendors/*.js', 'js/plugins/*.js', 'js/main.js'])
		.pipe(sourcemaps.init())
			.pipe(uglify({
				preserveComments: 'some'
			}))
			.pipe(concat('main.min.js'))
		.pipe(sourcemaps.write('../maps'))
		.pipe(gulp.dest('js/'));
});

/**
 * Compresse les images
 **/
gulp.task('images', function() {
	return gulp.src('images/**/*')
		.pipe(imagemin())
		.pipe(gulp.dest('images'));
});

gulp.task('default', ['stylus', 'javascript', 'images'], function() {
});

gulp.task('watch', function() {
	gulp.watch('stylus/*.styl', ['stylus']);
	gulp.watch('js/*.js', ['javascript']);
});
<|endoftext|>


<commit_before>/*global it:true */
"use strict";

var desc = require("../../helpers/driverblock.js").describeForSafari()
  , wvHelpers = require("../../helpers/webview.js")
  , webviewTests = wvHelpers.buildTests
  , loadWebView = wvHelpers.loadWebView
  , spinTitle = wvHelpers.spinTitle
  , _ = require('underscore')
  , should = require('should');

//var devices = ["iPhone", "iPad"];
var devices = ["iPad", "iPhone"];
_.each(devices, function(sim) {

  desc('windows and frames (' + sim + ')', function(h) {

    it('getting current window should work initially', function(done) {
      h.driver.windowHandle(function(err, handleId) {
        should.not.exist(err);
        parseInt(handleId, 10).should.be.above(0);
        done();
      });
    });

    it("should throw nosuchwindow if there's not one", function(done) {
      loadWebView("safari", h.driver, function() {
        h.driver.window('noexistman', function(err) {
          should.exist(err);
          err.status.should.eql(23);
          done();
        });
      });
    });

    it("should be able to open and close windows", function(done) {
      loadWebView("safari", h.driver, function() {
        h.driver.elementById('blanklink', function(err, link) {
          link.click(function() {
            spinTitle("I am another page title", h.driver, function(err) {
              should.not.exist(err);
              h.driver.windowHandles(function(err, handles) {
                var handles1 = handles.length;
                h.driver.close(function(err) {
                  // wait for safari to write window status
                  setTimeout(function() {
                    should.not.exist(err);
                    h.driver.windowHandles(function(err, handles) {
                      var handles2 = handles.length;
                      handles1.should.be.above(handles2);
                      spinTitle("I am a page title", h.driver, function(err) {
                        should.not.exist(err);
                        done();
                      });
                    });
                  }, 3000);
                });
              });
            });
          });
        });
      });
    });

    it('should be able to go back and forward', function(done) {
      loadWebView("safari", h.driver, function() {
        h.driver.elementByLinkText('i am a link', function(err, el) {
          el.click(function(){
            h.driver.elementById('only_on_page_2', function(err) {
              should.not.exist(err);
              h.driver.back(function(err) {
                should.not.exist(err);
                h.driver.elementById('i_am_a_textbox', function(err) {
                  should.not.exist(err);
                  h.driver.forward(function(err) {
                    should.not.exist(err);
                    h.driver.elementById('only_on_page_2', function(err) {
                      should.not.exist(err);
                      done();
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  }, null, null, {device: sim + " Simulator"});

});

webviewTests('safari');
<commit_msg>spacing.<commit_after>/*global it:true */
"use strict";

var desc = require("../../helpers/driverblock.js").describeForSafari()
  , wvHelpers = require("../../helpers/webview.js")
  , webviewTests = wvHelpers.buildTests
  , loadWebView = wvHelpers.loadWebView
  , spinTitle = wvHelpers.spinTitle
  , _ = require('underscore')
  , should = require('should');

//var devices = ["iPhone", "iPad"];
var devices = ["iPad", "iPhone"];
_.each(devices, function(sim) {

  desc('windows and frames (' + sim + ')', function(h) {

    it('getting current window should work initially', function(done) {
      h.driver.windowHandle(function(err, handleId) {
        should.not.exist(err);
        parseInt(handleId, 10).should.be.above(0);
        done();
      });
    });

    it("should throw nosuchwindow if there's not one", function(done) {
      loadWebView("safari", h.driver, function() {
        h.driver.window('noexistman', function(err) {
          should.exist(err);
          err.status.should.eql(23);
          done();
        });
      });
    });

    it("should be able to open and close windows", function(done) {
      loadWebView("safari", h.driver, function() {
        h.driver.elementById('blanklink', function(err, link) {
          link.click(function() {
            spinTitle("I am another page title", h.driver, function(err) {
              should.not.exist(err);
              h.driver.windowHandles(function(err, handles) {
                var handles1 = handles.length;
                h.driver.close(function(err) {
                  // wait for safari to write window status
                  setTimeout(function() {
                    should.not.exist(err);
                    h.driver.windowHandles(function(err, handles) {
                      var handles2 = handles.length;
                      handles1.should.be.above(handles2);
                      spinTitle("I am a page title", h.driver, function(err) {
                        should.not.exist(err);
                        done();
                      });
                    });
                  }, 3000);
                });
              });
            });
          });
        });
      });
    });

    it('should be able to go back and forward', function(done) {
      loadWebView("safari", h.driver, function() {
        h.driver.elementByLinkText('i am a link', function(err, el) {
          el.click(function() {
            h.driver.elementById('only_on_page_2', function(err) {
              should.not.exist(err);
              h.driver.back(function(err) {
                should.not.exist(err);
                h.driver.elementById('i_am_a_textbox', function(err) {
                  should.not.exist(err);
                  h.driver.forward(function(err) {
                    should.not.exist(err);
                    h.driver.elementById('only_on_page_2', function(err) {
                      should.not.exist(err);
                      done();
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  }, null, null, {device: sim + " Simulator"});

});

webviewTests('safari');
<|endoftext|>


<commit_before>/**
 * Yobi, Project Hosting SW
 *
 * Copyright 2013 NAVER Corp.
 * http://yobi.io
 *
 * @Author Jihan Kim
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

yobi = yobi || {};

yobi.CodeCommentBlock = (function(){
    var htVar = {};
    var htElement = {};
    var htBlockInfo = {};

    /**
     * 초기화
     *
     * @param sQuery
     * @private
     */
    function _init(htOptions){
        _initElement(htOptions);
        _attachEvent();

        htVar.bPopButtonOnBlock = (typeof htOptions.bPopButtonOnBlock !== "undefined")
                                    ? htOptions.bPopButtonOnBlock : true;
    }

    /**
     * Initialize element variables
     * @param htOptions
     * @private
     */
    function _initElement(htOptions){
        htElement.welContainer = $(htOptions.welContainer);
        htElement.welButtonOnBlock = $(htOptions.welButtonOnBlock);
    }

    /**
     * Attach event handler
     * @private
     */
    function _attachEvent(){
        htElement.welContainer.on("mouseup",   "td.code pre", _onMouseUpOnDiff);
        htElement.welContainer.on("mousedown", "td.code pre", _onMouseDownOnDiff);
    }

    /**
     * DiffBody 영역에서 mousedown 이벤트 발생시 핸들러
     * selectstart 이벤트는 FireFox 브라우저에서 지원되지 않기 때문에 mousedown 이벤트 사용
     *
     * @private
     */
    function _onMouseDownOnDiff(){
        _unwrapAll();
        window.getSelection().removeAllRanges(); // 기존의 Selection 정보를 지워야 함
    }

    /**
     * DiffBody 영역에서 mouseup 이벤트 발생시 핸들러
     *
     * @param weEvt
     * @private
     */
    function _onMouseUpOnDiff(){
        if(_doesCommentableRangeExists()){
            _setBlockDataBySelection();
            _onWrapCodeCommentBlock();
        }
    }

    /**
     * 코멘트 달 수 있는 블럭 영역인지 여부를 불리언 값으로 반환
     *
     * @returns {boolean}
     * @private
     */
    function _doesCommentableRangeExists(){
        var bHasRange = false;
        var oSelection = document.getSelection();

        // check empty seletction string
        if(oSelection.toString().length == 0){
            return false;
        }

        // get anchor, focus row (TR) from selected text node
        var welAnchor = $(oSelection.anchorNode.parentElement).closest("tr");
        var welFocus = $(oSelection.focusNode.parentElement).closest("tr");

        // data-line attribute is required on both of anchor and focus
        if(typeof welAnchor.data("line") === "undefined" || typeof welFocus.data("line") === "undefined"){
            return false;
        }

        // Range should be in same TABLE which means same file
        // .data("filePath") could be compared.
        if(welAnchor.closest("table").get(0) != welFocus.closest("table").get(0)){
            return false;
        }

        // detect whether is reversed
        var nAnchorIndex = welAnchor.index();
        var nFocusIndex = welFocus.index();
        var bIsReversed = (nAnchorIndex > nFocusIndex);
        var welStartLine = bIsReversed ? welFocus : welAnchor;
        var welEndLine = bIsReversed ? welAnchor : welFocus;

        // in range ...
        if(nAnchorIndex !== nFocusIndex){
            welStartLine.nextUntil(welEndLine).each(function(){
                var welLine = $(this);

                // tr.comments is tolerable
                if(!welLine.hasClass("comments") &&
                    welLine.find("td.code > pre").length !== 1){
                    bHasRange = true;
                }
            });
        }

        return !bHasRange;
    }

    /**
     * DiffBody 에서 선택한 영역 정보를 찾아 저장한다
     * @private
     */
    function _setBlockDataBySelection(){
        // get anchor, focus row (TR) from selected text node
        var oSelection = document.getSelection();
        var welAnchor = $(oSelection.anchorNode.parentElement).closest("tr");
        var welFocus = $(oSelection.focusNode.parentElement).closest("tr");
        var welTable = welAnchor.closest("table");

        // detect whether is reversed
        var nAnchorIndex = welAnchor.index();
        var nFocusIndex = welFocus.index();
        var bIsReversed = (nAnchorIndex > nFocusIndex);
        var welStartLine = bIsReversed ? welFocus : welAnchor;
        var welEndLine = bIsReversed ? welAnchor : welFocus;

        htBlockInfo = {
            "bIsReversed" : bIsReversed,
            "nStartLine"  : welStartLine.data("line"),
            "sStartType"  : welStartLine.data("type"),
            "sStartSide"  : welStartLine.data("type") == 'remove' ? 'A' : 'B',
            "nStartColumn": bIsReversed ? oSelection.focusOffset  : oSelection.anchorOffset,
            "nEndLine"    : welEndLine.data("line"),
            "sEndType"    : welEndLine.data("type"),
            "sEndSide"    : welEndLine.data("type") == 'remove' ? 'A' : 'B',
            "nEndColumn"  : bIsReversed ? oSelection.anchorOffset : oSelection.focusOffset,
            "sPathA"      : welTable.data("pathA"),
            "sPathB"      : welTable.data("pathB"),
            "sPrevCommitId": welTable.data("commitA"),
            "sCommitId"    : welTable.data("commitB"),
            "sFilePath"   : welTable.data("filePath"),
            "sPath"   : welTable.data("filePath")
        };
    }

    /**
     * CodeCommentBlock 에서 wrap 이벤트 발생시
     * 사용자가 어떤 영역을 선택하면 그 근처에 댓글작 버튼을 표시
     *
     * @private
     */
    function _onWrapCodeCommentBlock(){
        if(htVar.bPopButtonOnBlock && htElement.welButtonOnBlock){
            var htBlockInfo = _getBlockData();
            var htElements = _getElementsByOffsetOptions(htBlockInfo);

            var welLine = (htBlockInfo.bIsReversed ? htElements.welStartLine : htElements.welEndLine);
            var welCode = welLine.find("td.code");
            var nBlockOffset = (htBlockInfo.bIsReversed ? htBlockInfo.nStartColumn : htBlockInfo.nEndColumn);
            var htCodeOffset = welCode.position();
            var nTop = htCodeOffset.top + (htBlockInfo.bIsReversed ? -20 : welCode.height());
            var nLeft = htCodeOffset.left + (nBlockOffset * 7);

            // 블럭 영역이 diff-container 테이블 밖에 잡힐때를 대비해서
            if(nLeft > (htCodeOffset.left + welLine.width() - 40)){
                nLeft = htCodeOffset.left + welLine.width() - 80;
            }

            htElement.welButtonOnBlock.show();
            htElement.welButtonOnBlock.css({
                "top" : nTop +"px",
                "left": nLeft+"px"
            });
        }
    }

    /**
     * line:offset ~ line:offset 을 Wrap 하는 함수
     *
     * @param htOffset
     * @param htOffset.sPathA
     * @param htOffset.sPathB
     * @param htOffset.nStartLine
     * @param htOffset.nStartColumn (optional)
     * @param htOffset.sStartLineType (optional)
     * @param htOffset.nEndLine
     * @param htOffset.nEndColumn (optional)
     * @param htOffset.sEndLineType (optional)
     * @private
     * @example
     * _wrapByOffset({"nStartLine": 117, "nStartColumn":0, "nEndLine":120, "nEndColumn":3});
     */
    function _wrapByOffset(htOffset){
        _unwrapAll();
        _wrapOnDiff(htOffset);
    }

    /**
     * 선택한 영역을 Wrap
     *
     * @param {Hash Table} htOffset _setBlockDataBySelection 에서 반환하는 값과 같은 형식
     * @private
     */
    function _wrapOnDiff(htOffset){
        var htElements = _getElementsByOffsetOptions(htOffset);
        if(!htElements.elStartLine || !htElements.elEndLine){
            return false;
        }

        var nRows = htElements.aRows.length;
        htElements.aRows.forEach(function(welRow, nIndex){
            var welRowNode = welRow.find("td.code > pre").get(0).childNodes[0];
            var oRange = document.createRange();
            var elBlock = _getCommentLineWrapper();
            var nStartColumn = 0;
            var nEndColumn = 0;
            var nNodeLength = welRowNode.length;

            if(nRows === 1){               // in one line
                nStartColumn = htOffset.nStartColumn;
                nEndColumn = htOffset.nEndColumn;
            } else if(nIndex === 0){       // first line
                nStartColumn = htOffset.nStartColumn;
                nEndColumn = nNodeLength;
            } else if(nIndex === nRows-1){ // last line
                nStartColumn = 0;
                nEndColumn = htOffset.nEndColumn;
            } else {                       // and the others
                nStartColumn = 0;
                nEndColumn = nNodeLength;
            }

            oRange.setStart(welRowNode, nStartColumn);
            oRange.setEnd(welRowNode, nEndColumn);
            oRange.surroundContents(elBlock);
        });

        document.getSelection().removeAllRanges();
    }

    /**
     * _wrapByOffset() 함수의 인자로부터 필요한 엘리먼트들을 찾아서 반환하는 함수
     *
     * @param htOffset
     * @returns {Hash Table}
     * @private
     */
    function _getElementsByOffsetOptions(htOffset){
        var htResult = {};
        var sContainerProp = htOffset.sPath ? "[data-file-path='" + htOffset.sPath + "']": "";
        var sStartProp = [htOffset.nStartLine ? "[data-line=" + htOffset.nStartLine + "]": "",
            htOffset.sStartSide ? "[data-side=" + htOffset.sStartSide + "]" : ""].join("");
        var sEndProp = [htOffset.nEndLine ? "[data-line=" + htOffset.nEndLine + "]": "",
            htOffset.sEndSide ? "[data-side=" + htOffset.sEndSide + "]": ""].join("");
        var welContainer = $("table.diff-container" + sContainerProp);

        htResult.welStartLine = welContainer.find("tr" + sStartProp);
        htResult.welEndLine = welContainer.find("tr" + sEndProp);
        htResult.elStartLine = htResult.welStartLine.get(0);
        htResult.elEndLine = htResult.welEndLine.get(0);

        /// start of range
        htResult.aRows = [htResult.welStartLine];

        // if 2 or more rows has selected
        if(htResult.elStartLine !== htResult.elEndLine){
            /// in range
            htResult.aRows = htResult.aRows.concat(_getRowsBetween(htResult.elStartLine, htResult.elEndLine));

            /// end of range
            htResult.aRows.push(htResult.welEndLine);
        }

        return htResult;
    }

    /**
     * elStart 와 elEnd 사이의 TableRow(TR) 배열을 반환한다
     *
     * @param elStart
     * @param elEnd
     * @returns {Array}
     * @private
     */
    function _getRowsBetween(elStart, elEnd){
        var aRows = [];

        $(elStart).nextUntil(elEnd).each(function(i, elRow){
            var welRow = $(elRow);

            if(welRow.data("line")){
                aRows.push(welRow);
            }
        });

        return aRows;
    }

    /**
     * Returns comment line wrapper HTMLElement
     * @returns {HTMLElement}
     * @private
     */
    function _getCommentLineWrapper(){
        var elWrapper =  document.createElement("SPAN");
        elWrapper.setAttribute("data-toggle", "comment-block");
        elWrapper.className = "review-block";

        return elWrapper;
    }

    /**
     * Unwrap all comment wrappers
     * @private
     */
    function _unwrapAll(){
        $('[data-toggle="comment-block"]').each(function(){
            this.outerHTML = this.innerHTML;
        });

        _onUnwrapAllCodeCommentBlock();
    }

    /**
     * CodeCommentBlock 에서 unwrapAll 이벤트 발생시
     * 댓글작성 버튼 감춤
     * @private
     */
    function _onUnwrapAllCodeCommentBlock(){
        if(htElement.welButtonOnBlock){
            htElement.welButtonOnBlock.hide();
        }
    }

    /**
     * Getter for latest block data
     *
     * @returns {Hash Table}
     * @private
     */
    function _getBlockData(){
        return htBlockInfo;
    }

    // public interface
    return {
        "init"        : _init,
        "block"       : _wrapByOffset,
        "unblock"     : _unwrapAll,
        "getData"     : _getBlockData
    };
})();
<commit_msg>Review: detect reversed selection in single line<commit_after>/**
 * Yobi, Project Hosting SW
 *
 * Copyright 2013 NAVER Corp.
 * http://yobi.io
 *
 * @Author Jihan Kim
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

yobi = yobi || {};

yobi.CodeCommentBlock = (function(){
    var htVar = {};
    var htElement = {};
    var htBlockInfo = {};

    /**
     * 초기화
     *
     * @param sQuery
     * @private
     */
    function _init(htOptions){
        _initElement(htOptions);
        _attachEvent();

        htVar.bPopButtonOnBlock = (typeof htOptions.bPopButtonOnBlock !== "undefined")
                                    ? htOptions.bPopButtonOnBlock : true;
    }

    /**
     * Initialize element variables
     * @param htOptions
     * @private
     */
    function _initElement(htOptions){
        htElement.welContainer = $(htOptions.welContainer);
        htElement.welButtonOnBlock = $(htOptions.welButtonOnBlock);
    }

    /**
     * Attach event handler
     * @private
     */
    function _attachEvent(){
        htElement.welContainer.on("mouseup",   "td.code pre", _onMouseUpOnDiff);
        htElement.welContainer.on("mousedown", "td.code pre", _onMouseDownOnDiff);
    }

    /**
     * DiffBody 영역에서 mousedown 이벤트 발생시 핸들러
     * selectstart 이벤트는 FireFox 브라우저에서 지원되지 않기 때문에 mousedown 이벤트 사용
     *
     * @private
     */
    function _onMouseDownOnDiff(){
        _unwrapAll();
        window.getSelection().removeAllRanges(); // 기존의 Selection 정보를 지워야 함
    }

    /**
     * DiffBody 영역에서 mouseup 이벤트 발생시 핸들러
     *
     * @param weEvt
     * @private
     */
    function _onMouseUpOnDiff(){
        if(_doesCommentableRangeExists()){
            _setBlockDataBySelection();
            _onWrapCodeCommentBlock();
        }
    }

    /**
     * 코멘트 달 수 있는 블럭 영역인지 여부를 불리언 값으로 반환
     *
     * @returns {boolean}
     * @private
     */
    function _doesCommentableRangeExists(){
        var bHasRange = false;
        var oSelection = document.getSelection();

        // check empty seletction string
        if(oSelection.toString().length == 0){
            return false;
        }

        // get anchor, focus row (TR) from selected text node
        var welAnchor = $(oSelection.anchorNode.parentElement).closest("tr");
        var welFocus = $(oSelection.focusNode.parentElement).closest("tr");

        // data-line attribute is required on both of anchor and focus
        if(typeof welAnchor.data("line") === "undefined" || typeof welFocus.data("line") === "undefined"){
            return false;
        }

        // Range should be in same TABLE which means same file
        // .data("filePath") could be compared.
        if(welAnchor.closest("table").get(0) != welFocus.closest("table").get(0)){
            return false;
        }

        // detect whether is reversed
        var nAnchorIndex = welAnchor.index();
        var nFocusIndex = welFocus.index();
        var bIsReversed = (nAnchorIndex > nFocusIndex);
        var welStartLine = bIsReversed ? welFocus : welAnchor;
        var welEndLine = bIsReversed ? welAnchor : welFocus;

        // in range ...
        if(nAnchorIndex !== nFocusIndex){
            welStartLine.nextUntil(welEndLine).each(function(){
                var welLine = $(this);

                // tr.comments is tolerable
                if(!welLine.hasClass("comments") &&
                    welLine.find("td.code > pre").length !== 1){
                    bHasRange = true;
                }
            });
        }

        return !bHasRange;
    }

    /**
     * DiffBody 에서 선택한 영역 정보를 찾아 저장한다
     * @private
     */
    function _setBlockDataBySelection(){
        // get anchor, focus row (TR) from selected text node
        var oSelection = document.getSelection();
        var welAnchor = $(oSelection.anchorNode.parentElement).closest("tr");
        var welFocus = $(oSelection.focusNode.parentElement).closest("tr");
        var welTable = welAnchor.closest("table");

        // detect whether is reversed
        var nAnchorIndex = welAnchor.index();
        var nFocusIndex = welFocus.index();
        var nAnchorOffset = oSelection.anchorOffset;
        var nFocusOffset = oSelection.focusOffset;
        var bIsReversed = (nAnchorIndex > nFocusIndex) ||
                          (nAnchorIndex === nFocusIndex && nAnchorOffset > nFocusOffset);
        var welStartLine = bIsReversed ? welFocus : welAnchor;
        var welEndLine = bIsReversed ? welAnchor : welFocus;

        htBlockInfo = {
            "bIsReversed" : bIsReversed,
            "nStartLine"  : welStartLine.data("line"),
            "sStartType"  : welStartLine.data("type"),
            "sStartSide"  : welStartLine.data("type") === 'remove' ? 'A' : 'B',
            "nStartColumn": bIsReversed ? oSelection.focusOffset  : oSelection.anchorOffset,
            "nEndLine"    : welEndLine.data("line"),
            "sEndType"    : welEndLine.data("type"),
            "sEndSide"    : welEndLine.data("type") === 'remove' ? 'A' : 'B',
            "nEndColumn"  : bIsReversed ? oSelection.anchorOffset : oSelection.focusOffset,
            "sPathA"      : welTable.data("pathA"),
            "sPathB"      : welTable.data("pathB"),
            "sPrevCommitId": welTable.data("commitA"),
            "sCommitId"    : welTable.data("commitB"),
            "sFilePath"   : welTable.data("filePath"),
            "sPath"   : welTable.data("filePath")
        };
    }

    /**
     * CodeCommentBlock 에서 wrap 이벤트 발생시
     * 사용자가 어떤 영역을 선택하면 그 근처에 댓글작 버튼을 표시
     *
     * @private
     */
    function _onWrapCodeCommentBlock(){
        if(htVar.bPopButtonOnBlock && htElement.welButtonOnBlock){
            var htBlockInfo = _getBlockData();
            var htElements = _getElementsByOffsetOptions(htBlockInfo);

            var welLine = (htBlockInfo.bIsReversed ? htElements.welStartLine : htElements.welEndLine);
            var welCode = welLine.find("td.code");
            var nBlockOffset = (htBlockInfo.bIsReversed ? htBlockInfo.nStartColumn : htBlockInfo.nEndColumn);
            var htCodeOffset = welCode.position();
            var nTop = htCodeOffset.top + (htBlockInfo.bIsReversed ? -20 : welCode.height());
            var nLeft = htCodeOffset.left + (nBlockOffset * 7);

            // 블럭 영역이 diff-container 테이블 밖에 잡힐때를 대비해서
            if(nLeft > (htCodeOffset.left + welLine.width() - 40)){
                nLeft = htCodeOffset.left + welLine.width() - 80;
            }

            htElement.welButtonOnBlock.show();
            htElement.welButtonOnBlock.css({
                "top" : nTop +"px",
                "left": nLeft+"px"
            });
        }
    }

    /**
     * line:offset ~ line:offset 을 Wrap 하는 함수
     *
     * @param htOffset
     * @param htOffset.sPathA
     * @param htOffset.sPathB
     * @param htOffset.nStartLine
     * @param htOffset.nStartColumn (optional)
     * @param htOffset.sStartLineType (optional)
     * @param htOffset.nEndLine
     * @param htOffset.nEndColumn (optional)
     * @param htOffset.sEndLineType (optional)
     * @private
     * @example
     * _wrapByOffset({"nStartLine": 117, "nStartColumn":0, "nEndLine":120, "nEndColumn":3});
     */
    function _wrapByOffset(htOffset){
        _unwrapAll();
        _wrapOnDiff(htOffset);
    }

    /**
     * 선택한 영역을 Wrap
     *
     * @param {Hash Table} htOffset _setBlockDataBySelection 에서 반환하는 값과 같은 형식
     * @private
     */
    function _wrapOnDiff(htOffset){
        var htElements = _getElementsByOffsetOptions(htOffset);
        if(!htElements.elStartLine || !htElements.elEndLine){
            return false;
        }

        var nRows = htElements.aRows.length;
        htElements.aRows.forEach(function(welRow, nIndex){
            var welRowNode = welRow.find("td.code > pre").get(0).childNodes[0];
            var oRange = document.createRange();
            var elBlock = _getCommentLineWrapper();
            var nStartColumn = 0;
            var nEndColumn = 0;
            var nNodeLength = welRowNode.length;

            if(nRows === 1){               // in one line
                nStartColumn = htOffset.nStartColumn;
                nEndColumn = htOffset.nEndColumn;
            } else if(nIndex === 0){       // first line
                nStartColumn = htOffset.nStartColumn;
                nEndColumn = nNodeLength;
            } else if(nIndex === nRows-1){ // last line
                nStartColumn = 0;
                nEndColumn = htOffset.nEndColumn;
            } else {                       // and the others
                nStartColumn = 0;
                nEndColumn = nNodeLength;
            }

            oRange.setStart(welRowNode, nStartColumn);
            oRange.setEnd(welRowNode, nEndColumn);
            oRange.surroundContents(elBlock);
        });

        document.getSelection().removeAllRanges();
    }

    /**
     * _wrapByOffset() 함수의 인자로부터 필요한 엘리먼트들을 찾아서 반환하는 함수
     *
     * @param htOffset
     * @returns {Hash Table}
     * @private
     */
    function _getElementsByOffsetOptions(htOffset){
        var htResult = {};
        var sContainerProp = htOffset.sPath ? "[data-file-path='" + htOffset.sPath + "']": "";
        var sStartProp = [htOffset.nStartLine ? "[data-line=" + htOffset.nStartLine + "]": "",
            htOffset.sStartSide ? "[data-side=" + htOffset.sStartSide + "]" : ""].join("");
        var sEndProp = [htOffset.nEndLine ? "[data-line=" + htOffset.nEndLine + "]": "",
            htOffset.sEndSide ? "[data-side=" + htOffset.sEndSide + "]": ""].join("");
        var welContainer = $("table.diff-container" + sContainerProp);

        htResult.welStartLine = welContainer.find("tr" + sStartProp);
        htResult.welEndLine = welContainer.find("tr" + sEndProp);
        htResult.elStartLine = htResult.welStartLine.get(0);
        htResult.elEndLine = htResult.welEndLine.get(0);

        /// start of range
        htResult.aRows = [htResult.welStartLine];

        // if 2 or more rows has selected
        if(htResult.elStartLine !== htResult.elEndLine){
            /// in range
            htResult.aRows = htResult.aRows.concat(_getRowsBetween(htResult.elStartLine, htResult.elEndLine));

            /// end of range
            htResult.aRows.push(htResult.welEndLine);
        }

        return htResult;
    }

    /**
     * elStart 와 elEnd 사이의 TableRow(TR) 배열을 반환한다
     *
     * @param elStart
     * @param elEnd
     * @returns {Array}
     * @private
     */
    function _getRowsBetween(elStart, elEnd){
        var aRows = [];

        $(elStart).nextUntil(elEnd).each(function(i, elRow){
            var welRow = $(elRow);

            if(welRow.data("line")){
                aRows.push(welRow);
            }
        });

        return aRows;
    }

    /**
     * Returns comment line wrapper HTMLElement
     * @returns {HTMLElement}
     * @private
     */
    function _getCommentLineWrapper(){
        var elWrapper =  document.createElement("SPAN");
        elWrapper.setAttribute("data-toggle", "comment-block");
        elWrapper.className = "review-block";

        return elWrapper;
    }

    /**
     * Unwrap all comment wrappers
     * @private
     */
    function _unwrapAll(){
        $('[data-toggle="comment-block"]').each(function(){
            this.outerHTML = this.innerHTML;
        });

        _onUnwrapAllCodeCommentBlock();
    }

    /**
     * CodeCommentBlock 에서 unwrapAll 이벤트 발생시
     * 댓글작성 버튼 감춤
     * @private
     */
    function _onUnwrapAllCodeCommentBlock(){
        if(htElement.welButtonOnBlock){
            htElement.welButtonOnBlock.hide();
        }
    }

    /**
     * Getter for latest block data
     *
     * @returns {Hash Table}
     * @private
     */
    function _getBlockData(){
        return htBlockInfo;
    }

    // public interface
    return {
        "init"        : _init,
        "block"       : _wrapByOffset,
        "unblock"     : _unwrapAll,
        "getData"     : _getBlockData
    };
})();
<|endoftext|>


<commit_before>$(function(){
  $.widget( "custom.catcomplete", $.ui.autocomplete, {
    _create: function() {
      this._super();
      this.widget().menu( "option", "items", "> :not(.ui-autocomplete-category)" );
    },
    _renderMenu: function( ul, items ) {
      var that = this,
      currentCategory = "";
      function sortBy(a, b) {
        // We want to place 5to6 docs to the end of the list.
        // See if either a or b are in 5to6 category.
        var isp5a = false, isp5b = false;
        if ( a.category.substr(0,4) == '5to6' ) { isp5a = true; }
        if ( b.category.substr(0,4) == '5to6' ) { isp5b = true; }

        // If one of the categories is a 5to6 but other isn't,
        // move 5to6 to be last
        if ( isp5a  && !isp5b ) {return  1}
        if ( !isp5a && isp5b  ) {return -1}

        // Sort by category alphabetically; 5to6 items would both have
        // the same category if we reached this point and category sort
        // will happen only on non-5to6 items
        if ( a.category.toLowerCase()  b.category.toLowerCase() ) {return  1}

        // We reach this point when categories are the same; so
        // we sort items by value
        if ( a.value.toLowerCase()  b.value.toLowerCase() ) {return  1}
        return 0;
      }
      $.each( items.sort(sortBy), function( index, item ) {
        var li;
        if ( item.category != currentCategory ) {
          ul.append( "
" + item.category + "
" );
          currentCategory = item.category;
        }
        li = that._renderItemData( ul, item );
        if ( item.category ) {
          li.attr( "aria-label", item.category + " : " + item.label );
        }
      });
    }
  });
  $("#query").attr('placeholder', '🔍').catcomplete({
      response: function( e, ui ) {
        if ( ! ui.content.length ) {
            $('#search').addClass('not-found')
                .find('#try-web-search').attr(
                    'href', 'https://www.google.com/search?q=site%3Adocs.perl6.org+'
                    + encodeURIComponent( $("#query").val() )
                );
        }
        else {
            $('#search').removeClass('not-found')
        }
      },
      open: function() {
        var ui_el = $('.ui-autocomplete');
        if ( ui_el.offset().left < 0 ) {
            ui_el.css({left: 0})
        }
      },
      position: { my: "right top", at: "right bottom", of: "#search div" },
      source: [
          {
              category: "Syntax",
              value: "# single-line comment",
              url: "/language/syntax#Single-line_comments"
          }, {
              category: "Syntax",
              value: "#` multi-line comment",
              url: "/language/syntax#Multi-line_/_embedded_comments"
          }, {
              category: "Signature",
              value: ";; (long name)",
              url: "/type/Signature#index-entry-Long_Names"
          },
          ITEMS
      ],
      select: function (event, ui) { window.location.href = ui.item.url; },
      autoFocus: true
  });
});

// allow for inexact searching via levenshtein

$.extend( $.ui.autocomplete, {
    filter: function( array, term ) {
        var OK_distance = 1;
        return $.grep( array, function( value ) {
            var lookup = value.value;
            if (lookup.length > term.length) {
                lookup = lookup.substr(0,term.length);
            }
            return getEditDistance( lookup, term) <= OK_distance;
        } );
    }
} );
<commit_msg>disable levenshtein search for now due to speed concerns<commit_after>$(function(){
  $.widget( "custom.catcomplete", $.ui.autocomplete, {
    _create: function() {
      this._super();
      this.widget().menu( "option", "items", "> :not(.ui-autocomplete-category)" );
    },
    _renderMenu: function( ul, items ) {
      var that = this,
      currentCategory = "";
      function sortBy(a, b) {
        // We want to place 5to6 docs to the end of the list.
        // See if either a or b are in 5to6 category.
        var isp5a = false, isp5b = false;
        if ( a.category.substr(0,4) == '5to6' ) { isp5a = true; }
        if ( b.category.substr(0,4) == '5to6' ) { isp5b = true; }

        // If one of the categories is a 5to6 but other isn't,
        // move 5to6 to be last
        if ( isp5a  && !isp5b ) {return  1}
        if ( !isp5a && isp5b  ) {return -1}

        // Sort by category alphabetically; 5to6 items would both have
        // the same category if we reached this point and category sort
        // will happen only on non-5to6 items
        if ( a.category.toLowerCase()  b.category.toLowerCase() ) {return  1}

        // We reach this point when categories are the same; so
        // we sort items by value
        if ( a.value.toLowerCase()  b.value.toLowerCase() ) {return  1}
        return 0;
      }
      $.each( items.sort(sortBy), function( index, item ) {
        var li;
        if ( item.category != currentCategory ) {
          ul.append( "
" + item.category + "
" );
          currentCategory = item.category;
        }
        li = that._renderItemData( ul, item );
        if ( item.category ) {
          li.attr( "aria-label", item.category + " : " + item.label );
        }
      });
    }
  });
  $("#query").attr('placeholder', '🔍').catcomplete({
      response: function( e, ui ) {
        if ( ! ui.content.length ) {
            $('#search').addClass('not-found')
                .find('#try-web-search').attr(
                    'href', 'https://www.google.com/search?q=site%3Adocs.perl6.org+'
                    + encodeURIComponent( $("#query").val() )
                );
        }
        else {
            $('#search').removeClass('not-found')
        }
      },
      open: function() {
        var ui_el = $('.ui-autocomplete');
        if ( ui_el.offset().left < 0 ) {
            ui_el.css({left: 0})
        }
      },
      position: { my: "right top", at: "right bottom", of: "#search div" },
      source: [
          {
              category: "Syntax",
              value: "# single-line comment",
              url: "/language/syntax#Single-line_comments"
          }, {
              category: "Syntax",
              value: "#` multi-line comment",
              url: "/language/syntax#Multi-line_/_embedded_comments"
          }, {
              category: "Signature",
              value: ";; (long name)",
              url: "/type/Signature#index-entry-Long_Names"
          },
          ITEMS
      ],
      select: function (event, ui) { window.location.href = ui.item.url; },
      autoFocus: true
  });
});

// allow for inexact searching via levenshtein

/*
$.extend( $.ui.autocomplete, {
    filter: function( array, term ) {
        var OK_distance = 1;
        return $.grep( array, function( value ) {
            var lookup = value.value;
            if (lookup.length > term.length) {
                lookup = lookup.substr(0,term.length);
            }
            return getEditDistance( lookup, term) <= OK_distance;
        } );
    }
} );
*/
<|endoftext|>


<commit_before>//External modules
var path = require('path');
var util = require("util");

// Module Compile
exports.init = function (grunt) {
    'use strict';

    var module = require(path.join(path.dirname(__dirname), "default")).init(grunt);

    var configuration = {};
    var baseConfig = null;
    var kindOf = grunt.util.kindOf;

    module.registerTask("addDependencies", "Adding dependencies to files", function () {
        // Load library
        var deps = require("./include/dependencies").init(grunt);
        var depsPath = path.resolve(process.cwd(), configuration.default.dest);
        // Get Modules with dependencies
        deps.findDependencies(module.environment);
        deps.addFoundDependenciesToFiles(depsPath);
    });

    util._extend(module, {
        name: path.basename(__dirname),
        run: function () {
            this.getConfiguration();

            if (typeof configuration.default != "undefined" && configuration.default.dest != "undefined") {
                this.makeClear(configuration.default.dest);
            }

            // For Debug ->
            grunt.log.debug(JSON.stringify(configuration));

            // Generating files
            this.generateAppNoCache(configuration.default.dest);
            this.generateBootstrap(configuration.default.sourcePath, configuration.default.dest);

            // Setting configuration
            this.loadPlugin("grunt-typescript");
            this.configuration = configuration;
            var task = this.runTask("typescript", configuration);
            this.runTask("addDependencies", {default: {}}, []);
            return task;
        },
        getConfiguration: function () {
            // Load default configuration
            var configFile = path.resolve(__dirname, "config/default.json");
            if (grunt.file.exists(configFile)) {
                try {
                    configuration = grunt.file.readJSON(configFile);
                    grunt.log.debug(this.name + " plugin default configuration is loaded!");
                } catch (ex) {
                    grunt.log.error("[ERROR] " + this.name + " plugin default configuration has error!");
                    configuration = {};
                }
            }

            // Load user created configuration
            var userFile = path.resolve(process.cwd(), "config/build", this.name + ".js");
            if (grunt.file.exists(userFile)) {
                var config = require(userFile)(grunt);

                //Parsing configuration
                configuration = this.mergeObjects(configuration, this.parse(config));
            } else {
                grunt.log.debug(this.name + " user configuration not found, continue");
            }

            return configuration;
        },
        parse: function (configuration) {

            var parsed = {};

            // Parsing
            if (configuration.hasOwnProperty("source")) {
                parsed.sourcePath = path.resolve(process.cwd(), configuration.source);
                parsed.src = path.resolve(parsed.sourcePath, "**/*.ts");
                if (parsed.hasOwnProperty("options")) {
                    parsed.options.basePath = path.normalize(configuration.source);
                } else {
                    parsed.options = {
                        basePath: path.normalize(configuration.source)
                    }
                }
            }

            if (configuration.hasOwnProperty("target")) {
                parsed.dest = path.resolve(process.cwd(), configuration.target);
            }

            if (configuration.hasOwnProperty("version")) {
                if (parsed.hasOwnProperty("options")) {
                    parsed.options.target = configuration.version;
                } else {
                    parsed.options = {target: configuration.version};
                }
            }

            if (configuration.hasOwnProperty("jsMapping") && configuration.jsMapping && kindOf(configuration.jsMapping.files) == "array" && configuration.jsMapping.files.length) {
                parsed.jsMapping = configuration.jsMapping;
            }

            if (kindOf(configuration.baseConfig) == "function") {
                baseConfig = configuration.baseConfig;
            }

            // Fix for TypeScript
            return {
                default: parsed
            };
        },
        generateConfigFile: function (srcPath) {
            var pathRel = path.relative(process.cwd(), configuration.default.dest).replace(/\\/g, "/");
            var pathToLib = Array(pathRel.split(/[\/\\\\]/).length + 1).join("../");

            var fileText = "window.require = window.require || {};\n";
            fileText += "\t\twindow.require.baseUrl = rootDir + '" + pathRel + "';\n";

            var deps = [];
            var packages = [];
            var packageConfig = {};
            var allPackages = [];

            // Collect all library packages for baseConfig
            if (this.isNotEmptyObject(this.environment.libraries)) {
                for (var libraryName in this.environment.libraries) {
                    var library = this.environment.libraries[libraryName];

                    // Check each library and library name
                    if (library) {
                        if (this.isNotEmptyObject(library.packages)) {
                            for (var packageName in library.packages) {
                                var pkg = library.packages[packageName];
                                if (pkg) {
                                    packages.push(packageName);

                                    if (library.autoStart) {
                                        deps.push(packageName);
                                    }

                                    allPackages.push({
                                        name: packageName,
                                        package: packageName + "/main",
                                        library: libraryName,
                                        sourcePath: path.resolve(process.cwd(), srcPath, packageName),
                                        compiledPath: path.resolve(process.cwd(), pathRel, packageName)
                                    });

                                    if (pkg.config) {
                                        packageConfig[packageName + "/main"] = pkg.config;
                                    }
                                }
                            }
                        }
                    }
                }

                if (deps.length > 0) {
                    fileText += '\t\twindow.require.deps = (window.require.deps || []).concat(["' + deps.join('","') + '"]);\n';
                }
            }

            // Collect all library packages for baseConfig
            if (this.isNotEmptyObject(this.environment.packages)) {
                for (var packageName in this.environment.packages) {
                    if (this.environment.packages[packageName]) {
                        var pkg = this.environment.packages[packageName];
                        if (pkg) {
                            packages.push(packageName);

                            allPackages.push({
                                name: packageName,
                                package: packageName + "/main",
                                sourcePath: path.resolve(process.cwd(), srcPath, packageName),
                                compiledPath: path.resolve(process.cwd(), pathRel, packageName)
                            });

                            if (pkg.config) {
                                packageConfig[packageName + "/main"] = pkg.config;
                            }
                        }
                    }
                }
            }

            // Usage of baseConfig
            if (allPackages && allPackages.length) {
                if (typeof baseConfig == "function") {
                    var object = this;
                    allPackages.forEach(function (pkg) {
                        var ret = baseConfig(pkg, allPackages);
                        if (ret) {
                            if (packageConfig[pkg.name + "/main"]) {
                                object.smartMerge(ret, packageConfig[pkg.name + "/main"]);
                            }
                            packageConfig[pkg.name + "/main"] = ret;
                        }
                    });
                }
            }

            if (deps.concat(packages).length > 0) {
                fileText += '\t\twindow.require.packages = (window.require.packages || []).concat(["' + deps.concat(packages).join('","') + '"]);\n';
            }

            if (this.isNotEmptyObject(packageConfig)) {
                fileText += '\t\twindow.require.config = window.require.config || {};\n';
                for (var index in packageConfig) {
                    fileText += '\t\twindow.require.config["' + index + '"] = ' + JSON.stringify(packageConfig[index]) + ";\n";
                }
            }

            if ((this.isNotEmptyObject(this.environment.base) && Object.keys(this.environment.base).length > 1) || this.isNotEmptyObject(this.environment.reqModules) || this.isNotEmptyObject(configuration.default.jsMapping)) {
                fileText += "\t\twindow.require.paths = window.require.paths || {};\n";

                if (kindOf(this.environment.base) == "object") {
                    for (var lib in this.environment.base) {
                        if (lib != "require") {
                            fileText += '\t\twindow.require.paths["' + lib + '"] = "' + path.normalize(pathToLib + this.environment.base[lib]).replace(/\\/g, "/") + '";\n';
                        }
                    }
                }

                if (kindOf(this.environment.reqModules) == "object") {
                    for (var lib in this.environment.reqModules) {
                        var reqModulePath = this.environment.reqModules[lib]
                            .replace("{path.fromKelper}", path.relative(
                                path.resolve(process.cwd(), pathRel),
                                this.modulePath
                            ))
                            .replace("{path.fromSource}", path.relative(
                                path.resolve(process.cwd(), pathRel),
                                path.resolve(process.cwd(), srcPath)
                            ))
                            .replace("{path.fromRoot}", path.relative(
                                path.resolve(process.cwd(), pathRel),
                                process.cwd()
                            ))
                            .replace(/\\/g, "/");
                        fileText += '\t\twindow.require.paths["' + lib + '"] = "' + reqModulePath + '";\n';
                    }
                }

                if (kindOf(configuration.default.jsMapping) == "object") {
                    if (configuration.default.jsMapping.files && configuration.default.jsMapping.files.length > 0) {
                        var pathRelToSource = path.relative(process.cwd(), srcPath);
                        configuration.default.jsMapping.files.forEach(function (map) {
                            var files = grunt.file.expand(
                                path.resolve(process.cwd(), srcPath, map)
                            );
                            files.forEach(function (file) {
                                var fileMap = path.relative(
                                    path.resolve(process.cwd(), srcPath),
                                    file.replace(/\.js$/i, "")
                                ).replace(/\\/g, "/");
                                fileText += '\t\twindow.require.paths["' + fileMap + '"] = "' + path.join(pathToLib, pathRelToSource, fileMap).replace(/\\/g, "/") + '";\n';
                            });
                        });
                    }
                }
            }

            configuration.packageConfig = packageConfig;

            return fileText;
        },
        generateReplaces: function () {
            var replaces = {};
            var pathRel = path.relative(process.cwd(), configuration.default.dest).replace(/\\/g, "/");
            var pathToLib = Array(pathRel.split(/[\/\\\\]/).length + 1).join("../");

            // Replaces
            if (this.isNotEmptyObject(this.environment.libraries)) {
                for (var libraryName in this.environment.libraries) {
                    var library = this.environment.libraries[libraryName];
                    if (library) {
                        if (this.isNotEmptyObject(library.packages)) {
                            for (var packageName in library.packages) {
                                var pkg = library.packages[packageName];

                                // Check package and package name
                                if (pkg) {
                                    if (pkg.replace) {
                                        replaces = this.mergeObjects(replaces, pkg.replace);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (this.isNotEmptyObject(this.environment.packages)) {
                for (var packageName in this.environment.packages) {
                    var pkg = this.environment.packages[packageName];

                    // Check package and package name
                    if (pkg) {
                        if (pkg.replace) {
                            replaces = this.mergeObjects(replaces, pkg.replace);
                        }
                    }
                }
            }

            if (Object.keys(replaces).length > 0) {
                var fileText = 'window.require = window.require || {};\n';
                fileText += '\t\t\twindow.require.map = window.require.map || {};\n';
                fileText += '\t\t\twindow.require.map["*"] = window.require.map["*"] || {};\n';

                for (var key in replaces) {
                    var pathToCompiled = path.relative(
                        process.cwd(),
                        path.resolve(process.cwd(), pathToLib, configuration.default.dest, key)
                    );
                    fileText += '\t\t\twindow.require.map["*"]["' + key + '"] = "' + pathToCompiled + '";\n';
                }

                return fileText;
            }
            return "";
        },
        generateAppNoCache: function (destPath) {
            var appJs = path.join(destPath, "app.nocache.js");
            if (!grunt.file.exists(appJs)) {
                grunt.file.write(appJs, "__bootstrap();");
            }
        },
        generateBootstrap: function (srcPath, destPath) {
            var bootstrap = path.resolve(destPath, "bootstrap.js");
            var defBootstrap = path.resolve(__dirname, "config/bootstrap.js");
            var relSourcePath = path.relative(process.cwd(), srcPath);

            var fileText = "var script = ('currentScript' in document) ? document.currentScript : document.getElementsByTagName('script')[document.getElementsByTagName('script').length - 1];\n";
            fileText += "var rootDir = Array(String(document.location.origin + document.location.pathname).split(/[\/\\\\]/).filter(function(e, i){return script.src.split(/[\/\\\\]/)[i] !== e;}).length).join('../');\n";
            fileText += "var sourceDir = rootDir + '" + relSourcePath + "';\n";

            var compFile = grunt.file.read(defBootstrap);
            compFile = compFile.replace(/\{compiled}/g, this.generateConfigFile(srcPath));
            compFile = compFile.replace(/\{replaces}/g, this.generateReplaces());
            compFile = compFile.replace(/\{path_kelper_module}/g, path.relative(process.cwd(), this.modulePath + "/node_modules") + path.sep);
            compFile = compFile.replace(/\{path_kelper_include}/g, path.relative(process.cwd(), this.modulePath + "/include") + path.sep);

            compFile = compFile.replace(/\{path_compiled}/g, path.relative(process.cwd(), destPath) + path.sep);

            var optimization = require("../optimization/main").init(grunt).getConfiguration();
            compFile = compFile.replace(/\{path_optimized}/g, path.relative(process.cwd(), optimization.default.options.dir) + path.sep);
            var finalized = require("../finalization/main").init(grunt).getConfiguration();
            compFile = compFile.replace(/\{path_finalized}/g, path.relative(process.cwd(), finalized.target) + path.sep);

            // Fix slashes
            compFile = compFile.replace(/\\/g, "/");

            fileText += compFile;
            grunt.file.write(bootstrap, fileText);
        }
    });

    return module;
};<commit_msg>Fix for phantomJS<commit_after>//External modules
var path = require('path');
var util = require("util");

// Module Compile
exports.init = function (grunt) {
    'use strict';

    var module = require(path.join(path.dirname(__dirname), "default")).init(grunt);

    var configuration = {};
    var baseConfig = null;
    var kindOf = grunt.util.kindOf;

    module.registerTask("addDependencies", "Adding dependencies to files", function () {
        // Load library
        var deps = require("./include/dependencies").init(grunt);
        var depsPath = path.resolve(process.cwd(), configuration.default.dest);
        // Get Modules with dependencies
        deps.findDependencies(module.environment);
        deps.addFoundDependenciesToFiles(depsPath);
    });

    util._extend(module, {
        name: path.basename(__dirname),
        run: function () {
            this.getConfiguration();

            if (typeof configuration.default != "undefined" && configuration.default.dest != "undefined") {
                this.makeClear(configuration.default.dest);
            }

            // For Debug ->
            grunt.log.debug(JSON.stringify(configuration));

            // Generating files
            this.generateAppNoCache(configuration.default.dest);
            this.generateBootstrap(configuration.default.sourcePath, configuration.default.dest);

            // Setting configuration
            this.loadPlugin("grunt-typescript");
            this.configuration = configuration;
            var task = this.runTask("typescript", configuration);
            this.runTask("addDependencies", {default: {}}, []);
            return task;
        },
        getConfiguration: function () {
            // Load default configuration
            var configFile = path.resolve(__dirname, "config/default.json");
            if (grunt.file.exists(configFile)) {
                try {
                    configuration = grunt.file.readJSON(configFile);
                    grunt.log.debug(this.name + " plugin default configuration is loaded!");
                } catch (ex) {
                    grunt.log.error("[ERROR] " + this.name + " plugin default configuration has error!");
                    configuration = {};
                }
            }

            // Load user created configuration
            var userFile = path.resolve(process.cwd(), "config/build", this.name + ".js");
            if (grunt.file.exists(userFile)) {
                var config = require(userFile)(grunt);

                //Parsing configuration
                configuration = this.mergeObjects(configuration, this.parse(config));
            } else {
                grunt.log.debug(this.name + " user configuration not found, continue");
            }

            return configuration;
        },
        parse: function (configuration) {

            var parsed = {};

            // Parsing
            if (configuration.hasOwnProperty("source")) {
                parsed.sourcePath = path.resolve(process.cwd(), configuration.source);
                parsed.src = path.resolve(parsed.sourcePath, "**/*.ts");
                if (parsed.hasOwnProperty("options")) {
                    parsed.options.basePath = path.normalize(configuration.source);
                } else {
                    parsed.options = {
                        basePath: path.normalize(configuration.source)
                    }
                }
            }

            if (configuration.hasOwnProperty("target")) {
                parsed.dest = path.resolve(process.cwd(), configuration.target);
            }

            if (configuration.hasOwnProperty("version")) {
                if (parsed.hasOwnProperty("options")) {
                    parsed.options.target = configuration.version;
                } else {
                    parsed.options = {target: configuration.version};
                }
            }

            if (configuration.hasOwnProperty("jsMapping") && configuration.jsMapping && kindOf(configuration.jsMapping.files) == "array" && configuration.jsMapping.files.length) {
                parsed.jsMapping = configuration.jsMapping;
            }

            if (kindOf(configuration.baseConfig) == "function") {
                baseConfig = configuration.baseConfig;
            }

            // Fix for TypeScript
            return {
                default: parsed
            };
        },
        generateConfigFile: function (srcPath) {
            var pathRel = path.relative(process.cwd(), configuration.default.dest).replace(/\\/g, "/");
            var pathToLib = Array(pathRel.split(/[\/\\\\]/).length + 1).join("../");

            var fileText = "window.require = window.require || {};\n";
            fileText += "\t\twindow.require.baseUrl = rootDir + '" + pathRel + "';\n";

            var deps = [];
            var packages = [];
            var packageConfig = {};
            var allPackages = [];

            // Collect all library packages for baseConfig
            if (this.isNotEmptyObject(this.environment.libraries)) {
                for (var libraryName in this.environment.libraries) {
                    var library = this.environment.libraries[libraryName];

                    // Check each library and library name
                    if (library) {
                        if (this.isNotEmptyObject(library.packages)) {
                            for (var packageName in library.packages) {
                                var pkg = library.packages[packageName];
                                if (pkg) {
                                    packages.push(packageName);

                                    if (library.autoStart) {
                                        deps.push(packageName);
                                    }

                                    allPackages.push({
                                        name: packageName,
                                        package: packageName + "/main",
                                        library: libraryName,
                                        sourcePath: path.resolve(process.cwd(), srcPath, packageName),
                                        compiledPath: path.resolve(process.cwd(), pathRel, packageName)
                                    });

                                    if (pkg.config) {
                                        packageConfig[packageName + "/main"] = pkg.config;
                                    }
                                }
                            }
                        }
                    }
                }

                if (deps.length > 0) {
                    fileText += '\t\twindow.require.deps = (window.require.deps || []).concat(["' + deps.join('","') + '"]);\n';
                }
            }

            // Collect all library packages for baseConfig
            if (this.isNotEmptyObject(this.environment.packages)) {
                for (var packageName in this.environment.packages) {
                    if (this.environment.packages[packageName]) {
                        var pkg = this.environment.packages[packageName];
                        if (pkg) {
                            packages.push(packageName);

                            allPackages.push({
                                name: packageName,
                                package: packageName + "/main",
                                sourcePath: path.resolve(process.cwd(), srcPath, packageName),
                                compiledPath: path.resolve(process.cwd(), pathRel, packageName)
                            });

                            if (pkg.config) {
                                packageConfig[packageName + "/main"] = pkg.config;
                            }
                        }
                    }
                }
            }

            // Usage of baseConfig
            if (allPackages && allPackages.length) {
                if (typeof baseConfig == "function") {
                    var object = this;
                    allPackages.forEach(function (pkg) {
                        var ret = baseConfig(pkg, allPackages);
                        if (ret) {
                            if (packageConfig[pkg.name + "/main"]) {
                                object.smartMerge(ret, packageConfig[pkg.name + "/main"]);
                            }
                            packageConfig[pkg.name + "/main"] = ret;
                        }
                    });
                }
            }

            if (deps.concat(packages).length > 0) {
                fileText += '\t\twindow.require.packages = (window.require.packages || []).concat(["' + deps.concat(packages).join('","') + '"]);\n';
            }

            if (this.isNotEmptyObject(packageConfig)) {
                fileText += '\t\twindow.require.config = window.require.config || {};\n';
                for (var index in packageConfig) {
                    fileText += '\t\twindow.require.config["' + index + '"] = ' + JSON.stringify(packageConfig[index]) + ";\n";
                }
            }

            if ((this.isNotEmptyObject(this.environment.base) && Object.keys(this.environment.base).length > 1) || this.isNotEmptyObject(this.environment.reqModules) || this.isNotEmptyObject(configuration.default.jsMapping)) {
                fileText += "\t\twindow.require.paths = window.require.paths || {};\n";

                if (kindOf(this.environment.base) == "object") {
                    for (var lib in this.environment.base) {
                        if (lib != "require") {
                            fileText += '\t\twindow.require.paths["' + lib + '"] = "' + path.normalize(pathToLib + this.environment.base[lib]).replace(/\\/g, "/") + '";\n';
                        }
                    }
                }

                if (kindOf(this.environment.reqModules) == "object") {
                    for (var lib in this.environment.reqModules) {
                        var reqModulePath = this.environment.reqModules[lib]
                            .replace("{path.fromKelper}", path.relative(
                                path.resolve(process.cwd(), pathRel),
                                this.modulePath
                            ))
                            .replace("{path.fromSource}", path.relative(
                                path.resolve(process.cwd(), pathRel),
                                path.resolve(process.cwd(), srcPath)
                            ))
                            .replace("{path.fromRoot}", path.relative(
                                path.resolve(process.cwd(), pathRel),
                                process.cwd()
                            ))
                            .replace(/\\/g, "/");
                        fileText += '\t\twindow.require.paths["' + lib + '"] = "' + reqModulePath + '";\n';
                    }
                }

                if (kindOf(configuration.default.jsMapping) == "object") {
                    if (configuration.default.jsMapping.files && configuration.default.jsMapping.files.length > 0) {
                        var pathRelToSource = path.relative(process.cwd(), srcPath);
                        configuration.default.jsMapping.files.forEach(function (map) {
                            var files = grunt.file.expand(
                                path.resolve(process.cwd(), srcPath, map)
                            );
                            files.forEach(function (file) {
                                var fileMap = path.relative(
                                    path.resolve(process.cwd(), srcPath),
                                    file.replace(/\.js$/i, "")
                                ).replace(/\\/g, "/");
                                fileText += '\t\twindow.require.paths["' + fileMap + '"] = "' + path.join(pathToLib, pathRelToSource, fileMap).replace(/\\/g, "/") + '";\n';
                            });
                        });
                    }
                }
            }

            configuration.packageConfig = packageConfig;

            return fileText;
        },
        generateReplaces: function () {
            var replaces = {};
            var pathRel = path.relative(process.cwd(), configuration.default.dest).replace(/\\/g, "/");
            var pathToLib = Array(pathRel.split(/[\/\\\\]/).length + 1).join("../");

            // Replaces
            if (this.isNotEmptyObject(this.environment.libraries)) {
                for (var libraryName in this.environment.libraries) {
                    var library = this.environment.libraries[libraryName];
                    if (library) {
                        if (this.isNotEmptyObject(library.packages)) {
                            for (var packageName in library.packages) {
                                var pkg = library.packages[packageName];

                                // Check package and package name
                                if (pkg) {
                                    if (pkg.replace) {
                                        replaces = this.mergeObjects(replaces, pkg.replace);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (this.isNotEmptyObject(this.environment.packages)) {
                for (var packageName in this.environment.packages) {
                    var pkg = this.environment.packages[packageName];

                    // Check package and package name
                    if (pkg) {
                        if (pkg.replace) {
                            replaces = this.mergeObjects(replaces, pkg.replace);
                        }
                    }
                }
            }

            if (Object.keys(replaces).length > 0) {
                var fileText = 'window.require = window.require || {};\n';
                fileText += '\t\t\twindow.require.map = window.require.map || {};\n';
                fileText += '\t\t\twindow.require.map["*"] = window.require.map["*"] || {};\n';

                for (var key in replaces) {
                    var pathToCompiled = path.relative(
                        process.cwd(),
                        path.resolve(process.cwd(), pathToLib, configuration.default.dest, key)
                    );
                    fileText += '\t\t\twindow.require.map["*"]["' + key + '"] = "' + pathToCompiled + '";\n';
                }

                return fileText;
            }
            return "";
        },
        generateAppNoCache: function (destPath) {
            var appJs = path.join(destPath, "app.nocache.js");
            if (!grunt.file.exists(appJs)) {
                grunt.file.write(appJs, "__bootstrap();");
            }
        },
        generateBootstrap: function (srcPath, destPath) {
            var bootstrap = path.resolve(destPath, "bootstrap.js");
            var defBootstrap = path.resolve(__dirname, "config/bootstrap.js");
            var relSourcePath = path.relative(process.cwd(), srcPath);

            var fileText = "var script = ('currentScript' in document) ? document.currentScript : document.getElementsByTagName('script')[document.getElementsByTagName('script').length - 1];\n";
            fileText += "var rootDir = Array(document.location.href.replace(document.location.hash,'').split(/[\/\\\\]/).filter(function(e, i){return script.src.split(/[\/\\\\]/)[i] !== e;}).length).join('../');\n";
            fileText += "var sourceDir = rootDir + '" + relSourcePath + "';\n";

            var compFile = grunt.file.read(defBootstrap);
            compFile = compFile.replace(/\{compiled}/g, this.generateConfigFile(srcPath));
            compFile = compFile.replace(/\{replaces}/g, this.generateReplaces());
            compFile = compFile.replace(/\{path_kelper_module}/g, path.relative(process.cwd(), this.modulePath + "/node_modules") + path.sep);
            compFile = compFile.replace(/\{path_kelper_include}/g, path.relative(process.cwd(), this.modulePath + "/include") + path.sep);

            compFile = compFile.replace(/\{path_compiled}/g, path.relative(process.cwd(), destPath) + path.sep);

            var optimization = require("../optimization/main").init(grunt).getConfiguration();
            compFile = compFile.replace(/\{path_optimized}/g, path.relative(process.cwd(), optimization.default.options.dir) + path.sep);
            var finalized = require("../finalization/main").init(grunt).getConfiguration();
            compFile = compFile.replace(/\{path_finalized}/g, path.relative(process.cwd(), finalized.target) + path.sep);

            // Fix slashes
            compFile = compFile.replace(/\\/g, "/");

            fileText += compFile;
            grunt.file.write(bootstrap, fileText);
        }
    });

    return module;
};<|endoftext|>


<commit_before>/**
# Type Checking, Mocks By Example

The goal of this module is to provide simple, effective type-checking by example. Ultimately, it is
intended to afford both static analysis of `b8r` code and components and efficient
run-time checking of application state -- see [The Registry](#source=source/b8r.registry.js)
documentation for more information.

(WORK IN PROGRESS)
As a side-benefit, it is also capable of driving mock-data and optimistic rendering.
Annotations in example data can provide hints as to how to generate mock data for
testing purposes and for rendering user interfaces before live data is available.

General usage is:

    matchType(example, subject) // returns empty list if subject has same type as example
      // returns a list of problems discovered otherwise
E.g.

    matchType(0, 17) // [] -- no errors
    matchType('foo', 17) // ["was number, expected string"]

This is most useful when comparing objects, e.g.

    matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 'world'}) // [] -- no errors
    matchType({foo: 17, bar: 'hello'}, {bar: 'world'}) // [".foo was undefined, expected number"]
    matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 17}) // [".bar was number, expected string"]

If the example includes arrays, the elements in the array are assumed to be the valid examples
for items in the array, e.g.

    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], []) // [] -- no errors
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{x: 10, y: 10}, {x: -1, y: -1}]) // []
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{lat: -20, long: 40, alt: 100}]) // []
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{x: 5, y: -5}, {long: 20}])
      // ["[1] had no matching type"]
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{x: 5}, {long: 20}])
      // ["[0] had no matching type", "[1] had no matching type"]

For efficiency, put the most common example elements in example arrays first (since checks are
performed in order) and do not include unnecessary elements in example arrays.

## Custom Types

A `Match` class which allows you to declare arbitrarily specific (or general) types. Usage:

    new Match(testFunc, typeDescription, generateMock)

- `testFunc` is a function that tests its first parameter and returns a string complaining
about what's wrong with it or nothing if it's OK.
- `typeDescription` describes the type
- `generateMock` is a function that produces example data that satisfies the textFunc

    const wholeNumber = new Match(
      x => typeof x !== 'number' || isNaN(x) || x % 1 ? `was ${x}` : false,
      'whole number',
      () => Math.floor(Math.random() * 100 - 50)
    )

## `describe`

A simple and useful wrapper for `typeof` is provided in the form of `describe` which
gives the typeof the value passed unless it's an `Array` (in which case it returns
'array') or `null` (in which case it returns 'null')

    describe([]) // 'array'
    describe(null) // 'null'

## Type Utilities

Some useful utilities (built using Match) are also provided, including `oneOf`,
`optional`, `nullable`, and `nonEmpty`.

    oneOf('a', 'b', 'c') // creates a type that will match one of the arguments provided

~~~~
const {
  matchType,
  describe,
  oneOf,
  Match,
  nonEmpty,
  nullable,
  optional,
  pickOne
} = await import('./b8r.byExample.js');

Test(() => matchType(0, 17)).shouldBeJSON([])
Test(() => matchType(0, 'hello')).shouldBeJSON(['was string, expected number'])
Test(() => matchType(false, true)).shouldBeJSON([])
Test(() => matchType(false, null)).shouldBeJSON(["was null, expected boolean"])
Test(() => matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 'world'}))
  .shouldBeJSON([])
Test(() => matchType({foo: 17, bar: 'hello'}, {bar: 'world'}))
  .shouldBeJSON([".foo was undefined, expected number"])
Test(() => matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 17}))
  .shouldBeJSON([".bar was number, expected string"])
Test(() => matchType({foo: 17, bar: 'hello'}, {bar: 17}))
  .shouldBeJSON([".bar was number, expected string", ".foo was undefined, expected number"])
Test(() => matchType({foo: {bar: {baz: true}}}, {foo: {bar: {baz: false}}}))
  .shouldBeJSON([])
Test(() => matchType({foo: {bar: {baz: true}}}, {foo: {bar: {baz: 17}}}))
  .shouldBeJSON([".foo.bar.baz was number, expected boolean"])
Test(() => matchType([], []))
  .shouldBeJSON([])
Test(() => matchType([1], []))
  .shouldBeJSON([])
Test(() => matchType([], [1]))
  .shouldBeJSON([])
Test(() => matchType(['hello'], ['world']))
  .shouldBeJSON([])
Test(() => matchType([false], ['world']))
  .shouldBeJSON(["[0] was string, expected boolean"])
Test(() => matchType([{x: 0, y: 17}], [{y: 0, x: 17}]))
  .shouldBeJSON([])
Test(() => matchType([{x: 0, y: 17}], [{y: 0}]))
  .shouldBeJSON(["[0].x was undefined, expected number"])
Test(() => matchType([{x: 0, y: 17}], [{x: 'world'}]))
  .shouldBeJSON(["[0].x was string, expected number", "[0].y was undefined, expected number"])
Test(() => matchType([{x: 0, y: 17}, {foo: 'bar'}], [{foo: 'baz'}]))
  .shouldBeJSON([])
Test(() => matchType([{x: 0, y: 17}, {foo: 'bar'}], [{foo: false}]))
  .shouldBeJSON(["[0] had no matching type"])

const cardinal = new Match(subject => {
  const subjectType = describe(subject)
  if (subjectType !== 'number') {
    return `was ${subjectType}`
  } else if (subject < 0) {
    return `was negative`
  } else if (subject % 1) {
    return `was not a whole number`
  }
}, 'cardinal number', () => 17)
Test(() => matchType(cardinal, 0))
  .shouldBeJSON([])
Test(() => matchType(cardinal, null))
  .shouldBeJSON(["was null, expected cardinal number"])
Test(() => matchType(cardinal, -1))
  .shouldBeJSON(["was negative, expected cardinal number"])

const requestType = oneOf('get', 'post', 'put', 'delete', 'head')
Test(() => matchType(requestType, 'post'))
  .shouldBeJSON([])
Test(() => matchType(requestType, 'save'))
  .shouldBeJSON(["was save, expected one of get|post|put|delete|head"])

const wholeNumber = new Match(
  x => typeof x !== 'number' || isNaN(x) || x % 1 ? `was ${x}` : false,
  'whole number',
  () => Math.floor(Math.random() * 100 - 50)
)
Test(() => matchType(wholeNumber, 11))
  .shouldBeJSON([])
Test(() => matchType(wholeNumber, 12.345))
  .shouldBeJSON(["was 12.345, expected whole number"])

const nonEmptyString = nonEmpty('test')
Test(() => matchType(nonEmptyString, 'hello'))
  .shouldBeJSON([])
Test(() => matchType(nonEmptyString, ''))
  .shouldBeJSON(["has length 0, expected non-empty string"])
Test(() => matchType(nonEmptyString, []))
  .shouldBeJSON(["was array, expected non-empty string"])

const nullableObject = nullable({})
Test(() => matchType(nullableObject, null))
  .shouldBeJSON([])
Test(() => matchType(nullableObject, 'hello'))
  .shouldBeJSON(["was string, expected object or null"])
const optionalArrayOfNumbers = optional([1], 'array<number>')
Test(() => matchType(optionalArrayOfNumbers))
  .shouldBeJSON([])
Test(() => matchType(optionalArrayOfNumbers, [1,2,3]))
  .shouldBeJSON([])
Test(() => matchType(optionalArrayOfNumbers, ['a','b','c']))
  .shouldBeJSON(['was array, expected array<number>, null, or undefined'])
Test(() => matchType(['a', 17], ['qq'], [], '', true))
  .shouldBeJSON([])
Test(() => matchType(['a', 17], [0, 'qq'], [], '', true))
  .shouldBeJSON([])
Test(() => matchType(['a', 17], [0, 'qq', {}], [], '', true))
  .shouldBeJSON(["[2] had no matching type"])
Test(() => new Match(x => typeof x === 'number' && x > 0, 'positiveNumber', -5))
  .shouldThrow()
~~~~
*/

export const describe = x => {
  if (x === null) return 'null'
  if (x instanceof Match) return x.description
  if (Array.isArray(x)) return 'array'
  if (typeof x === 'number' && isNaN(x)) return 'NaN'
  return typeof x
}

export const describeType = (x) => {
  const scalarType = describe(x)
  switch (scalarType) {
    case 'array':
      return x.map(describeType)
    case 'object':
      const _type = {}
      Object.keys(x).forEach((key) => { _type[key] = describeType(x[key]) })
      return _type
    default:
      return scalarType
  }
}

export const typeJSON = (x) => JSON.stringify(describeType(x), false, 2)
export const typeJS = (x) => typeJSON(x).replace(/"(\w+)":/g, '$1:')

export class Match {
  constructor (testFunction, description, generateMock) {
    this.test = testFunction
    this.description = description
    if (typeof generateMock !== 'function') {
      throw new Error(`Match "${description}" requires generateMock to be specified`)
    }
    if (this.test(generateMock())) {
      throw new Error(`Match "${description}" mock() fails its own test!`)
    }
    this.mock = () => {
      let example
      do {
        example = generateMock()
      } while (example instanceof Match)
      return example
    }
  }
}

export const pickOne = (...array) => array[Math.floor(array.length * Math.random())]

export const oneOf = (...options) => new Match(subject => {
  if (!options.includes(subject)) {
    return `was ${subject}`
  }
}, `one of ${options.join('|')}`, () => pickOne(...options))

export const nullable = (example, description = '') => new Match(subject => {
  if (subject !== null) {
    if (matchType(example, subject).length) return `was ${describe(subject)}`
  }
}, `${description || describe(example)} or null`, () => pickOne(null, example))

export const optional = (example, description = '') => new Match(subject => {
  if (subject !== null && subject !== undefined) {
    if (matchType(example, subject).length) return `was ${describe(subject)}`
  }
}, `${description || describe(example)}, null, or undefined`, () => pickOne(null, undefined, example))

export const nonEmpty = (example, description = '') => new Match(subject => {
  if (subject == null) return `was ${describe(subject)}`
  if (matchType(example, subject).length) return `was ${describe(subject)}`
  if (subject.length === 0) return 'has length 0'
}, `non-empty ${description || describe(example)}`, () => example)

export const matchType = (example, subject, errors = [], path = '') => {
  if (example instanceof Match) {
    const outcome = example.test(subject)
    if (outcome) errors.push(`${path ? path + ' ' : ''}${outcome}, expected ${example.description}`)
    return errors
  }
  const exampleType = describe(example)
  const subjectType = describe(subject)
  if (exampleType !== subjectType) {
    errors.push(`${path ? path + ' ' : ''}was ${subjectType}, expected ${exampleType}`)
  } else if (exampleType === 'array') {
    // only checking first element of subject for now
    const count = subject.length
    if (example.length === 1 && count) {
      // assume homogenous array
      for (let i = 0; i < count; i++) {
        matchType(example[0], subject[i], errors, `${path}[${i}]`)
      }
    } else if (example.length > 1 && count) {
      // assume heterogeneous array
      for (let i = 0; i < count; i++) {
        let foundMatch = false
        for (let listItem of example) {
          if (matchType(listItem, subject[i], [], '').length === 0) {
            foundMatch = true
            break
          }
        }
        if (!foundMatch) errors.push(`${path}[${i}] had no matching type`)
      }
    }
  } else if (exampleType === 'object') {
    matchKeys(example, subject, errors, path)
  }
  return errors
}

const matchKeys = (example, subject, errors = [], path = '') => {
  for (let key of Object.keys(example).sort()) {
    matchType(example[key], subject[key], errors, path + '.' + key)
  }
  return errors
}
<commit_msg>implemented exampleAtPath<commit_after>/**
# Type Checking, Mocks By Example

The goal of this module is to provide simple, effective type-checking by example. Ultimately, it is
intended to afford both static analysis of `b8r` code and components and efficient
run-time checking of application state -- see [The Registry](#source=source/b8r.registry.js)
documentation for more information.

(WORK IN PROGRESS)
As a side-benefit, it is also capable of driving mock-data and optimistic rendering.
Annotations in example data can provide hints as to how to generate mock data for
testing purposes and for rendering user interfaces before live data is available.

General usage is:

    matchType(example, subject) // returns empty list if subject has same type as example
      // returns a list of problems discovered otherwise
E.g.

    matchType(0, 17) // [] -- no errors
    matchType('foo', 17) // ["was number, expected string"]

This is most useful when comparing objects, e.g.

    matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 'world'}) // [] -- no errors
    matchType({foo: 17, bar: 'hello'}, {bar: 'world'}) // [".foo was undefined, expected number"]
    matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 17}) // [".bar was number, expected string"]

If the example includes arrays, the elements in the array are assumed to be the valid examples
for items in the array, e.g.

    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], []) // [] -- no errors
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{x: 10, y: 10}, {x: -1, y: -1}]) // []
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{lat: -20, long: 40, alt: 100}]) // []
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{x: 5, y: -5}, {long: 20}])
      // ["[1] had no matching type"]
    matchType([{x: 0, y: 0}, {long: 0, lat: 0, alt: 0}], [{x: 5}, {long: 20}])
      // ["[0] had no matching type", "[1] had no matching type"]

For efficiency, put the most common example elements in example arrays first (since checks are
performed in order) and do not include unnecessary elements in example arrays.

## Custom Types

A `Match` class which allows you to declare arbitrarily specific (or general) types. Usage:

    new Match(testFunc, typeDescription, generateMock)

- `testFunc` is a function that tests its first parameter and returns a string complaining
about what's wrong with it or nothing if it's OK.
- `typeDescription` describes the type
- `generateMock` is a function that produces example data that satisfies the textFunc

    const wholeNumber = new Match(
      x => typeof x !== 'number' || isNaN(x) || x % 1 ? `was ${x}` : false,
      'whole number',
      () => Math.floor(Math.random() * 100 - 50)
    )

## `describe`

A simple and useful wrapper for `typeof` is provided in the form of `describe` which
gives the typeof the value passed unless it's an `Array` (in which case it returns
'array') or `null` (in which case it returns 'null')

    describe([]) // 'array'
    describe(null) // 'null'

## Type Utilities

Some useful utilities (built using Match) are also provided, including `oneOf`,
`optional`, `nullable`, and `nonEmpty`.

    oneOf('a', 'b', 'c') // creates a type that will match one of the arguments provided

~~~~
const {
  matchType,
  describe,
  oneOf,
  Match,
  nonEmpty,
  nullable,
  optional,
  pickOne
} = await import('./b8r.byExample.js');

Test(() => matchType(0, 17)).shouldBeJSON([])
Test(() => matchType(0, 'hello')).shouldBeJSON(['was string, expected number'])
Test(() => matchType(false, true)).shouldBeJSON([])
Test(() => matchType(false, null)).shouldBeJSON(["was null, expected boolean"])
Test(() => matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 'world'}))
  .shouldBeJSON([])
Test(() => matchType({foo: 17, bar: 'hello'}, {bar: 'world'}))
  .shouldBeJSON([".foo was undefined, expected number"])
Test(() => matchType({foo: 17, bar: 'hello'}, {foo: 0, bar: 17}))
  .shouldBeJSON([".bar was number, expected string"])
Test(() => matchType({foo: 17, bar: 'hello'}, {bar: 17}))
  .shouldBeJSON([".bar was number, expected string", ".foo was undefined, expected number"])
Test(() => matchType({foo: {bar: {baz: true}}}, {foo: {bar: {baz: false}}}))
  .shouldBeJSON([])
Test(() => matchType({foo: {bar: {baz: true}}}, {foo: {bar: {baz: 17}}}))
  .shouldBeJSON([".foo.bar.baz was number, expected boolean"])
Test(() => matchType([], []))
  .shouldBeJSON([])
Test(() => matchType([1], []))
  .shouldBeJSON([])
Test(() => matchType([], [1]))
  .shouldBeJSON([])
Test(() => matchType(['hello'], ['world']))
  .shouldBeJSON([])
Test(() => matchType([false], ['world']))
  .shouldBeJSON(["[0] was string, expected boolean"])
Test(() => matchType([{x: 0, y: 17}], [{y: 0, x: 17}]))
  .shouldBeJSON([])
Test(() => matchType([{x: 0, y: 17}], [{y: 0}]))
  .shouldBeJSON(["[0].x was undefined, expected number"])
Test(() => matchType([{x: 0, y: 17}], [{x: 'world'}]))
  .shouldBeJSON(["[0].x was string, expected number", "[0].y was undefined, expected number"])
Test(() => matchType([{x: 0, y: 17}, {foo: 'bar'}], [{foo: 'baz'}]))
  .shouldBeJSON([])
Test(() => matchType([{x: 0, y: 17}, {foo: 'bar'}], [{foo: false}]))
  .shouldBeJSON(["[0] had no matching type"])

const cardinal = new Match(subject => {
  const subjectType = describe(subject)
  if (subjectType !== 'number') {
    return `was ${subjectType}`
  } else if (subject < 0) {
    return `was negative`
  } else if (subject % 1) {
    return `was not a whole number`
  }
}, 'cardinal number', () => 17)
Test(() => matchType(cardinal, 0))
  .shouldBeJSON([])
Test(() => matchType(cardinal, null))
  .shouldBeJSON(["was null, expected cardinal number"])
Test(() => matchType(cardinal, -1))
  .shouldBeJSON(["was negative, expected cardinal number"])

const requestType = oneOf('get', 'post', 'put', 'delete', 'head')
Test(() => matchType(requestType, 'post'))
  .shouldBeJSON([])
Test(() => matchType(requestType, 'save'))
  .shouldBeJSON(["was save, expected one of get|post|put|delete|head"])

const wholeNumber = new Match(
  x => typeof x !== 'number' || isNaN(x) || x % 1 ? `was ${x}` : false,
  'whole number',
  () => Math.floor(Math.random() * 100 - 50)
)
Test(() => matchType(wholeNumber, 11))
  .shouldBeJSON([])
Test(() => matchType(wholeNumber, 12.345))
  .shouldBeJSON(["was 12.345, expected whole number"])

const nonEmptyString = nonEmpty('test')
Test(() => matchType(nonEmptyString, 'hello'))
  .shouldBeJSON([])
Test(() => matchType(nonEmptyString, ''))
  .shouldBeJSON(["has length 0, expected non-empty string"])
Test(() => matchType(nonEmptyString, []))
  .shouldBeJSON(["was array, expected non-empty string"])

const nullableObject = nullable({})
Test(() => matchType(nullableObject, null))
  .shouldBeJSON([])
Test(() => matchType(nullableObject, 'hello'))
  .shouldBeJSON(["was string, expected object or null"])
const optionalArrayOfNumbers = optional([1], 'array<number>')
Test(() => matchType(optionalArrayOfNumbers))
  .shouldBeJSON([])
Test(() => matchType(optionalArrayOfNumbers, [1,2,3]))
  .shouldBeJSON([])
Test(() => matchType(optionalArrayOfNumbers, ['a','b','c']))
  .shouldBeJSON(['was array, expected array<number>, null, or undefined'])
Test(() => matchType(['a', 17], ['qq'], [], '', true))
  .shouldBeJSON([])
Test(() => matchType(['a', 17], [0, 'qq'], [], '', true))
  .shouldBeJSON([])
Test(() => matchType(['a', 17], [0, 'qq', {}], [], '', true))
  .shouldBeJSON(["[2] had no matching type"])
Test(() => new Match(x => typeof x === 'number' && x > 0, 'positiveNumber', -5))
  .shouldThrow()

Test(() => exampleAtPath({foo: 17}, 'foo')).shouldBe('string')
Test(() => exampleAtPath({bar: 'hello'}, 'foo')).shouldBe('string')
~~~~
*/

export const describe = x => {
  if (x === null) return 'null'
  if (x instanceof Match) return x.description
  if (Array.isArray(x)) return 'array'
  if (typeof x === 'number' && isNaN(x)) return 'NaN'
  return typeof x
}

export const describeType = (x) => {
  const scalarType = describe(x)
  switch (scalarType) {
    case 'array':
      return x.map(describeType)
    case 'object':
      const _type = {}
      Object.keys(x).forEach((key) => { _type[key] = describeType(x[key]) })
      return _type
    default:
      return scalarType
  }
}

export const typeJSON = (x) => JSON.stringify(describeType(x))
export const typeJS = (x) => typeJSON(x).replace(/"(\w+)":/g, '$1:')

export class Match {
  constructor (testFunction, description, generateMock) {
    this.test = testFunction
    this.description = description
    if (typeof generateMock !== 'function') {
      throw new Error(`Match "${description}" requires generateMock to be specified`)
    }
    if (this.test(generateMock())) {
      throw new Error(`Match "${description}" mock() fails its own test!`)
    }
    this.mock = () => {
      let example
      do {
        example = generateMock()
      } while (example instanceof Match)
      return example
    }
  }
}

export const pickOne = (...array) => array[Math.floor(array.length * Math.random())]

export const oneOf = (...options) => {
  if (options.length === 0) return undefined
  if (options.length === 1) return options[1]
  const optionDescriptions = options.map(typeJS).reduce((a, b) => {
    if (!a.includes(b)) a.push(b)
    return a
  }, [])
  return new Match(subject => {
    if (!options.includes(subject)) {
      return `was ${subject}`
    }
  }, `one of ${optionDescriptions.join('|')}`, () => pickOne(...options))
}

export const nullable = (example, description = '') => new Match(subject => {
  if (subject !== null) {
    if (matchType(example, subject).length) return `was ${describe(subject)}`
  }
}, `${description || describe(example)} or null`, () => pickOne(null, example))

export const optional = (example, description = '') => new Match(subject => {
  if (subject !== null && subject !== undefined) {
    if (matchType(example, subject).length) return `was ${describe(subject)}`
  }
}, `${description || describe(example)}, null, or undefined`, () => pickOne(null, undefined, example))

export const nonEmpty = (example, description = '') => new Match(subject => {
  if (subject == null) return `was ${describe(subject)}`
  if (matchType(example, subject).length) return `was ${describe(subject)}`
  if (subject.length === 0) return 'has length 0'
}, `non-empty ${description || describe(example)}`, () => example)

export const matchType = (example, subject, errors = [], path = '') => {
  if (example instanceof Match) {
    const outcome = example.test(subject)
    if (outcome) errors.push(`${path ? path + ' ' : ''}${outcome}, expected ${example.description}`)
    return errors
  }
  const exampleType = describe(example)
  const subjectType = describe(subject)
  if (exampleType !== subjectType) {
    errors.push(`${path ? path + ' ' : ''}was ${subjectType}, expected ${exampleType}`)
  } else if (exampleType === 'array') {
    // only checking first element of subject for now
    const count = subject.length
    if (example.length === 1 && count) {
      // assume homogenous array
      for (let i = 0; i < count; i++) {
        matchType(example[0], subject[i], errors, `${path}[${i}]`)
      }
    } else if (example.length > 1 && count) {
      // assume heterogeneous array
      for (let i = 0; i < count; i++) {
        let foundMatch = false
        for (let listItem of example) {
          if (matchType(listItem, subject[i], [], '').length === 0) {
            foundMatch = true
            break
          }
        }
        if (!foundMatch) errors.push(`${path}[${i}] had no matching type`)
      }
    }
  } else if (exampleType === 'object') {
    matchKeys(example, subject, errors, path)
  }
  return errors
}

export const exampleAtPath = (example, path) => {
  const parts = Array.isArray(path)
    ? [...path]
    : path.replace(/\[[^\]]*\]/g, '.*').split('.')
  if(example === null || example === undefined || parts.length === 0) {
    return example
  } else {
    const part = parts.shift()
    if (part === '*') {
      if (Array.isArray(example)) {
        return oneOf(...example.map(x => exampleAtPath(x, parts)))
      } else {
        return undefined
      }
    } else {
      return exampleAtPath(example[part], parts)
    }
  }
}

const matchKeys = (example, subject, errors = [], path = '') => {
  for (let key of Object.keys(example).sort()) {
    matchType(example[key], subject[key], errors, path + '.' + key)
  }
  return errors
}
<|endoftext|>