<commit_before>// Avoid `chrome` namespace
if (typeof browser === "undefined" &&
    typeof chrome === "object"){
    //console.log("On Chrome");
    var browser = chrome;
}

browser.runtime.onInstalled.addListener(function(e){
    // Open options page to initialize localStorage
    if (e.reason === "install")
        browser.runtime.openOptionsPage();
    else if (e.reason === "update")
        if (localStorage.getItem("replyUser") === null)
            localStorage.setItem("replyUser", 1);

});

//——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————
const s = localStorage;
const storage = chrome.storage.sync;

browser.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    if ( request.img_base64 ){
        var post_url, patt_id, url_start, url_end, data;
        var img_status;
        //——————————设置微博或 imgur 的信息——————————
      storage.get(function (response) {
        if ( response.imageHosting === "weibo" ){
          post_url = "http://picupload.service.weibo.com/interface/pic_upload.php?\
                    ori=1&mime=image%2Fjpeg&data=base64&url=0&markpos=1&logo=&nick=0&marks=1&app=miniblog";
          patt_id = "pid\":\"(.*?)\"";
          url_start = "https://ws2.sinaimg.cn/large/";
          url_end = ".jpg";
          data = {"b64_data": request.img_base64};
        }else{
          post_url = "https://api.imgur.com/3/image";
          patt_id = "id\":\"(.*?)\"";
          url_start = "https://i.imgur.com/";
          url_end = ".png";
          data = {"image": request.img_base64};
        }
        $.ajax({
          url: post_url,
          method: "POST",
          data: data,
          dataType: "text",
          beforeSend: (xhr) => {
              if ( response.imageHosting === "imgur" ){
                xhr.setRequestHeader("Authorization", "Client-ID 9311f6be1c10160");
              }
          },
          success: (data) => {
            try{
              img_status = url_start + RegExp(patt_id).exec(data)[1] + url_end;
              //console.log("Succeed: "+ img_status);
            } catch(e){
              //console.error("Field not found");
              img_status = "Failed";
            }
          },
          error: () => {
            img_status = "Failed";
            //console.info("Request failed");
          },
          complete: () => {
            sendResponse({img_status: img_status});
          }
        });
      });
        return true;
    }
    //——————————微博或 imgur 的信息完成——————————
    //——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————

    //——————————————————————————————————返回设置选项——————————————————————————————————
    switch (request.action) {
    case "get_preview_status":
        sendResponse({preview_status: Number(s.getItem("preview"))});
        break;
    case "get_dblclickToTop":
        sendResponse({dblclickToTop: Number(s.getItem("dblclickToTop"))});
        break;
    case "get_replySetting":
        sendResponse({
            replyColor: s.getItem("replyColor"),
            replyA: s.getItem("replyA"),
            fold: Number(s.getItem("fold")),
            thankColor: s.getItem("thankColor")
        });
        break;
    case "get_newWindow_status":
        sendResponse({newWindow_status: Number(s.getItem("newWindow"))});
        break;
    case "get_replyUser":
        sendResponse({replyUser: Number(s.getItem("replyUser"))});
        break;
    case "get_blockList":
        $.get("https://www.v2ex.com",function(data,status){
            if(status == "success"){
                var block_list = /blocked = \[(.*?)\];/.exec(data);
                var username = /首页<\/a>   <a href="\/member\/(.+?)"/.exec(data);
                if ( block_list && username ){
                    block_list = block_list[1];
                    username = username[1];
                    browser.tabs.create({url:"/page/block_list.html#"+username+"="+block_list});
                }else{
                    alert("扩展没有获取到任何信息 : (\n或许是您未登录 V2EX 账号");
                }
            }else{
                alert("扩展没有获取到任何信息 : (\n很有可能是网络问题，请稍后再试");
            }
        });
        sendResponse({blockList: "get"});
        break;
    case "get_collectList":
        window.collectNotified = false
        sendResponse({cached: localStorage.collectTopicCachedReplyCountList, latest: localStorage.collectTopicLatestReplyCountList});
        break;
    case "clear_collect":
        localStorage.collectTopicCachedReplyCountList = request.list;
        localStorage.collectTopicLatestReplyCountList = request.list;
        sendResponse(null);
        break;
    case "sync_collect":
        localStorage.collectTopicCachedReplyCountList = request.cached;
        localStorage.collectTopicLatestReplyCountList = request.latest;
        break;
    default:
        throw "invaild action";
    }
});

//——————————————————————————————————返回设置选项——————————————————————————————————

//——————————————————————————————————定时任务初始化——————————————————————————————————

storage.get(function (response) {
  Number(response.newMsg) && checkMsg();
  Number(response.followMsg) && followMsg();
  Number(response.collectMsg) && collectMsg();
});

browser.alarms.create("checkMsg", {periodInMinutes: 5});
browser.alarms.create("autoMission", {periodInMinutes: 30});

browser.alarms.onAlarm.addListener(function( a ){
    switch (a.name){
    case "checkMsg":
      storage.get(function (response) {
        Number(response.newMsg) && checkMsg();
        Number(response.followMsg) && followMsg();
        Number(response.collectMsg) && collectMsg();
      });
        break;
    case "autoMission":
      storage.get(function (response) {
        Number(response.autoMission) && autoMission();
        Number(response.autoLoginWeibo) && autoLoginWeibo();
      });
        break;
    }
});

//——————————————————————————————————定时任务初始化——————————————————————————————————

//——————————————————————————————————检查关注人新主题——————————————————————————————————
function followMsg() {
    $.get('https://www.v2ex.com/my/following', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        window.a = $html
        var topics = $html.find('#Main .box:nth(0) table')
        if (!topics.length) return

        var $firstOne = topics.eq(2)
        var topicId = $firstOne.find('.item_title a').attr('href').substr(3).split('#')[0]
        var topic = $firstOne.find('.item_title').text()
        var author = $firstOne.find('.small.fade > strong:nth-child(3)').text()

        storage.get(function (response) {
          if( response.followMsgTopicId == topicId ) return
          storage.set({"followMsgTopicId":topicId});
          window.newFollowTopicId = topicId
          browser.notifications.create(
            "newFollowTopic" ,
            {
              type       : "basic",
              iconUrl    : "icon/icon38_msg.png",
              title      : "v2ex plus 提醒您",
              message    : `${author} 创作了新主题：${topic}`,
            });
        });

    })
}

//——————————————————————————————————检查关注人新主题——————————————————————————————————

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————
function collectMsg() {
    $.get('https://www.v2ex.com/my/topics', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        var topics = $html.find('div.cell.item')
        if (!topics.length) return

        var cachedReplyCountList = localStorage.collectTopicCachedReplyCountList
        cachedReplyCountList = cachedReplyCountList ? JSON.parse(cachedReplyCountList) : {}
        var latestReplyCountList = localStorage.collectTopicLatestReplyCountList
        latestReplyCountList = latestReplyCountList ? JSON.parse(latestReplyCountList) : {}

        var topicIds = [];
        var newReply = false
        var topicIndex
        for (topicIndex = 0; topicIndex < topics.length; topicIndex++){
            var topic = topics[topicIndex]
            var topicReplyCountEl = $(topic).find('.count_livid, .count_orange')
            var topicReplyCount = topicReplyCountEl.length ? Number(topicReplyCountEl[0].innerText) : 0
            var topicId = Number($(topic).find('.item_title a')[0].href.match(/\/t\/(\d+)/)[1]);
            topicIds.push(topicId)

            if (cachedReplyCountList[topicId] === undefined){
                cachedReplyCountList[topicId] = topicReplyCount
            }

            if (latestReplyCountList[topicId] === undefined){
                latestReplyCountList[topicId] = topicReplyCount
            }else if (latestReplyCountList[topicId] != topicReplyCount){
                latestReplyCountList[topicId] = topicReplyCount
                newReply = true
            }
        }

        for (topicIndex in cachedReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(cachedReplyCountList[topicIndex])
            }
        }

        for (topicIndex in latestReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(latestReplyCountList[topicIndex])
            }
        }

        localStorage.collectTopicCachedReplyCountList = JSON.stringify(cachedReplyCountList)
        localStorage.collectTopicLatestReplyCountList = JSON.stringify(latestReplyCountList)

        if (!window.collectNotified && newReply){
            window.collectNotified = true
            browser.notifications.create(
                "newCollectTopicReply" ,
            {
                type       : "basic",
                iconUrl    : "icon/icon38_msg.png",
                title      : "v2ex plus 提醒您",
                message    : "您收藏的主题有了新回复，点击查看",
            });
            //20分钟内最多提示一次
            setTimeout(function(){
                window.collectNotified = false
            }, 1200000)
        }
    })
}

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————

//——————————————————————————————————通知功能——————————————————————————————————

//现在是每5分钟刷新一次状态，除非点击了browserAction
function checkMsg(){
    $.get("https://www.v2ex.com/settings",function(data,status){
        if(status == "success"){
            var sign = RegExp("([0-9]*?) (条未读提醒|unread)").exec(data);
            sign = sign != null && sign[1] || "未登录";
            if ( sign == "未登录" ){
                browser.browserAction.setIcon({path: "icon/icon38_nologin.png"});
            }else if( sign!="0") {
                browser.browserAction.setIcon({path: "icon/icon38_msg.png"});
                browser.notifications.create(
                    "newMsg" ,
                    {
                        type       : "basic",
                        iconUrl    : "icon/icon38_msg.png",
                        title      : "v2ex plus 提醒您",
                        message    : "您有 V2EX 的未读新消息，点击查看。",
                    });
            }else{
                browser.browserAction.setIcon({path: "icon/icon38.png"});
            }
        }else{
            alert("V2EX消息获取失败：" + status);
        }
    });
}

//——————————————————————————————————通知功能——————————————————————————————————

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//清除通知图标，打开通知地址
function clean_msg(){
    browser.browserAction.setIcon({path: "icon/icon38.png"});
    browser.tabs.create({url:"https://www.v2ex.com/notifications"});
}

browser.commands.onCommand.addListener(clean_msg);
browser.browserAction.onClicked.addListener(clean_msg);
browser.notifications.onClicked.addListener(function(notificationId){
    switch (notificationId){
    case "newMsg":
        clean_msg();
        break;
    case "autoMission":
        browser.tabs.create({url:"https://www.v2ex.com/balance"});
        break;
    case "newFollowTopic":
        browser.tabs.create({url:`https://www.v2ex.com/t/${window.newFollowTopicId}?p=1`});
        break;
    case "newCollectTopicReply":
        browser.tabs.create({url:"https://www.v2ex.com/my/topics"});
        break;
    }
    browser.notifications.clear(notificationId);
});

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//——————————————————————————————————自动签到——————————————————————————————————
function autoMission(){
  storage.get(function (response) {
    if( response.autoMission == new Date().getUTCDate() ){
      //console.log('今天已经成功领取奖励了');
      return;
    }
    console.log('开始签到')
    $.ajax({
      url: "https://www.v2ex.com/",
      success: function(data){
        let sign = data.match("/signout(\\?once=[0-9]+)");
        sign = sign != null && sign[1] || "未登录";
        if ( sign != "未登录" ){
          $.ajax({
            url: "https://www.v2ex.com/mission/daily/redeem" + sign,
            success: function(data){
              if ( data.search("查看我的账户余额") ){
                let result = data.match(/已连续登录 (\d+?) 天/)
                browser.notifications.create(
                  "autoMission" ,
                  {
                    type    : "basic",
                    iconUrl : "icon/icon38_msg.png",
                    title   : "v2ex plus 提醒您",
                    message : `签到成功，${result[0]}。\nTake your passion and make it come true.`,
                  }
                );
                storage.set( {"autoMission" : new Date().getUTCDate()} );
              }else{
                alert("罕见错误！基本可以忽略，如果你遇见两次以上请联系开发者，当该提示已打扰到您，请关闭扩展的自动签到功能。");
              }
            },
            error: function(){
              alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
            }
          });
        }
      },
      error: function(){
        alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
      }
    });
  });

}
//——————————————————————————————————自动签到——————————————————————————————————

//——————————————————————————————————自动登陆微博——————————————————————————————————

function autoLoginWeibo(){
    //console.log('0.8 测试版！ 目前准备自动激活微博');
    $.get("http://weibo.com");
}

//——————————————————————————————————自动登陆微博——————————————————————————————————
<commit_msg>transfer user setting data to new storage during update<commit_after>// Avoid `chrome` namespace
if (typeof browser === "undefined" &&
    typeof chrome === "object"){
    //console.log("On Chrome");
    var browser = chrome;
}

browser.runtime.onInstalled.addListener(function(e){
    // Open options page to initialize localStorage
    if (e.reason === "install")
        browser.runtime.openOptionsPage();
    // else if (e.reason === "update")
    //     if (localStorage.getItem("replyUser") === null)
    //         localStorage.setItem("replyUser", 1);

    // 因新存储可通过浏览器自动同步设置，更新时主动告知用户此特性
    if (e.reason === 'update' &&
        e.previousVersion === '1.2.9' ||
        e.previousVersion.substring(0,3) !== '1.3'){
      // 迁移用户设置到新storage中，免去更新后重新设置的麻烦
        let obj = {};
        for (let i = 0; i < localStorage.length; i++) {
          obj[localStorage.key(i)] = localStorage.getItem(localStorage.key(i));
        }
        obj.followMsg = 1;
        obj.collectMsg = 0;
        chrome.storage.sync.set(obj);
        browser.notifications.create({
          type   : 'basic',
          iconUrl: 'icon/icon38_msg.png',
          title  : '我们刚刚进行了更新',
          message: '更新存储方式，现在用户设置可通过Chrome浏览器自动同步。若更新时发现配置丢失，请在配置页面中重新设置。',
        });
    }
});

//——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————
const s = localStorage;
const storage = chrome.storage.sync;

browser.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    if ( request.img_base64 ){
        var post_url, patt_id, url_start, url_end, data;
        var img_status;
        //——————————设置微博或 imgur 的信息——————————
      storage.get(function (response) {
        if ( response.imageHosting === "weibo" ){
          post_url = "http://picupload.service.weibo.com/interface/pic_upload.php?\
                    ori=1&mime=image%2Fjpeg&data=base64&url=0&markpos=1&logo=&nick=0&marks=1&app=miniblog";
          patt_id = "pid\":\"(.*?)\"";
          url_start = "https://ws2.sinaimg.cn/large/";
          url_end = ".jpg";
          data = {"b64_data": request.img_base64};
        }else{
          post_url = "https://api.imgur.com/3/image";
          patt_id = "id\":\"(.*?)\"";
          url_start = "https://i.imgur.com/";
          url_end = ".png";
          data = {"image": request.img_base64};
        }
        $.ajax({
          url: post_url,
          method: "POST",
          data: data,
          dataType: "text",
          beforeSend: (xhr) => {
              if ( response.imageHosting === "imgur" ){
                xhr.setRequestHeader("Authorization", "Client-ID 9311f6be1c10160");
              }
          },
          success: (data) => {
            try{
              img_status = url_start + RegExp(patt_id).exec(data)[1] + url_end;
              //console.log("Succeed: "+ img_status);
            } catch(e){
              //console.error("Field not found");
              img_status = "Failed";
            }
          },
          error: () => {
            img_status = "Failed";
            //console.info("Request failed");
          },
          complete: () => {
            sendResponse({img_status: img_status});
          }
        });
      });
        return true;
    }
    //——————————微博或 imgur 的信息完成——————————
    //——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————

    //——————————————————————————————————返回设置选项——————————————————————————————————
    switch (request.action) {
    case "get_preview_status":
        sendResponse({preview_status: Number(s.getItem("preview"))});
        break;
    case "get_dblclickToTop":
        sendResponse({dblclickToTop: Number(s.getItem("dblclickToTop"))});
        break;
    case "get_replySetting":
        sendResponse({
            replyColor: s.getItem("replyColor"),
            replyA: s.getItem("replyA"),
            fold: Number(s.getItem("fold")),
            thankColor: s.getItem("thankColor")
        });
        break;
    case "get_newWindow_status":
        sendResponse({newWindow_status: Number(s.getItem("newWindow"))});
        break;
    case "get_replyUser":
        sendResponse({replyUser: Number(s.getItem("replyUser"))});
        break;
    case "get_blockList":
        $.get("https://www.v2ex.com",function(data,status){
            if(status == "success"){
                var block_list = /blocked = \[(.*?)\];/.exec(data);
                var username = /首页<\/a>   <a href="\/member\/(.+?)"/.exec(data);
                if ( block_list && username ){
                    block_list = block_list[1];
                    username = username[1];
                    browser.tabs.create({url:"/page/block_list.html#"+username+"="+block_list});
                }else{
                    alert("扩展没有获取到任何信息 : (\n或许是您未登录 V2EX 账号");
                }
            }else{
                alert("扩展没有获取到任何信息 : (\n很有可能是网络问题，请稍后再试");
            }
        });
        sendResponse({blockList: "get"});
        break;
    case "get_collectList":
        window.collectNotified = false
        sendResponse({cached: localStorage.collectTopicCachedReplyCountList, latest: localStorage.collectTopicLatestReplyCountList});
        break;
    case "clear_collect":
        localStorage.collectTopicCachedReplyCountList = request.list;
        localStorage.collectTopicLatestReplyCountList = request.list;
        sendResponse(null);
        break;
    case "sync_collect":
        localStorage.collectTopicCachedReplyCountList = request.cached;
        localStorage.collectTopicLatestReplyCountList = request.latest;
        break;
    default:
        throw "invaild action";
    }
});

//——————————————————————————————————返回设置选项——————————————————————————————————

//——————————————————————————————————定时任务初始化——————————————————————————————————

storage.get(function (response) {
  Number(response.newMsg) && checkMsg();
  Number(response.followMsg) && followMsg();
  Number(response.collectMsg) && collectMsg();
});

browser.alarms.create("checkMsg", {periodInMinutes: 5});
browser.alarms.create("autoMission", {periodInMinutes: 30});

browser.alarms.onAlarm.addListener(function( a ){
    switch (a.name){
    case "checkMsg":
      storage.get(function (response) {
        Number(response.newMsg) && checkMsg();
        Number(response.followMsg) && followMsg();
        Number(response.collectMsg) && collectMsg();
      });
        break;
    case "autoMission":
      storage.get(function (response) {
        Number(response.autoMission) && autoMission();
        Number(response.autoLoginWeibo) && autoLoginWeibo();
      });
        break;
    }
});

//——————————————————————————————————定时任务初始化——————————————————————————————————

//——————————————————————————————————检查关注人新主题——————————————————————————————————
function followMsg() {
    $.get('https://www.v2ex.com/my/following', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        window.a = $html
        var topics = $html.find('#Main .box:nth(0) table')
        if (!topics.length) return

        var $firstOne = topics.eq(2)
        var topicId = $firstOne.find('.item_title a').attr('href').substr(3).split('#')[0]
        var topic = $firstOne.find('.item_title').text()
        var author = $firstOne.find('.small.fade > strong:nth-child(3)').text()

        storage.get(function (response) {
          if( response.followMsgTopicId == topicId ) return
          storage.set({"followMsgTopicId":topicId});
          window.newFollowTopicId = topicId
          browser.notifications.create(
            "newFollowTopic" ,
            {
              type       : "basic",
              iconUrl    : "icon/icon38_msg.png",
              title      : "v2ex plus 提醒您",
              message    : `${author} 创作了新主题：${topic}`,
            });
        });

    })
}

//——————————————————————————————————检查关注人新主题——————————————————————————————————

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————
function collectMsg() {
    $.get('https://www.v2ex.com/my/topics', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        var topics = $html.find('div.cell.item')
        if (!topics.length) return

        var cachedReplyCountList = localStorage.collectTopicCachedReplyCountList
        cachedReplyCountList = cachedReplyCountList ? JSON.parse(cachedReplyCountList) : {}
        var latestReplyCountList = localStorage.collectTopicLatestReplyCountList
        latestReplyCountList = latestReplyCountList ? JSON.parse(latestReplyCountList) : {}

        var topicIds = [];
        var newReply = false
        var topicIndex
        for (topicIndex = 0; topicIndex < topics.length; topicIndex++){
            var topic = topics[topicIndex]
            var topicReplyCountEl = $(topic).find('.count_livid, .count_orange')
            var topicReplyCount = topicReplyCountEl.length ? Number(topicReplyCountEl[0].innerText) : 0
            var topicId = Number($(topic).find('.item_title a')[0].href.match(/\/t\/(\d+)/)[1]);
            topicIds.push(topicId)

            if (cachedReplyCountList[topicId] === undefined){
                cachedReplyCountList[topicId] = topicReplyCount
            }

            if (latestReplyCountList[topicId] === undefined){
                latestReplyCountList[topicId] = topicReplyCount
            }else if (latestReplyCountList[topicId] != topicReplyCount){
                latestReplyCountList[topicId] = topicReplyCount
                newReply = true
            }
        }

        for (topicIndex in cachedReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(cachedReplyCountList[topicIndex])
            }
        }

        for (topicIndex in latestReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(latestReplyCountList[topicIndex])
            }
        }

        localStorage.collectTopicCachedReplyCountList = JSON.stringify(cachedReplyCountList)
        localStorage.collectTopicLatestReplyCountList = JSON.stringify(latestReplyCountList)

        if (!window.collectNotified && newReply){
            window.collectNotified = true
            browser.notifications.create(
                "newCollectTopicReply" ,
            {
                type       : "basic",
                iconUrl    : "icon/icon38_msg.png",
                title      : "v2ex plus 提醒您",
                message    : "您收藏的主题有了新回复，点击查看",
            });
            //20分钟内最多提示一次
            setTimeout(function(){
                window.collectNotified = false
            }, 1200000)
        }
    })
}

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————

//——————————————————————————————————通知功能——————————————————————————————————

//现在是每5分钟刷新一次状态，除非点击了browserAction
function checkMsg(){
    $.get("https://www.v2ex.com/settings",function(data,status){
        if(status == "success"){
            var sign = RegExp("([0-9]*?) (条未读提醒|unread)").exec(data);
            sign = sign != null && sign[1] || "未登录";
            if ( sign == "未登录" ){
                browser.browserAction.setIcon({path: "icon/icon38_nologin.png"});
            }else if( sign!="0") {
                browser.browserAction.setIcon({path: "icon/icon38_msg.png"});
                browser.notifications.create(
                    "newMsg" ,
                    {
                        type       : "basic",
                        iconUrl    : "icon/icon38_msg.png",
                        title      : "v2ex plus 提醒您",
                        message    : "您有 V2EX 的未读新消息，点击查看。",
                    });
            }else{
                browser.browserAction.setIcon({path: "icon/icon38.png"});
            }
        }else{
            alert("V2EX消息获取失败：" + status);
        }
    });
}

//——————————————————————————————————通知功能——————————————————————————————————

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//清除通知图标，打开通知地址
function clean_msg(){
    browser.browserAction.setIcon({path: "icon/icon38.png"});
    browser.tabs.create({url:"https://www.v2ex.com/notifications"});
}

browser.commands.onCommand.addListener(clean_msg);
browser.browserAction.onClicked.addListener(clean_msg);
browser.notifications.onClicked.addListener(function(notificationId){
    switch (notificationId){
    case "newMsg":
        clean_msg();
        break;
    case "autoMission":
        browser.tabs.create({url:"https://www.v2ex.com/balance"});
        break;
    case "newFollowTopic":
        browser.tabs.create({url:`https://www.v2ex.com/t/${window.newFollowTopicId}?p=1`});
        break;
    case "newCollectTopicReply":
        browser.tabs.create({url:"https://www.v2ex.com/my/topics"});
        break;
    }
    browser.notifications.clear(notificationId);
});

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//——————————————————————————————————自动签到——————————————————————————————————
function autoMission(){
  storage.get(function (response) {
    if( response.autoMission == new Date().getUTCDate() ){
      //console.log('今天已经成功领取奖励了');
      return;
    }
    console.log('开始签到')
    $.ajax({
      url: "https://www.v2ex.com/",
      success: function(data){
        let sign = data.match("/signout(\\?once=[0-9]+)");
        sign = sign != null && sign[1] || "未登录";
        if ( sign != "未登录" ){
          $.ajax({
            url: "https://www.v2ex.com/mission/daily/redeem" + sign,
            success: function(data){
              if ( data.search("查看我的账户余额") ){
                let result = data.match(/已连续登录 (\d+?) 天/)
                browser.notifications.create(
                  "autoMission" ,
                  {
                    type    : "basic",
                    iconUrl : "icon/icon38_msg.png",
                    title   : "v2ex plus 提醒您",
                    message : `签到成功，${result[0]}。\nTake your passion and make it come true.`,
                  }
                );
                storage.set( {"autoMission" : new Date().getUTCDate()} );
              }else{
                alert("罕见错误！基本可以忽略，如果你遇见两次以上请联系开发者，当该提示已打扰到您，请关闭扩展的自动签到功能。");
              }
            },
            error: function(){
              alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
            }
          });
        }
      },
      error: function(){
        alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
      }
    });
  });

}
//——————————————————————————————————自动签到——————————————————————————————————

//——————————————————————————————————自动登陆微博——————————————————————————————————

function autoLoginWeibo(){
    //console.log('0.8 测试版！ 目前准备自动激活微博');
    $.get("http://weibo.com");
}

//——————————————————————————————————自动登陆微博——————————————————————————————————
<|endoftext|>


<commit_before>'use strict';

const fs = require('fs');
const path = require('path');
const util = require('util');
const findit = require('findit');
const assert = require('assert');
const rimraf = require('rimraf');
const Emitter = require('events').EventEmitter;

util.inherits(Remy, Emitter);

module.exports = (from, files) => {
    assert(typeof from === 'string', 'from should be string!');

    if (!files) {
        files   = [
            path.basename(from)
        ];

        from = path.dirname(from);
    }

    const emitter = new Remy(from, files);

    return emitter;
};

function Remy(from, files) {
    this._i             = 0;
    this._files         = [];
    this._dirs          = [];

    this._percent       = 0;
    this._percentPrev   = 0;

    this._parallel(from, files, () => {
        const filesCount = this._files.length;
        const dirsCount  = this._dirs.length;

        this._n = filesCount + dirsCount;

        this._rmAll();
    });
}

Remy.prototype._parallel  = function(from, files, callback) {
    let i = files.length;
    const fn = () => {
        if (!--i)
            callback();
    };

    files.forEach((name) => {
        const full = path.join(from, name);

        this._findFiles(full, fn);
    });
};

Remy.prototype.continue = function() {
    this._pause = false;
    this._rmAll();
};

Remy.prototype.pause = function() {
    this._pause = true;
};

Remy.prototype.abort = function() {
    this._files = [];
    this._dirs  = [];

    this._rmAll();
};

Remy.prototype._rmAll = function() {
    let rmFn = this._rmOneFile;
    let name = this._files.shift();

    if (!name) {
        name = this._dirs.shift();
        rmFn = this._rmOneDir;
    }

    if (!name)
        return this.emit('end');

    const rmEmitter = rmFn(name);

    rmEmitter.on('error', (error) => {
        this._progress();
        this.emit('error', error);
    });

    rmEmitter.on('end', () => {
        this._progress();

        this.emit('file', name);

        if (!this._pause)
            this._rmAll();
    });
};

Remy.prototype._findFiles = function(filename, fn) {
    const finder = findit(filename);

    const onError = (error) => {
        this._files.push(error.path);
    };

    const onFile = (name) => {
        this._files.push(name);
    };

    const onDir = (name) => {
        this._dirs.push(name);
    };

    finder.on('file', onFile);
    finder.on('error', onError);
    finder.on('directory', onDir);
    finder.on('link', onFile);
    finder.on('end', fn);
};

Remy.prototype._rmOneFile = function(name) {
    const emitter = new Emitter();

    fs.unlink(name, (error) => {
        if (error)
            return emitter.emit('error', error);

        emitter.emit('end');
    });

    return emitter;
};

Remy.prototype._rmOneDir = function(name) {
    const emitter = new Emitter();

    rimraf(name, (error) => {
        if (error && error.code !== 'ENOENT')
            return emitter.emit('error', error);

        emitter.emit('end');
    });

    return emitter;
};

Remy.prototype._progress = function() {
    ++this._i;

    const value = Math.round(this._i * 100 / this._n);

    this._percent = value;

    if (value === this._percentPrev)
        return;

    this._percentPrev = value;
    this.emit('progress', value);
};

<commit_msg>chore(remy) rm emitter<commit_after>'use strict';

const fs = require('fs');
const path = require('path');
const util = require('util');
const findit = require('findit');
const assert = require('assert');
const rimraf = require('rimraf');
const Emitter = require('events').EventEmitter;

util.inherits(Remy, Emitter);

module.exports = (from, files) => {
    assert(typeof from === 'string', 'from should be string!');

    if (!files) {
        files   = [
            path.basename(from)
        ];

        from = path.dirname(from);
    }

    return new Remy(from, files);
};

function Remy(from, files) {
    this._i             = 0;
    this._files         = [];
    this._dirs          = [];

    this._percent       = 0;
    this._percentPrev   = 0;

    this._parallel(from, files, () => {
        const filesCount = this._files.length;
        const dirsCount  = this._dirs.length;

        this._n = filesCount + dirsCount;

        this._rmAll();
    });
}

Remy.prototype._parallel  = function(from, files, callback) {
    let i = files.length;
    const fn = () => {
        if (!--i)
            callback();
    };

    files.forEach((name) => {
        const full = path.join(from, name);

        this._findFiles(full, fn);
    });
};

Remy.prototype.continue = function() {
    this._pause = false;
    this._rmAll();
};

Remy.prototype.pause = function() {
    this._pause = true;
};

Remy.prototype.abort = function() {
    this._files = [];
    this._dirs  = [];

    this._rmAll();
};

Remy.prototype._rmAll = function() {
    let rmFn = this._rmOneFile;
    let name = this._files.shift();

    if (!name) {
        name = this._dirs.shift();
        rmFn = this._rmOneDir;
    }

    if (!name)
        return this.emit('end');

    const rmEmitter = rmFn(name);

    rmEmitter.on('error', (error) => {
        this._progress();
        this.emit('error', error);
    });

    rmEmitter.on('end', () => {
        this._progress();

        this.emit('file', name);

        if (!this._pause)
            this._rmAll();
    });
};

Remy.prototype._findFiles = function(filename, fn) {
    const finder = findit(filename);

    const onError = (error) => {
        this._files.push(error.path);
    };

    const onFile = (name) => {
        this._files.push(name);
    };

    const onDir = (name) => {
        this._dirs.push(name);
    };

    finder.on('file', onFile);
    finder.on('error', onError);
    finder.on('directory', onDir);
    finder.on('link', onFile);
    finder.on('end', fn);
};

Remy.prototype._rmOneFile = function(name) {
    const emitter = new Emitter();

    fs.unlink(name, (error) => {
        if (error)
            return emitter.emit('error', error);

        emitter.emit('end');
    });

    return emitter;
};

Remy.prototype._rmOneDir = function(name) {
    const emitter = new Emitter();

    rimraf(name, (error) => {
        if (error && error.code !== 'ENOENT')
            return emitter.emit('error', error);

        emitter.emit('end');
    });

    return emitter;
};

Remy.prototype._progress = function() {
    ++this._i;

    const value = Math.round(this._i * 100 / this._n);

    this._percent = value;

    if (value === this._percentPrev)
        return;

    this._percentPrev = value;
    this.emit('progress', value);
};

<|endoftext|>


<commit_before>"use strict";

var _ = require('lodash');
var async = require('async');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var sira = require('sira');
var express = require('express');
var debug = require('debug')('sira:rest');

var bodyParser = require('body-parser');
var cors = require('cors');

var json = bodyParser.json;
var urlencoded = bodyParser.urlencoded;

var SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript'];

exports = module.exports = function (remotes, sapp) {
    if (remotes.remotes && remotes.handle) {
        sapp = remotes;
        remotes = sapp.remotes;
    }
    var rest = new Rest(remotes, sapp);
    return rest.createHandler();
};

exports.Rest = Rest;
exports.RestClass = RestClass;
exports.RestMethod = RestMethod;
exports.sortRoutes = sortRoutes;

function Rest(remotes, sapp) {
    EventEmitter.call(this);

    this.remotes = remotes;
    this.sapp = sapp;
}

util.inherits(Rest, EventEmitter);

Rest.prototype.createHandler = function () {
    var root = express.Router();
    var rest = this;
    var classes = this.buildClasses();

    // Add a handler to tolerate empty json as connect's json middleware throws an error
    root.use(function (req, res, next) {
        if (req.is('application/json')) {
            if (req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked
                req._body = true; // Mark it as parsed
                req.body = {};
            }
        }
        next();
    });

    // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
    debug("remoting options: %j", this.remotes.options);
    var urlencodedOptions = this.remotes.options.urlencoded || {extended: true};
    if (urlencodedOptions.extended === undefined) {
        urlencodedOptions.extended = true;
    }
    var jsonOptions = this.remotes.options.json || {strict: false};
    var corsOptions = this.remotes.options.cors || {};
    root.use(urlencoded(urlencodedOptions));
    root.use(json(jsonOptions));
    root.use(cors(corsOptions));

    classes.forEach(function (restClass) {
        var router = express.Router();
        var className = restClass.sharedClass.name;

        debug('registering REST handler for class %j', className);

        var methods = [];
        // Register handlers for all shared methods of this class sharedClass
        restClass
            .methods
            .forEach(function (restMethod) {
                var sharedMethod = restMethod.sharedMethod;
                debug('    method %s', sharedMethod.stringName);
                restMethod.routes.forEach(function (route) {
                    methods.push({route: route, method: sharedMethod});
                });
            });

        // Sort all methods based on the route path
        methods.sort(sortRoutes);

        methods.forEach(function (m) {
            rest._registerMethodRouteHandlers(router, m.method, m.route);
        });

        // Convert requests for unknown methods of this sharedClass into 404.
        // Do not allow other middleware to invade our URL space.
        router.use(Rest.remoteMethodNotFoundHandler(className));

        // Mount the remoteClass router on all class routes.
        restClass
            .routes
            .forEach(function (route) {
                debug('    at %s', route.path);
                root.use(route.path, router);
            });

    });

    // Convert requests for unknown URLs into 404.
    // Do not allow other middleware to invade our URL space.
    root.use(Rest.urlNotFoundHandler());

    // Use our own error handler to make sure the error response has
    // always the format expected by remoting clients.
    root.use(Rest.errorHandler());

    return root;
};

Rest.prototype.buildClasses = function () {
    return this.remotes.classes().map(function (c) {
        return new RestClass(c);
    });
};

Rest.prototype._registerMethodRouteHandlers = function (router, sharedMethod, route) {
    debug('        %s %s %s', route.verb, route.path, sharedMethod.stringName);
    var verb = route.verb;
    if (verb === 'del') {
        // Express 4.x only supports delete
        verb = 'delete';
    }
    var rest = this;
    router[verb](route.path, function (req, res, next) {
        var context = new RestContext(req, res, sharedMethod, rest.sapp);
        rest._handleRequest(context, sharedMethod, next);
    });
};

Rest.prototype._handleRequest = function (ctx, method, next) {

    var remotes = this.remotes;
    var steps = [];

    if (method.rest.before) {
        steps.push(function invokeRestBefore(cb) {
            debug('Invoking rest.before for ' + ctx.methodString);
            method.rest.before.call(remotes.getScope(ctx, method), ctx, cb);
        });
    }

    steps.push(function (cb) {
        ctx.handle(method, cb);
    });

    if (method.rest.after) {
        steps.push(function invokeRestAfter(cb) {
            debug('Invoking rest.after for ' + ctx.methodString);
            method.rest.after.call(remotes.getScope(ctx, method), ctx, cb);
        });
    }

    async.series(steps, function (err) {
        if (err) return next(err);
        ctx.done();
    });
};

Rest.remoteMethodNotFoundHandler = function (className) {
    className = className || '(unknown)';
    return function restRemoteMethodNotFound(req, res, next) {
        var message = 'Shared class "' + className + '"' +
            ' has no method handling ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.urlNotFoundHandler = function () {
    return function restUrlNotFound(req, res, next) {
        var message = 'There is no method to handle ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.errorHandler = function () {
    return function restErrorHandler(err, req, res, next) {
        if (typeof err === 'string') {
            err = new Error(err);
            err.status = err.statusCode = 500;
        }

        res.statusCode = err.statusCode || err.status || 500;

        debug('Error in %s %s: %s', req.method, req.url, err.stack);
        var data = {
            name: err.name,
            status: res.statusCode,
            message: err.message || 'An unknown error occurred'
        };

        for (var prop in err)
            data[prop] = err[prop];

        // TODO(bajtos) Remove stack info when running in production
//        data.stack = err.stack;

        res.send({ error: data });
    };
};

Rest.prototype.allRoutes = function () {
    var routes = [];
    var adapter = this;
    var classes = this.remotes.classes();
    var currentRoot = '';

    classes.forEach(function (sc) {
        adapter
            .getRoutes(sc)
            .forEach(function (classRoute) {
                currentRoot = classRoute.path;
                var methods = sc.methods();

                methods.forEach(function (method) {
                    adapter.getRoutes(method).forEach(function (route) {
                        if (method.isStatic) {
                            addRoute(route.verb, route.path, method);
                        } else {
                            adapter
                                .getRoutes(method.sharedCtor)
                                .forEach(function (sharedCtorRoute) {
                                    addRoute(route.verb, sharedCtorRoute.path + route.path, method);
                                });
                        }
                    });
                });
            });
    });

    return routes;

    function addRoute(verb, path, method) {
        if (path === '/' || path === '//') {
            path = currentRoot;
        } else {
            path = currentRoot + path;
        }

        if (path[path.length - 1] === '/') {
            path = path.substr(0, path.length - 1);
        }

        // TODO this could be cleaner
        path = path.replace(/\/\//g, '/');

        routes.push({
            verb: verb,
            path: path,
            description: method.description,
            method: method.stringName,
            accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
            returns: (method.returns && method.returns.length) ? method.returns : undefined
        });
    }
};

/*!
 * Compare two routes
 * @param {Object} r1 The first route {route: {verb: 'get', path: '/:id'}, method: ...}
 * @param [Object} r2 The second route route: {verb: 'get', path: '/findOne'}, method: ...}
 * @returns {number} 1: r1 comes after 2, -1: r1 comes before r2, 0: equal
 */
function sortRoutes(r1, r2) {
    var a = r1.route;
    var b = r2.route;

    // Normalize the verbs
    var verb1 = a.verb.toLowerCase();
    var verb2 = b.verb.toLowerCase();

    if (verb1 === 'del') {
        verb1 = 'delete';
    }
    if (verb2 === 'del') {
        verb2 = 'delete';
    }
    // First sort by verb
    if (verb1 > verb2) {
        return -1;
    } else if (verb1 < verb2) {
        return 1;
    }

    // Sort by path part by part using the / delimiter
    // For example '/:id' will become ['', ':id'], '/findOne' will become
    // ['', 'findOne']
    var p1 = a.path.split('/');
    var p2 = b.path.split('/');
    var len = Math.min(p1.length, p2.length);

    // Loop through the parts and decide which path should come first
    for (var i = 0; i < len; i++) {
        // Empty part has lower weight
        if (p1[i] === '' && p2[i] !== '') {
            return 1;
        } else if (p1[i] !== '' && p2[i] === '') {
            return -1;
        }
        // Wildcard has lower weight
        if (p1[i][0] === ':' && p2[i][0] !== ':') {
            return 1;
        } else if (p1[i][0] !== ':' && p2[i][0] === ':') {
            return -1;
        }
        // Now the regular string comparision
        if (p1[i] > p2[i]) {
            return 1;
        } else if (p1[i] < p2[i]) {
            return -1;
        }
    }
    // Both paths have the common parts. The longer one should come before the
    // shorter one
    return p2.length - p1.length;
}

/**
 * Get the path for the given method.
 */

//Rest.prototype.buildRoutes = buildRoutes;
function buildRoutes(obj) {
    var routes = obj.http;

    if (routes && !Array.isArray(routes)) {
        routes = [routes];
    }

    // overidden
    if (routes) {
        // patch missing verbs / routes
        routes.forEach(function (r) {
            r.verb = String(r.verb || 'all').toLowerCase();
            r.path = r.path || ('/' + obj.name);
        });
    } else {
        if (obj.name === 'sharedCtor') {
            routes = [
                {
                    verb: 'all',
                    path: '/prototype'
                }
            ];
        } else {
            // build default route
            routes = [
                {
                    verb: 'all',
                    path: obj.name ? ('/' + obj.name) : ''
                }
            ];
        }
    }

    return routes;
}

function RestClass(sharedClass) {
    hiddenConstProperty(this, 'sharedClass', sharedClass);

    var self = this;
    this.name = sharedClass.name;
    this.routes = buildRoutes(sharedClass);

    this.ctor = sharedClass.sharedCtor && new RestMethod(this, sharedClass.sharedCtor);

    this.methods = sharedClass.methods()
        .filter(function (sm) {
            return !sm.isSharedCtor;
        })
        .map(function (sm) {
            return new RestMethod(self, sm);
        });
}

RestClass.prototype.getPath = function () {
    return this.routes[0].path;
};

function RestMethod(restClass, sharedMethod) {
    hiddenConstProperty(this, 'restClass', restClass);
    hiddenConstProperty(this, 'sharedMethod', sharedMethod);

    // The full name is ClassName.methodName or ClassName.prototype.methodName
    this.fullName = sharedMethod.stringName;
    this.name = this.fullName.split('.').slice(1).join('.');

    this.accepts = sharedMethod.accepts;
    this.returns = sharedMethod.returns;
    this.description = sharedMethod.description;

    var methodRoutes = buildRoutes(sharedMethod);
    if (sharedMethod.isStatic || !restClass.ctor) {
        this.routes = methodRoutes;
    } else {
        var routes = this.routes = [];
        methodRoutes.forEach(function (route) {
            restClass.ctor.routes.forEach(function (ctorRoute) {
                var fullRoute = util._extend({}, route);
                fullRoute.path = joinPaths(ctorRoute.path, route.path);
                routes.push(fullRoute);
            });
        });
    }
}

RestMethod.prototype.isReturningArray = function () {
    return this.returns.length == 1 &&
        this.returns[0].root &&
        getTypeString(this.returns[0].type) === 'array' || false;
};

RestMethod.prototype.acceptsSingleBodyArgument = function () {
    if (this.accepts.length != 1) return false;
    var accepts = this.accepts[0];

    return accepts.http &&
        accepts.http.source == 'body' &&
        getTypeString(accepts.type) == 'object' || false;
};

RestMethod.prototype.getHttpMethod = function () {
    var verb = this.routes[0].verb;
    if (verb == 'all') return 'POST';
    if (verb == 'del') return 'DELETE';
    return verb.toUpperCase();
};

RestMethod.prototype.getPath = function () {
    return this.routes[0].path;
};

RestMethod.prototype.getFullPath = function () {
    return joinPaths(this.restClass.getPath(), this.getPath());
};

function RestContext(req, res, method, sapp) {
    this.req = req;
    this.res = res;
    this.method = method;
    this.sapp = sapp;
}

RestContext.prototype.handle = function (method, cb) {
    var self = this, req = this.req, canceled;

    // future may be null if some sira middleware are async
    var future = this.sapp.rekuest(method).props(req).payload(req.body)
        .send(function (err, result) {
            if (err === 'canceled' || canceled) return debug('request has been canceled - ' + req.url);
            if (err) return cb(err);
            self.result = result;
            cb();
        });

    this.req.on('close', function () {
        canceled = true;
        future && future.cancel();
    });
};

RestContext.prototype.done = function () {
    // send the result back as
    // the requested content type
    var data = this.result;
    var res = this.res;
    var accepts = this.req.accepts(SUPPORTED_TYPES);
    var dataExists = typeof data !== 'undefined';

    if (dataExists) {
        switch (accepts) {
            case 'json':
                res.json(data);
                break;
            case 'application/javascript':
            case 'text/javascript':
                res.jsonp(data);
                break;
            default:
                // not acceptable
                res.send(406);
                break;
        }
    } else {
        res.header('Content-Type', 'application/json');
        res.statusCode = 204;
        res.end();
    }
};

function hiddenConstProperty(where, property, value) {
    Object.defineProperty(where, property, {
        writable: false,
        enumerable: false,
        configurable: false,
        value: value
    });
}

function getTypeString(ctorOrName) {
    if (typeof ctorOrName === 'function')
        ctorOrName = ctorOrName.name;
    if (typeof ctorOrName === 'string') {
        return ctorOrName.toLowerCase();
    } else {
        debug('WARNING: unknown ctorOrName of type %s: %j', typeof ctorOrName, ctorOrName);
        return typeof undefined;
    }
}

function joinPaths(left, right) {
    if (!left) return right;
    if (!right || right == '/') return left;

    var glue = left[left.length - 1] + right[0];
    if (glue == '//')
        return left + right.slice(1);
    else if (glue[0] == '/' || glue[1] == '/')
        return left + right;
    else
        return left + '/' + right;
}

<commit_msg>expose classes to rest handler<commit_after>"use strict";

var _ = require('lodash');
var async = require('async');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var sira = require('sira');
var express = require('express');
var debug = require('debug')('sira:rest');

var bodyParser = require('body-parser');
var cors = require('cors');

var json = bodyParser.json;
var urlencoded = bodyParser.urlencoded;

var SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript'];

exports = module.exports = function (remotes, sapp) {
    if (remotes.remotes && remotes.handle) {
        sapp = remotes;
        remotes = sapp.remotes;
    }
    var rest = new Rest(remotes, sapp);
    return rest.createHandler();
};

exports.Rest = Rest;
exports.RestClass = RestClass;
exports.RestMethod = RestMethod;
exports.sortRoutes = sortRoutes;

function Rest(remotes, sapp) {
    EventEmitter.call(this);

    this.remotes = remotes;
    this.sapp = sapp;
}

util.inherits(Rest, EventEmitter);

Rest.prototype.createHandler = function () {
    var root = express.Router();
    var rest = this;
    var classes = root.classes = this.buildClasses();

    // Add a handler to tolerate empty json as connect's json middleware throws an error
    root.use(function (req, res, next) {
        if (req.is('application/json')) {
            if (req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked
                req._body = true; // Mark it as parsed
                req.body = {};
            }
        }
        next();
    });

    // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
    debug("remoting options: %j", this.remotes.options);
    var urlencodedOptions = this.remotes.options.urlencoded || {extended: true};
    if (urlencodedOptions.extended === undefined) {
        urlencodedOptions.extended = true;
    }
    var jsonOptions = this.remotes.options.json || {strict: false};
    var corsOptions = this.remotes.options.cors || {};
    root.use(urlencoded(urlencodedOptions));
    root.use(json(jsonOptions));
    root.use(cors(corsOptions));

    classes.forEach(function (restClass) {
        var router = express.Router();
        var className = restClass.sharedClass.name;

        debug('registering REST handler for class %j', className);

        var methods = [];
        // Register handlers for all shared methods of this class sharedClass
        restClass
            .methods
            .forEach(function (restMethod) {
                var sharedMethod = restMethod.sharedMethod;
                debug('    method %s', sharedMethod.stringName);
                restMethod.routes.forEach(function (route) {
                    methods.push({route: route, method: sharedMethod});
                });
            });

        // Sort all methods based on the route path
        methods.sort(sortRoutes);

        methods.forEach(function (m) {
            rest._registerMethodRouteHandlers(router, m.method, m.route);
        });

        // Convert requests for unknown methods of this sharedClass into 404.
        // Do not allow other middleware to invade our URL space.
        router.use(Rest.remoteMethodNotFoundHandler(className));

        // Mount the remoteClass router on all class routes.
        restClass
            .routes
            .forEach(function (route) {
                debug('    at %s', route.path);
                root.use(route.path, router);
            });

    });

    // Convert requests for unknown URLs into 404.
    // Do not allow other middleware to invade our URL space.
    root.use(Rest.urlNotFoundHandler());

    // Use our own error handler to make sure the error response has
    // always the format expected by remoting clients.
    root.use(Rest.errorHandler());

    return root;
};

Rest.prototype.buildClasses = function () {
    return this.remotes.classes().map(RestClass);
};

Rest.prototype._registerMethodRouteHandlers = function (router, sharedMethod, route) {
    debug('        %s %s %s', route.verb, route.path, sharedMethod.stringName);
    var verb = route.verb;
    if (verb === 'del') {
        // Express 4.x only supports delete
        verb = 'delete';
    }
    var rest = this;
    router[verb](route.path, function (req, res, next) {
        var context = new RestContext(req, res, sharedMethod, rest.sapp);
        rest._handleRequest(context, sharedMethod, next);
    });
};

Rest.prototype._handleRequest = function (ctx, method, next) {

    var remotes = this.remotes;
    var steps = [];

    if (method.rest.before) {
        steps.push(function invokeRestBefore(cb) {
            debug('Invoking rest.before for ' + ctx.methodString);
            method.rest.before.call(remotes.getScope(ctx, method), ctx, cb);
        });
    }

    steps.push(function (cb) {
        ctx.handle(method, cb);
    });

    if (method.rest.after) {
        steps.push(function invokeRestAfter(cb) {
            debug('Invoking rest.after for ' + ctx.methodString);
            method.rest.after.call(remotes.getScope(ctx, method), ctx, cb);
        });
    }

    async.series(steps, function (err) {
        if (err) return next(err);
        ctx.done();
    });
};

Rest.remoteMethodNotFoundHandler = function (className) {
    className = className || '(unknown)';
    return function restRemoteMethodNotFound(req, res, next) {
        var message = 'Shared class "' + className + '"' +
            ' has no method handling ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.urlNotFoundHandler = function () {
    return function restUrlNotFound(req, res, next) {
        var message = 'There is no method to handle ' + req.method + ' ' + req.url;
        var error = new Error(message);
        error.status = error.statusCode = 404;
        next(error);
    };
};

Rest.errorHandler = function () {
    return function restErrorHandler(err, req, res, next) {
        if (typeof err === 'string') {
            err = new Error(err);
            err.status = err.statusCode = 500;
        }

        res.statusCode = err.statusCode || err.status || 500;

        debug('Error in %s %s: %s', req.method, req.url, err.stack);
        var data = {
            name: err.name,
            status: res.statusCode,
            message: err.message || 'An unknown error occurred'
        };

        for (var prop in err)
            data[prop] = err[prop];

        // TODO(bajtos) Remove stack info when running in production
//        data.stack = err.stack;

        res.send({ error: data });
    };
};

Rest.prototype.allRoutes = function () {
    var routes = [];
    var adapter = this;
    var classes = this.remotes.classes();
    var currentRoot = '';

    classes.forEach(function (sc) {
        adapter
            .getRoutes(sc)
            .forEach(function (classRoute) {
                currentRoot = classRoute.path;
                var methods = sc.methods();

                methods.forEach(function (method) {
                    adapter.getRoutes(method).forEach(function (route) {
                        if (method.isStatic) {
                            addRoute(route.verb, route.path, method);
                        } else {
                            adapter
                                .getRoutes(method.sharedCtor)
                                .forEach(function (sharedCtorRoute) {
                                    addRoute(route.verb, sharedCtorRoute.path + route.path, method);
                                });
                        }
                    });
                });
            });
    });

    return routes;

    function addRoute(verb, path, method) {
        if (path === '/' || path === '//') {
            path = currentRoot;
        } else {
            path = currentRoot + path;
        }

        if (path[path.length - 1] === '/') {
            path = path.substr(0, path.length - 1);
        }

        // TODO this could be cleaner
        path = path.replace(/\/\//g, '/');

        routes.push({
            verb: verb,
            path: path,
            description: method.description,
            method: method.stringName,
            accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
            returns: (method.returns && method.returns.length) ? method.returns : undefined
        });
    }
};

/*!
 * Compare two routes
 * @param {Object} r1 The first route {route: {verb: 'get', path: '/:id'}, method: ...}
 * @param [Object} r2 The second route route: {verb: 'get', path: '/findOne'}, method: ...}
 * @returns {number} 1: r1 comes after 2, -1: r1 comes before r2, 0: equal
 */
function sortRoutes(r1, r2) {
    var a = r1.route;
    var b = r2.route;

    // Normalize the verbs
    var verb1 = a.verb.toLowerCase();
    var verb2 = b.verb.toLowerCase();

    if (verb1 === 'del') {
        verb1 = 'delete';
    }
    if (verb2 === 'del') {
        verb2 = 'delete';
    }
    // First sort by verb
    if (verb1 > verb2) {
        return -1;
    } else if (verb1 < verb2) {
        return 1;
    }

    // Sort by path part by part using the / delimiter
    // For example '/:id' will become ['', ':id'], '/findOne' will become
    // ['', 'findOne']
    var p1 = a.path.split('/');
    var p2 = b.path.split('/');
    var len = Math.min(p1.length, p2.length);

    // Loop through the parts and decide which path should come first
    for (var i = 0; i < len; i++) {
        // Empty part has lower weight
        if (p1[i] === '' && p2[i] !== '') {
            return 1;
        } else if (p1[i] !== '' && p2[i] === '') {
            return -1;
        }
        // Wildcard has lower weight
        if (p1[i][0] === ':' && p2[i][0] !== ':') {
            return 1;
        } else if (p1[i][0] !== ':' && p2[i][0] === ':') {
            return -1;
        }
        // Now the regular string comparision
        if (p1[i] > p2[i]) {
            return 1;
        } else if (p1[i] < p2[i]) {
            return -1;
        }
    }
    // Both paths have the common parts. The longer one should come before the
    // shorter one
    return p2.length - p1.length;
}

/**
 * Get the path for the given method.
 */

//Rest.prototype.buildRoutes = buildRoutes;
function buildRoutes(obj) {
    var routes = obj.http;

    if (routes && !Array.isArray(routes)) {
        routes = [routes];
    }

    // overidden
    if (routes) {
        // patch missing verbs / routes
        routes.forEach(function (r) {
            r.verb = String(r.verb || 'all').toLowerCase();
            r.path = r.path || ('/' + obj.name);
        });
    } else {
        if (obj.name === 'sharedCtor') {
            routes = [
                {
                    verb: 'all',
                    path: '/prototype'
                }
            ];
        } else {
            // build default route
            routes = [
                {
                    verb: 'all',
                    path: obj.name ? ('/' + obj.name) : ''
                }
            ];
        }
    }

    return routes;
}

function RestClass(sharedClass) {
    if (!(this instanceof RestClass)) return new RestClass(sharedClass);

    hiddenConstProperty(this, 'sharedClass', sharedClass);

    var self = this;
    this.name = sharedClass.name;
    this.routes = buildRoutes(sharedClass);

    this.ctor = sharedClass.sharedCtor && new RestMethod(this, sharedClass.sharedCtor);

    this.methods = sharedClass.methods()
        .filter(function (sm) {
            return !sm.isSharedCtor;
        })
        .map(function (sm) {
            return new RestMethod(self, sm);
        });
}

RestClass.prototype.getPath = function () {
    return this.routes[0].path;
};

function RestMethod(restClass, sharedMethod) {
    hiddenConstProperty(this, 'restClass', restClass);
    hiddenConstProperty(this, 'sharedMethod', sharedMethod);

    // The full name is ClassName.methodName or ClassName.prototype.methodName
    this.fullName = sharedMethod.stringName;
    this.name = this.fullName.split('.').slice(1).join('.');

    this.accepts = sharedMethod.accepts;
    this.returns = sharedMethod.returns;
    this.description = sharedMethod.description;

    var methodRoutes = buildRoutes(sharedMethod);
    if (sharedMethod.isStatic || !restClass.ctor) {
        this.routes = methodRoutes;
    } else {
        var routes = this.routes = [];
        methodRoutes.forEach(function (route) {
            restClass.ctor.routes.forEach(function (ctorRoute) {
                var fullRoute = util._extend({}, route);
                fullRoute.path = joinPaths(ctorRoute.path, route.path);
                routes.push(fullRoute);
            });
        });
    }
}

RestMethod.prototype.isReturningArray = function () {
    return this.returns.length == 1 &&
        this.returns[0].root &&
        getTypeString(this.returns[0].type) === 'array' || false;
};

RestMethod.prototype.acceptsSingleBodyArgument = function () {
    if (this.accepts.length != 1) return false;
    var accepts = this.accepts[0];

    return accepts.http &&
        accepts.http.source == 'body' &&
        getTypeString(accepts.type) == 'object' || false;
};

RestMethod.prototype.getHttpMethod = function () {
    var verb = this.routes[0].verb;
    if (verb == 'all') return 'POST';
    if (verb == 'del') return 'DELETE';
    return verb.toUpperCase();
};

RestMethod.prototype.getPath = function () {
    return this.routes[0].path;
};

RestMethod.prototype.getFullPath = function () {
    return joinPaths(this.restClass.getPath(), this.getPath());
};

function RestContext(req, res, method, sapp) {
    this.req = req;
    this.res = res;
    this.method = method;
    this.sapp = sapp;
}

RestContext.prototype.handle = function (method, cb) {
    var self = this, req = this.req, canceled;

    // future may be null if some sira middleware are async
    var future = this.sapp.rekuest(method).props(req).payload(req.body)
        .send(function (err, result) {
            if (err === 'canceled' || canceled) return debug('request has been canceled - ' + req.url);
            if (err) return cb(err);
            self.result = result;
            cb();
        });

    this.req.on('close', function () {
        canceled = true;
        future && future.cancel();
    });
};

RestContext.prototype.done = function () {
    // send the result back as
    // the requested content type
    var data = this.result;
    var res = this.res;
    var accepts = this.req.accepts(SUPPORTED_TYPES);
    var dataExists = typeof data !== 'undefined';

    if (dataExists) {
        switch (accepts) {
            case 'json':
                res.json(data);
                break;
            case 'application/javascript':
            case 'text/javascript':
                res.jsonp(data);
                break;
            default:
                // not acceptable
                res.send(406);
                break;
        }
    } else {
        res.header('Content-Type', 'application/json');
        res.statusCode = 204;
        res.end();
    }
};

function hiddenConstProperty(where, property, value) {
    Object.defineProperty(where, property, {
        writable: false,
        enumerable: false,
        configurable: false,
        value: value
    });
}

function getTypeString(ctorOrName) {
    if (typeof ctorOrName === 'function')
        ctorOrName = ctorOrName.name;
    if (typeof ctorOrName === 'string') {
        return ctorOrName.toLowerCase();
    } else {
        debug('WARNING: unknown ctorOrName of type %s: %j', typeof ctorOrName, ctorOrName);
        return typeof undefined;
    }
}

function joinPaths(left, right) {
    if (!left) return right;
    if (!right || right == '/') return left;

    var glue = left[left.length - 1] + right[0];
    if (glue == '//')
        return left + right.slice(1);
    else if (glue[0] == '/' || glue[1] == '/')
        return left + right;
    else
        return left + '/' + right;
}

<|endoftext|>


<commit_before>function Text(content, bounds, color, size, position, weight, style) {
	this.bounds = bounds;
	this.content = content;
	this.color = color;
	this.size = size;
	this.weight = weight;
	this.style = style;
	this.position = position;

	this.element = null;
	this.draw();
}

Text.prototype.draw = function () {

	var canvas = document.getElementById('canvas');
	var container = document.createElement('div');
	var textBox = document.createElement('div');

	var tools = document.createElement('div');
	var boldButton = document.createElement('button');
	var italicizeButton = document.createElement('button');
	var fontSizeSelect = document.createElement('select');
	var deleteButton = document.createElement('button');

	var that = this;

	textBox.innerHTML = this.content;

	textBox.style.border = '1px dotted transparent';
	textBox.style.display = 'block';
	textBox.style.position = 'relative';
	textBox.style.cursor = 'move';

	textBox.style.fontSize = this.size;
	textBox.style.fontWeight = this.weight;
	textBox.style.color = this.color;
	//textBox.style.width = this.bounds.width;
	textBox.style.height = this.bounds.height;

	// Bold button setup
	boldButton.innerHTML = "B";
	boldButton.addEventListener('click', function (e) {
		if (that.weight === "bold") {
			that.weight = 'normal';
			textBox.style.fontWeight = 'normal';

		} else if (that.weight === "normal") {
			that.weight = 'bold';
			textBox.style.fontWeight = 'bold';
		}
	});

	// Italics button setup
	italicizeButton.innerHTML = "I";
	italicizeButton.addEventListener('click', function (e) {
		if (that.style === "italic") {
			that.style = 'normal';
			textBox.style.fontStyle = 'normal';

		} else if (that.style === "normal") {
			that.style = 'italic';
			textBox.style.fontStyle = 'italic';
		}
	});

	// Font size select setup
	fontSizeSelect.innerHTML = '<option value="2">2</option><option value="5">5</option><option value="7">7</option><option value="10">10</option>';
	fontSizeSelect.addEventListener('change', function (e) {

		textBox.style.fontSize = this.value + 'em';
	});

	// Delete button setup
	deleteButton.innerHTML = "×";
	deleteButton.addEventListener('click', function (e) {
		canvas.removeChild(container);
	});

	tools.style.display = "none";
	tools.style.position = "absolute";
	tools.style.top = "-23px";
	tools.style.border = "1px solid #ccc";
	tools.appendChild(boldButton);
	tools.appendChild(italicizeButton);
	tools.appendChild(fontSizeSelect);
	tools.appendChild(deleteButton);

	container.className = 'text-object';
	container.style.width= '100%';
	container.style.position = "absolute";
	container.style.top = this.position.top;
	container.style.left = this.position.left;

	container.appendChild(textBox);
	container.appendChild(tools);

	container.addEventListener('mouseover', function () {
		textBox.style.borderColor = "#000";
		tools.style.display = "block";
	});

	container.addEventListener('mouseleave', function () {
		textBox.style.borderColor = "transparent";
		tools.style.display = "none";
	});

	// Editable text
	textBox.addEventListener('dblclick', function (e) {
		e.stopPropagation();
		this.contentEditable = true;
		this.focus();
		this.style.background = "#ffffff";
	});

	textBox.addEventListener('blur', function () {
		var updatedText = textBox.innerText || textBox.textContent;

		that.content = updatedText;
		this.innerHTML = updatedText;
		this.contentEditable = false;
		this.style.background = "transparent";
	});

	// Dragging text box
	container.addEventListener('mousedown', function () {
		window.addEventListener('mousemove', move);
	});

	window.addEventListener('mouseup', function () {
		window.removeEventListener('mousemove', move);
	});

	function move(e) {

		that.position.top = e.clientY;
		that.position.left = e.clientX;

		container.style.top = e.clientY + 'px';
		container.style.left = e.clientX + 'px';
	}

	canvas.appendChild(container);

	this.element = textBox;
};<commit_msg>prevent content from getting out of viewport<commit_after>function Text(content, bounds, color, size, position, weight, style) {
	this.bounds = bounds;
	this.content = content;
	this.color = color;
	this.size = size;
	this.weight = weight;
	this.style = style;
	this.position = position;

	this.element = null;
	this.draw();
}

Text.prototype.draw = function () {

	var canvas = document.getElementById('canvas');
	var container = document.createElement('div');
	var textBox = document.createElement('div');

	var tools = document.createElement('div');
	var boldButton = document.createElement('button');
	var italicizeButton = document.createElement('button');
	var fontSizeSelect = document.createElement('select');
	var deleteButton = document.createElement('button');

	var that = this;

	textBox.innerHTML = this.content;

	textBox.style.border = '1px dotted transparent';
	textBox.style.display = 'block';
	textBox.style.position = 'relative';
	textBox.style.cursor = 'move';

	textBox.style.fontSize = this.size;
	textBox.style.fontWeight = this.weight;
	textBox.style.color = this.color;
	textBox.style.width = this.bounds.width;
	textBox.style.height = this.bounds.height;

	// Bold button setup
	boldButton.innerHTML = "B";
	boldButton.addEventListener('click', function (e) {
		if (that.weight === "bold") {
			that.weight = 'normal';
			textBox.style.fontWeight = 'normal';

		} else if (that.weight === "normal") {
			that.weight = 'bold';
			textBox.style.fontWeight = 'bold';
		}
	});

	// Italics button setup
	italicizeButton.innerHTML = "I";
	italicizeButton.addEventListener('click', function (e) {
		if (that.style === "italic") {
			that.style = 'normal';
			textBox.style.fontStyle = 'normal';

		} else if (that.style === "normal") {
			that.style = 'italic';
			textBox.style.fontStyle = 'italic';
		}
	});

	// Font size select setup
	fontSizeSelect.innerHTML = '<option value="2">2</option><option value="5">5</option><option value="7">7</option><option value="10">10</option>';
	fontSizeSelect.addEventListener('change', function (e) {

		textBox.style.fontSize = this.value + 'em';
	});

	// Delete button setup
	deleteButton.innerHTML = "×";
	deleteButton.addEventListener('click', function (e) {
		canvas.removeChild(container);
	});

	tools.style.display = "none";
	tools.style.position = "absolute";
	tools.style.top = "-23px";
	tools.style.border = "1px solid #ccc";
	tools.appendChild(boldButton);
	tools.appendChild(italicizeButton);
	tools.appendChild(fontSizeSelect);
	tools.appendChild(deleteButton);

	container.className = 'text-object';
	container.style.width= '100%';
	container.style.position = "absolute";
	container.style.top = this.position.top;
	container.style.left = this.position.left;

	container.appendChild(textBox);
	container.appendChild(tools);

	container.addEventListener('mouseover', function () {
		textBox.style.borderColor = "#000";
		tools.style.display = "block";
	});

	container.addEventListener('mouseleave', function () {
		textBox.style.borderColor = "transparent";
		tools.style.display = "none";
	});

	// Editable text
	textBox.addEventListener('dblclick', function (e) {
		e.stopPropagation();
		this.contentEditable = true;
		this.focus();
		this.style.background = "#ffffff";
	});

	textBox.addEventListener('blur', function () {
		var updatedText = textBox.innerText || textBox.textContent;

		that.content = updatedText;
		this.innerHTML = updatedText;
		this.contentEditable = false;
		this.style.background = "transparent";
	});

	// Dragging text box
	container.addEventListener('mousedown', function () {
		window.addEventListener('mousemove', move);
	});

	window.addEventListener('mouseup', function () {
		window.removeEventListener('mousemove', move);
	});

	function move(e) {

		that.position.top = e.clientY;
		that.position.left = e.clientX;

		container.style.top = e.clientY + 'px';
		container.style.left = e.clientX + 'px';
	}

	canvas.appendChild(container);

	this.element = textBox;
};<|endoftext|>


<commit_before>/**
 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or http://ckeditor.com/license
 */

CKEDITOR.editorConfig = function( config ) {
    // Define changes to default configuration here.
    // For complete reference see:
    // http://docs.ckeditor.com/#!/api/CKEDITOR.config

    config.toolbar = [
        { name: 'document', items: [ 'Source' ] },
        { name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ] },
        { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ], items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'BidiLtr', 'BidiRtl' ] },
        { name: 'links', items: [ 'Link', 'Unlink', 'Anchor' ] },
        { name: 'insert', items: [ 'Image', 'Table', 'HorizontalRule', 'Iframe' ] },
        { name: 'styles', items: [ 'Format' ] },
        { name: 'tools', items: [ 'Maximize', 'ShowBlocks' ] }
    ];

    // The toolbar groups arrangement, optimized for a single toolbar row.
    config.toolbarGroups = [
        { name: 'document',	   groups: [ 'mode', 'document', 'doctools' ] },
        { name: 'clipboard',   groups: [ 'clipboard', 'undo' ] },
        { name: 'editing',     groups: [ 'find', 'selection', 'spellchecker' ] },
        { name: 'forms' },
        { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
        { name: 'paragraph',   groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] },
        { name: 'links' },
        { name: 'insert' },
        { name: 'styles' },
        { name: 'colors' },
        { name: 'tools' },
        { name: 'others' },
        { name: 'about' }
    ];

    // The default plugins included in the basic setup define some buttons that
    // are not needed in a basic editor. They are removed here.
    config.removeButtons = 'Cut,Copy,Paste,Undo,Redo,Anchor,Underline,Strike,Subscript,Superscript';

    // Dialog windows are also simplified.
    config.removeDialogTabs = 'link:advanced';

    config.enterMode = 'CKEDITOR.ENTER_P';

    config.startupOutlineBlocks = true;
};<commit_msg>added advanced tab to links on ckeditor<commit_after>/**
 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or http://ckeditor.com/license
 */

CKEDITOR.editorConfig = function( config ) {
    // Define changes to default configuration here.
    // For complete reference see:
    // http://docs.ckeditor.com/#!/api/CKEDITOR.config

    config.toolbar = [
        { name: 'document', items: [ 'Source' ] },
        { name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ] },
        { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ], items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'BidiLtr', 'BidiRtl' ] },
        { name: 'links', items: [ 'Link', 'Unlink', 'Anchor' ] },
        { name: 'insert', items: [ 'Image', 'Table', 'HorizontalRule', 'Iframe' ] },
        { name: 'styles', items: [ 'Format' ] },
        { name: 'tools', items: [ 'Maximize', 'ShowBlocks' ] }
    ];

    // The toolbar groups arrangement, optimized for a single toolbar row.
    config.toolbarGroups = [
        { name: 'document',	   groups: [ 'mode', 'document', 'doctools' ] },
        { name: 'clipboard',   groups: [ 'clipboard', 'undo' ] },
        { name: 'editing',     groups: [ 'find', 'selection', 'spellchecker' ] },
        { name: 'forms' },
        { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
        { name: 'paragraph',   groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] },
        { name: 'links' },
        { name: 'insert' },
        { name: 'styles' },
        { name: 'colors' },
        { name: 'tools' },
        { name: 'others' },
        { name: 'about' }
    ];

    // The default plugins included in the basic setup define some buttons that
    // are not needed in a basic editor. They are removed here.
    config.removeButtons = 'Cut,Copy,Paste,Undo,Redo,Anchor,Underline,Strike,Subscript,Superscript';

    // Dialog windows are also simplified.
    //config.removeDialogTabs = 'link:advanced';

    config.enterMode = 'CKEDITOR.ENTER_P';

    config.startupOutlineBlocks = true;
};<|endoftext|>