weechatRN

Weechat relay client for iOS using websockets https://github.com/mhoran/weechatRN
git clone http://git.hanabi.in/repos/weechatRN.git
Log | Files | Refs | README | LICENSE

parser.js (31288B)


      1 // http://weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings
      2 'use strict';
      3 
      4 /**
      5  * WeeChat protocol handling.
      6  *
      7  * This object parses messages and formats commands for the WeeChat
      8  * protocol. It's independent from the communication layer and thus
      9  * may be used with any network mechanism.
     10  */
     11 export const WeeChatProtocol = function () {
     12   // specific parsing for each object type
     13   this._types = {
     14     chr: this._getChar,
     15     int: this._getInt,
     16     str: this._getString,
     17     inf: this._getInfo,
     18     hda: this._getHdata,
     19     ptr: this._getPointer,
     20     lon: this._getStrNumber,
     21     tim: this._getTime,
     22     buf: this._getString,
     23     arr: this._getArray,
     24     htb: this._getHashTable,
     25     inl: this._getInfolist
     26   };
     27 
     28   // string value for some object types
     29   this._typesStr = {
     30     chr: this._strDirect,
     31     str: this._strDirect,
     32     int: this._strToString,
     33     tim: this._strToString,
     34     ptr: this._strDirect
     35   };
     36 };
     37 
     38 /**
     39  * WeeChat colors names.
     40  */
     41 WeeChatProtocol._weeChatColorsNames = [
     42   'default',
     43   'black',
     44   'darkgray',
     45   'red',
     46   'lightred',
     47   'green',
     48   'lightgreen',
     49   'brown',
     50   'yellow',
     51   'blue',
     52   'lightblue',
     53   'magenta',
     54   'lightmagenta',
     55   'cyan',
     56   'lightcyan',
     57   'gray',
     58   'white'
     59 ];
     60 
     61 /**
     62  * Style options names.
     63  */
     64 WeeChatProtocol._colorsOptionsNames = [
     65   'separator',
     66   'chat',
     67   'chat_time',
     68   'chat_time_delimiters',
     69   'chat_prefix_error',
     70   'chat_prefix_network',
     71   'chat_prefix_action',
     72   'chat_prefix_join',
     73   'chat_prefix_quit',
     74   'chat_prefix_more',
     75   'chat_prefix_suffix',
     76   'chat_buffer',
     77   'chat_server',
     78   'chat_channel',
     79   'chat_nick',
     80   'chat_nick_self',
     81   'chat_nick_other',
     82   'invalid',
     83   'invalid',
     84   'invalid',
     85   'invalid',
     86   'invalid',
     87   'invalid',
     88   'invalid',
     89   'invalid',
     90   'invalid',
     91   'invalid',
     92   'chat_host',
     93   'chat_delimiters',
     94   'chat_highlight',
     95   'chat_read_marker',
     96   'chat_text_found',
     97   'chat_value',
     98   'chat_prefix_buffer',
     99   'chat_tags',
    100   'chat_inactive_window',
    101   'chat_inactive_buffer',
    102   'chat_prefix_buffer_inactive_buffer',
    103   'chat_nick_offline',
    104   'chat_nick_offline_highlight',
    105   'chat_nick_prefix',
    106   'chat_nick_suffix',
    107   'emphasis',
    108   'chat_day_change'
    109 ];
    110 
    111 /**
    112  * Gets the default color.
    113  *
    114  * @return Default color
    115  */
    116 WeeChatProtocol._getDefaultColor = function () {
    117   return {
    118     type: 'weechat',
    119     name: 'default'
    120   };
    121 };
    122 
    123 /**
    124  * Gets the default attributes.
    125  *
    126  * @return Default attributes
    127  */
    128 WeeChatProtocol._getDefaultAttributes = function () {
    129   return {
    130     name: null,
    131     override: {
    132       bold: false,
    133       reverse: false,
    134       italic: false,
    135       underline: false
    136     }
    137   };
    138 };
    139 
    140 /**
    141  * Gets the default style (default colors and attributes).
    142  *
    143  * @return Default style
    144  */
    145 WeeChatProtocol._getDefaultStyle = function () {
    146   return {
    147     fgColor: WeeChatProtocol._getDefaultColor(),
    148     bgColor: WeeChatProtocol._getDefaultColor(),
    149     attrs: WeeChatProtocol._getDefaultAttributes()
    150   };
    151 };
    152 
    153 /**
    154  * Clones a color object.
    155  *
    156  * @param color Color object to clone
    157  * @return Cloned color object
    158  */
    159 WeeChatProtocol._cloneColor = function (color) {
    160   var clone = {};
    161 
    162   for (var key in color) {
    163     clone[key] = color[key];
    164   }
    165 
    166   return clone;
    167 };
    168 
    169 /**
    170  * Clones an attributes object.
    171  *
    172  * @param attrs Attributes object to clone
    173  * @return Cloned attributes object
    174  */
    175 WeeChatProtocol._cloneAttrs = function (attrs) {
    176   var clone = {};
    177 
    178   clone.name = attrs.name;
    179   clone.override = {};
    180   for (var attr in attrs.override) {
    181     clone.override[attr] = attrs.override[attr];
    182   }
    183 
    184   return clone;
    185 };
    186 
    187 /**
    188  * Gets the name of an attribute from its character.
    189  *
    190  * @param ch Character of attribute
    191  * @return Name of attribute
    192  */
    193 WeeChatProtocol._attrNameFromChar = function (ch) {
    194   var chars = {
    195     // WeeChat protocol
    196     '*': 'b',
    197     '!': 'r',
    198     '/': 'i',
    199     _: 'u',
    200 
    201     // some extension often used (IRC?)
    202     '\x01': 'b',
    203     '\x02': 'r',
    204     '\x03': 'i',
    205     '\x04': 'u'
    206   };
    207 
    208   if (ch in chars) {
    209     return chars[ch];
    210   }
    211 
    212   return null;
    213 };
    214 
    215 /**
    216  * Gets an attributes object from a string of attribute characters.
    217  *
    218  * @param str String of attribute characters
    219  * @return Attributes object (null if unchanged)
    220  */
    221 WeeChatProtocol._attrsFromStr = function (str) {
    222   var attrs = WeeChatProtocol._getDefaultAttributes();
    223 
    224   for (var i = 0; i < str.length; ++i) {
    225     var ch = str.charAt(i);
    226     if (ch === '|') {
    227       // means keep attributes, so unchanged
    228       return null;
    229     }
    230     var attrName = WeeChatProtocol._attrNameFromChar(ch);
    231     if (attrName !== null) {
    232       attrs.override[attrName] = true;
    233     }
    234   }
    235 
    236   return attrs;
    237 };
    238 
    239 /**
    240  * Gets a single color from a string representing its index (WeeChat and
    241  * extended colors only, NOT colors options).
    242  *
    243  * @param str Color string (e.g., "05" or "00134")
    244  * @return Color object
    245  */
    246 WeeChatProtocol._getColorObj = function (str) {
    247   if (str.length === 2) {
    248     var code = parseInt(str);
    249     if (code > 16) {
    250       // should never happen
    251       return WeeChatProtocol._getDefaultColor();
    252     } else {
    253       return {
    254         type: 'weechat',
    255         name: WeeChatProtocol._weeChatColorsNames[code]
    256       };
    257     }
    258   } else {
    259     var codeStr = str.substring(1);
    260     return {
    261       type: 'ext',
    262       name: parseInt(codeStr).toString()
    263     };
    264   }
    265 };
    266 
    267 /**
    268  * Gets colors and attributes of text element.
    269  *
    270  * See <http://www.weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings>.
    271  *
    272  * @param txt Text element
    273  * @return Colors, attributes and plain text of this text element:
    274  *          fgColor: Foreground color (null if unchanged)
    275  *          bgColor: Background color (null if unchanged)
    276  *          attrs: Attributes (null if unchanged)
    277  *          text: Plain text element
    278  */
    279 WeeChatProtocol._getStyle = function (txt) {
    280   var matchers = [
    281     {
    282       // color option
    283       //   STD
    284       regex: /^(\d{2})/,
    285       fn: function (m) {
    286         var ret = {};
    287         var optionCode = parseInt(m[1]);
    288 
    289         if (optionCode >= WeeChatProtocol._colorsOptionsNames.length) {
    290           // should never happen
    291           return {
    292             fgColor: null,
    293             bgColor: null,
    294             attrs: null
    295           };
    296         }
    297         var optionName = WeeChatProtocol._colorsOptionsNames[optionCode];
    298         ret.fgColor = {
    299           type: 'option',
    300           name: optionName
    301         };
    302         ret.bgColor = WeeChatProtocol._cloneColor(ret.fgColor);
    303         ret.attrs = {
    304           name: optionName,
    305           override: {}
    306         };
    307 
    308         return ret;
    309       }
    310     },
    311     {
    312       // ncurses pair
    313       //   EXT
    314       regex: /^@(\d{5})/,
    315       fn: function (m) {
    316         // unimplemented case
    317         return {
    318           fgColor: null,
    319           bgColor: null,
    320           attrs: null
    321         };
    322       }
    323     },
    324     {
    325       // foreground color with F
    326       //   "F" + (A)STD
    327       //   "F" + (A)EXT
    328       regex: /^F(?:([*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5}))/,
    329       fn: function (m) {
    330         var ret = {
    331           bgColor: null
    332         };
    333 
    334         if (m[2]) {
    335           ret.attrs = WeeChatProtocol._attrsFromStr(m[1]);
    336           ret.fgColor = WeeChatProtocol._getColorObj(m[2]);
    337         } else {
    338           ret.attrs = WeeChatProtocol._attrsFromStr(m[3]);
    339           ret.fgColor = WeeChatProtocol._getColorObj(m[4]);
    340         }
    341 
    342         return ret;
    343       }
    344     },
    345     {
    346       // background color (no attributes)
    347       //   "B" + STD
    348       //   "B" + EXT
    349       regex: /^B(\d{2}|@\d{5})/,
    350       fn: function (m) {
    351         return {
    352           fgColor: null,
    353           bgColor: WeeChatProtocol._getColorObj(m[1]),
    354           attrs: null
    355         };
    356       }
    357     },
    358     {
    359       // foreground, background (+ attributes)
    360       //   "*" + (A)STD + "," + STD
    361       //   "*" + (A)STD + "," + EXT
    362       //   "*" + (A)EXT + "," + STD
    363       //   "*" + (A)EXT + "," + EXT
    364       // WeeChat 2.6+ use a tilde (~) instead of a comma (,) so recognise both
    365       regex: /^\*(?:([\x01\x02\x03\x04*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5}))[,~](\d{2}|@\d{5})/,
    366       fn: function (m) {
    367         var ret = {};
    368 
    369         if (m[2]) {
    370           ret.attrs = WeeChatProtocol._attrsFromStr(m[1]);
    371           ret.fgColor = WeeChatProtocol._getColorObj(m[2]);
    372         } else {
    373           ret.attrs = WeeChatProtocol._attrsFromStr(m[3]);
    374           ret.fgColor = WeeChatProtocol._getColorObj(m[4]);
    375         }
    376         ret.bgColor = WeeChatProtocol._getColorObj(m[5]);
    377 
    378         return ret;
    379       }
    380     },
    381     {
    382       // foreground color with * (+ attributes) (fall back, must be checked before previous case)
    383       //   "*" + (A)STD
    384       //   "*" + (A)EXT
    385       regex: /^\*([\x01\x02\x03\x04*!\/_|]*)(\d{2}|@\d{5})/,
    386       fn: function (m) {
    387         return {
    388           fgColor: WeeChatProtocol._getColorObj(m[2]),
    389           bgColor: null,
    390           attrs: WeeChatProtocol._attrsFromStr(m[1])
    391         };
    392       }
    393     },
    394     {
    395       // emphasis
    396       //   "E"
    397       regex: /^E/,
    398       fn: function (m) {
    399         var ret = {};
    400 
    401         ret.fgColor = {
    402           type: 'option',
    403           name: 'emphasis'
    404         };
    405         ret.bgColor = WeeChatProtocol._cloneColor(ret.fgColor);
    406         ret.attrs = {
    407           name: 'emphasis',
    408           override: {}
    409         };
    410 
    411         return ret;
    412       }
    413     }
    414   ];
    415 
    416   // parse
    417   var ret = {
    418     fgColor: null,
    419     bgColor: null,
    420     attrs: null,
    421     text: txt
    422   };
    423   matchers.some(function (matcher) {
    424     var m = txt.match(matcher.regex);
    425     if (m) {
    426       ret = matcher.fn(m);
    427       ret.text = txt.substring(m[0].length);
    428       return true;
    429     }
    430 
    431     return false;
    432   });
    433 
    434   return ret;
    435 };
    436 
    437 /**
    438  * Transforms a raw text into an array of text elements with integrated
    439  * colors and attributes.
    440  *
    441  * @param rawText Raw text to transform
    442  * @return Array of text elements
    443  */
    444 WeeChatProtocol.rawText2Rich = function (rawText) {
    445   /* This is subtle, but JavaScript adds the token to the output list
    446    * when it's surrounded by capturing parentheses.
    447    */
    448   var parts = rawText.split(/(\x19|\x1a|\x1b|\x1c)/);
    449 
    450   // no colors/attributes
    451   if (parts.length === 1) {
    452     return [
    453       {
    454         attrs: WeeChatProtocol._getDefaultAttributes(),
    455         fgColor: WeeChatProtocol._getDefaultColor(),
    456         bgColor: WeeChatProtocol._getDefaultColor(),
    457         text: parts[0]
    458       }
    459     ];
    460   }
    461 
    462   // find the style of every part
    463   var curFgColor = WeeChatProtocol._getDefaultColor();
    464   var curBgColor = WeeChatProtocol._getDefaultColor();
    465   var curAttrs = WeeChatProtocol._getDefaultAttributes();
    466   var curSpecialToken = null;
    467   var curAttrsOnlyFalseOverrides = true;
    468 
    469   return parts
    470     .map(function (p) {
    471       if (p.length === 0) {
    472         return null;
    473       }
    474       var firstCharCode = p.charCodeAt(0);
    475       var firstChar = p.charAt(0);
    476 
    477       if (firstCharCode >= 0x19 && firstCharCode <= 0x1c) {
    478         // special token
    479         if (firstCharCode === 0x1c) {
    480           // always reset colors
    481           curFgColor = WeeChatProtocol._getDefaultColor();
    482           curBgColor = WeeChatProtocol._getDefaultColor();
    483           if (curSpecialToken !== 0x19) {
    484             // also reset attributes
    485             curAttrs = WeeChatProtocol._getDefaultAttributes();
    486           }
    487         }
    488         curSpecialToken = firstCharCode;
    489         return null;
    490       }
    491 
    492       var text = p;
    493       if (curSpecialToken === 0x19) {
    494         // get new style
    495         var style = WeeChatProtocol._getStyle(p);
    496 
    497         // set foreground color if changed
    498         if (style.fgColor !== null) {
    499           curFgColor = style.fgColor;
    500         }
    501 
    502         // set background color if changed
    503         if (style.bgColor !== null) {
    504           curBgColor = style.bgColor;
    505         }
    506 
    507         // set attibutes if changed
    508         if (style.attrs !== null) {
    509           curAttrs = style.attrs;
    510         }
    511 
    512         // set plain text
    513         text = style.text;
    514       } else if (curSpecialToken === 0x1a || curSpecialToken === 0x1b) {
    515         // set/reset attribute
    516         var orideVal = curSpecialToken === 0x1a;
    517 
    518         // set attribute override if we don't have to keep all of them
    519         if (firstChar !== '|') {
    520           var orideName = WeeChatProtocol._attrNameFromChar(firstChar);
    521           if (orideName) {
    522             // known attribute
    523             curAttrs.override[orideName] = orideVal;
    524             text = p.substring(1);
    525           }
    526         }
    527       }
    528 
    529       // reset current special token
    530       curSpecialToken = null;
    531 
    532       // if text is empty, don't bother returning it
    533       if (text.length === 0) {
    534         return null;
    535       }
    536 
    537       /* As long as attributes are only false overrides, without any option
    538        * name, it's safe to remove them.
    539        */
    540       if (curAttrsOnlyFalseOverrides && curAttrs.name === null) {
    541         var allReset = true;
    542         for (var attr in curAttrs.override) {
    543           if (curAttrs.override[attr]) {
    544             allReset = false;
    545             break;
    546           }
    547         }
    548         if (allReset) {
    549           curAttrs.override = {};
    550         } else {
    551           curAttrsOnlyFalseOverrides = false;
    552         }
    553       }
    554 
    555       // parsed text element
    556       return {
    557         fgColor: WeeChatProtocol._cloneColor(curFgColor),
    558         bgColor: WeeChatProtocol._cloneColor(curBgColor),
    559         attrs: WeeChatProtocol._cloneAttrs(curAttrs),
    560         text: text
    561       };
    562     })
    563     .filter(function (p) {
    564       return p !== null;
    565     });
    566 };
    567 
    568 /**
    569  * Unsigned integer array to string.
    570  *
    571  * @param uia Unsigned integer array
    572  * @return Decoded string
    573  */
    574 WeeChatProtocol._uia2s = function (uia) {
    575   if (!uia.length || uia[0] === 0) return '';
    576 
    577   try {
    578     var encodedString = String.fromCharCode.apply(null, uia),
    579       decodedString = decodeURIComponent(escape(encodedString));
    580     return decodedString;
    581   } catch (exception) {
    582     // Replace all non-ASCII bytes with "?" if the string couldn't be
    583     // decoded as UTF-8.
    584     var s = '';
    585     for (var i = 0, n = uia.length; i < n; i++) {
    586       s += uia[i] < 0x80 ? String.fromCharCode(uia[i]) : '?';
    587     }
    588     return s;
    589   }
    590 };
    591 
    592 /**
    593  * Merges default parameters with overriding parameters.
    594  *
    595  * @param defaults Default parameters
    596  * @param override Overriding parameters
    597  * @return Merged parameters
    598  */
    599 WeeChatProtocol._mergeParams = function (defaults, override) {
    600   for (var v in override) {
    601     defaults[v] = override[v];
    602   }
    603 
    604   return defaults;
    605 };
    606 
    607 /**
    608  * Formats a command.
    609  *
    610  * @param id Command ID (null for no ID)
    611  * @param name Command name
    612  * @param parts Command parts
    613  * @return Formatted command string
    614  */
    615 WeeChatProtocol._formatCmd = function (id, name, parts) {
    616   var cmdIdName;
    617   var cmd;
    618 
    619   cmdIdName = id !== null ? '(' + id + ') ' : '';
    620   cmdIdName += name;
    621   parts.unshift(cmdIdName);
    622   cmd = parts.join(' ');
    623   cmd += '\n';
    624 
    625   cmd.replace(/[\r\n]+$/g, '').split('\n');
    626 
    627   return cmd;
    628 };
    629 
    630 /**
    631  * Formats a handshake command.
    632  *
    633  * @param params Parameters:
    634  *            password: list of supported hash algorithms, colon separated (optional)
    635  *            compression: compression ('off' or 'zlib') (optional)
    636  * @return Formatted handshake command string
    637  */
    638 //https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_handshake
    639 WeeChatProtocol.formatHandshake = function (params) {
    640   var defaultParams = {
    641     password_hash_algo: 'pbkdf2+sha512',
    642     compression: 'zlib'
    643   };
    644   var keys = [];
    645   var parts = [];
    646 
    647   params = WeeChatProtocol._mergeParams(defaultParams, params);
    648 
    649   if (params.compression !== null) {
    650     keys.push('compression=' + params.compression);
    651   }
    652 
    653   if (params.password_hash_algo !== null) {
    654     keys.push('password_hash_algo=' + params.password_hash_algo);
    655   }
    656 
    657   parts.push(keys.join(','));
    658 
    659   return WeeChatProtocol._formatCmd(null, 'handshake', parts);
    660 };
    661 
    662 /**
    663  * Formats an init command for weechat versions < 2.9
    664  *
    665  * @param params Parameters:
    666  *            password: password (optional)
    667  *            compression: compression ('off' or 'zlib') (optional)
    668  *            totp: One Time Password (optional)
    669  * @return Formatted init command string
    670  */
    671 WeeChatProtocol.formatInitPre29 = function (params) {
    672   var defaultParams = {
    673     password: null,
    674     compression: 'zlib',
    675     totp: null
    676   };
    677   var keys = [];
    678   var parts = [];
    679 
    680   params = WeeChatProtocol._mergeParams(defaultParams, params);
    681   keys.push('compression=' + params.compression);
    682   if (params.password !== null) {
    683     keys.push('password=' + params.password);
    684   }
    685   if (params.totp !== null) {
    686     keys.push('totp=' + params.totp);
    687   }
    688   parts.push(keys.join(','));
    689 
    690   return WeeChatProtocol._formatCmd(null, 'init', parts);
    691 };
    692 
    693 /**
    694  * Formats an init command for weechat versions >= 2.9
    695  *
    696  * @param params Parameters:
    697  *            password_hash: hash of password with method and salt
    698  *            totp: One Time Password (can be null)
    699  * @return Formatted init command string
    700  */
    701 WeeChatProtocol.formatInit29 = function (password_hash, totp) {
    702   var keys = [];
    703   var parts = [];
    704 
    705   if (totp != null) {
    706     keys.push('totp=' + totp);
    707   }
    708   if (password_hash !== null) {
    709     keys.push('password_hash=' + password_hash);
    710   }
    711   parts.push(keys.join(','));
    712 
    713   return WeeChatProtocol._formatCmd(null, 'init', parts);
    714 };
    715 
    716 /**
    717  * Formats an hdata command.
    718  *
    719  * @param params Parameters:
    720  *            id: command ID (optional)
    721  *            path: hdata path (mandatory)
    722  *            keys: array of keys (optional)
    723  * @return Formatted hdata command string
    724  */
    725 WeeChatProtocol.formatHdata = function (params) {
    726   var defaultParams = {
    727     id: null,
    728     keys: null
    729   };
    730   var parts = [];
    731 
    732   params = WeeChatProtocol._mergeParams(defaultParams, params);
    733   parts.push(params.path);
    734   if (params.keys !== null) {
    735     parts.push(params.keys.join(','));
    736   }
    737 
    738   return WeeChatProtocol._formatCmd(params.id, 'hdata', parts);
    739 };
    740 
    741 /**
    742  * Formats an info command.
    743  *
    744  * @param params Parameters:
    745  *            id: command ID (optional)
    746  *            name: info name (mandatory)
    747  * @return Formatted info command string
    748  */
    749 WeeChatProtocol.formatInfo = function (params) {
    750   var defaultParams = {
    751     id: null
    752   };
    753   var parts = [];
    754 
    755   params = WeeChatProtocol._mergeParams(defaultParams, params);
    756   parts.push(params.name);
    757 
    758   return WeeChatProtocol._formatCmd(params.id, 'info', parts);
    759 };
    760 
    761 /**
    762  * Formats an infolist command.
    763  *
    764  * @param params Parameters:
    765  *            id: command ID (optional)
    766  *            name: infolist name (mandatory)
    767  *            pointer: optional
    768  *            arguments: optional
    769  * @return Formatted infolist command string
    770  */
    771 WeeChatProtocol.formatInfolist = function (params) {
    772   var defaultParams = {
    773     id: null,
    774     pointer: null,
    775     args: null
    776   };
    777   var parts = [];
    778 
    779   params = WeeChatProtocol._mergeParams(defaultParams, params);
    780   parts.push(params.name);
    781   if (params.pointer !== null) {
    782     parts.push(params.pointer);
    783   }
    784   if (params.pointer !== null) {
    785     parts.push(params.args);
    786   }
    787 
    788   return WeeChatProtocol._formatCmd(params.id, 'infolist', parts);
    789 };
    790 
    791 /**
    792  * Formats a nicklist command.
    793  *
    794  * @param params Parameters:
    795  *            id: command ID (optional)
    796  *            buffer: buffer name (optional)
    797  * @return Formatted nicklist command string
    798  */
    799 WeeChatProtocol.formatNicklist = function (params) {
    800   var defaultParams = {
    801     id: null,
    802     buffer: null
    803   };
    804   var parts = [];
    805 
    806   params = WeeChatProtocol._mergeParams(defaultParams, params);
    807   if (params.buffer !== null) {
    808     parts.push(params.buffer);
    809   }
    810 
    811   return WeeChatProtocol._formatCmd(params.id, 'nicklist', parts);
    812 };
    813 
    814 /**
    815  * Formats an input command.
    816  *
    817  * @param params Parameters:
    818  *            id: command ID (optional)
    819  *            buffer: target buffer (mandatory)
    820  *            data: input data (mandatory)
    821  * @return Formatted input command string
    822  */
    823 WeeChatProtocol.formatInput = function (params) {
    824   var defaultParams = {
    825     id: null
    826   };
    827   var parts = [];
    828 
    829   params = WeeChatProtocol._mergeParams(defaultParams, params);
    830   parts.push(params.buffer);
    831   parts.push(params.data);
    832 
    833   return WeeChatProtocol._formatCmd(params.id, 'input', parts);
    834 };
    835 
    836 /**
    837  * Formats a completion command.
    838  * https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_completion
    839  * @param params Parameters:
    840  *            id: command ID (optional)
    841  *            buffer: target buffer (mandatory)
    842  *            position: position for completion in string (optional)
    843  *            data: input data (optional)
    844  * @return Formatted input command string
    845  */
    846 WeeChatProtocol.formatCompletion = function (params) {
    847   var defaultParams = {
    848     id: null,
    849     position: -1
    850   };
    851   var parts = [];
    852 
    853   params = WeeChatProtocol._mergeParams(defaultParams, params);
    854   parts.push(params.buffer);
    855   parts.push(params.position);
    856   if (params.data) {
    857     parts.push(params.data);
    858   }
    859 
    860   return WeeChatProtocol._formatCmd(params.id, 'completion', parts);
    861 };
    862 
    863 /**
    864  * Formats a sync or a desync command.
    865  *
    866  * @param params Parameters (see _formatSync and _formatDesync)
    867  * @return Formatted sync/desync command string
    868  */
    869 WeeChatProtocol._formatSyncDesync = function (cmdName, params) {
    870   var defaultParams = {
    871     id: null,
    872     buffers: null,
    873     options: null
    874   };
    875   var parts = [];
    876 
    877   params = WeeChatProtocol._mergeParams(defaultParams, params);
    878   if (params.buffers !== null) {
    879     parts.push(params.buffers.join(','));
    880     if (params.options !== null) {
    881       parts.push(params.options.join(','));
    882     }
    883   }
    884 
    885   return WeeChatProtocol._formatCmd(params.id, cmdName, parts);
    886 };
    887 
    888 /**
    889  * Formats a sync command.
    890  *
    891  * @param params Parameters:
    892  *            id: command ID (optional)
    893  *            buffers: array of buffers to sync (optional)
    894  *            options: array of options (optional)
    895  * @return Formatted sync command string
    896  */
    897 WeeChatProtocol.formatSync = function (params) {
    898   return WeeChatProtocol._formatSyncDesync('sync', params);
    899 };
    900 
    901 /**
    902  * Formats a desync command.
    903  *
    904  * @param params Parameters:
    905  *            id: command ID (optional)
    906  *            buffers: array of buffers to desync (optional)
    907  *            options: array of options (optional)
    908  * @return Formatted desync command string
    909  */
    910 WeeChatProtocol.formatDesync = function (params) {
    911   return WeeChatProtocol._formatSyncDesync('desync', params);
    912 };
    913 
    914 /**
    915  * Formats a test command.
    916  *
    917  * @param params Parameters:
    918  *            id: command ID (optional)
    919  * @return Formatted test command string
    920  */
    921 WeeChatProtocol.formatTest = function (params) {
    922   var defaultParams = {
    923     id: null
    924   };
    925   var parts = [];
    926 
    927   params = WeeChatProtocol._mergeParams(defaultParams, params);
    928 
    929   return WeeChatProtocol._formatCmd(params.id, 'test', parts);
    930 };
    931 
    932 /**
    933  * Formats a quit command.
    934  *
    935  * @return Formatted quit command string
    936  */
    937 WeeChatProtocol.formatQuit = function () {
    938   return WeeChatProtocol._formatCmd(null, 'quit', []);
    939 };
    940 
    941 /**
    942  * Formats a ping command.
    943  *
    944  * @param params Parameters:
    945  *            id: command ID (optional)
    946  *            args: array of custom arguments (optional)
    947  * @return Formatted ping command string
    948  */
    949 WeeChatProtocol.formatPing = function (params) {
    950   var defaultParams = {
    951     id: null,
    952     args: null
    953   };
    954   var parts = [];
    955 
    956   params = WeeChatProtocol._mergeParams(defaultParams, params);
    957   if (params.args !== null) {
    958     parts.push(params.args.join(' '));
    959   }
    960 
    961   return WeeChatProtocol._formatCmd(params.id, 'ping', parts);
    962 };
    963 
    964 WeeChatProtocol.prototype = {
    965   /**
    966    * Warns that message parsing is not implemented for a
    967    * specific type.
    968    *
    969    * @param type Message type to display
    970    */
    971   _warnUnimplemented: function (type) {
    972     console.log('Warning: ' + type + ' message parsing is not implemented');
    973   },
    974 
    975   /**
    976    * Reads a 3-character message type token value from current
    977    * set data.
    978    *
    979    * @return Type
    980    */
    981   _getType: function () {
    982     var t = this._getSlice(3);
    983 
    984     if (!t) {
    985       return null;
    986     }
    987 
    988     return WeeChatProtocol._uia2s(new Uint8Array(t));
    989   },
    990 
    991   /**
    992    * Runs the appropriate read routine for the specified message type.
    993    *
    994    * @param type Message type
    995    * @return Data value
    996    */
    997   _runType: function (type) {
    998     var cb = this._types[type];
    999     var boundCb = cb.bind(this);
   1000 
   1001     return boundCb();
   1002   },
   1003 
   1004   /**
   1005    * Reads a "number as a string" token value from current set data.
   1006    *
   1007    * @return Number as a string
   1008    */
   1009   _getStrNumber: function () {
   1010     var len = this._getByte();
   1011     var str = this._getSlice(len);
   1012 
   1013     return WeeChatProtocol._uia2s(new Uint8Array(str));
   1014   },
   1015 
   1016   /**
   1017    * Returns the passed object.
   1018    *
   1019    * @param obj Object
   1020    * @return Passed object
   1021    */
   1022   _strDirect: function (obj) {
   1023     return obj;
   1024   },
   1025 
   1026   /**
   1027    * Calls toString() on the passed object and returns the value.
   1028    *
   1029    * @param obj Object to call toString() on
   1030    * @return String value of object
   1031    */
   1032   _strToString: function (obj) {
   1033     return obj.toString();
   1034   },
   1035 
   1036   /**
   1037    * Gets the string value of an object representing the message
   1038    * value for a specified type.
   1039    *
   1040    * @param obj Object for which to get the string value
   1041    * @param type Message type
   1042    * @return String value of object
   1043    */
   1044   _objToString: function (obj, type) {
   1045     var cb = this._typesStr[type];
   1046     var boundCb = cb.bind(this);
   1047 
   1048     return boundCb(obj);
   1049   },
   1050 
   1051   /**
   1052    * Reads an info token value from current set data.
   1053    *
   1054    * @return Info object
   1055    */
   1056   _getInfo: function () {
   1057     var info = {};
   1058     info.key = this._getString();
   1059     info.value = this._getString();
   1060 
   1061     return info;
   1062   },
   1063 
   1064   /**
   1065    * Reads an hdata token value from current set data.
   1066    *
   1067    * @return Hdata object
   1068    */
   1069   _getHdata: function () {
   1070     var self = this;
   1071     var paths;
   1072     var count;
   1073     var objs = [];
   1074     var hpath = this._getString();
   1075 
   1076     var keys = this._getString().split(',');
   1077     paths = hpath.split('/');
   1078     count = this._getInt();
   1079 
   1080     keys = keys.map(function (key) {
   1081       return key.split(':');
   1082     });
   1083 
   1084     function runType() {
   1085       var tmp = {};
   1086 
   1087       tmp.pointers = paths.map(function (path) {
   1088         return self._getPointer();
   1089       });
   1090       keys.forEach(function (key) {
   1091         tmp[key[0]] = self._runType(key[1]);
   1092       });
   1093       objs.push(tmp);
   1094     }
   1095 
   1096     for (var i = 0; i < count; i++) {
   1097       runType();
   1098     }
   1099 
   1100     return objs;
   1101   },
   1102 
   1103   /**
   1104    * Reads a pointer token value from current set data.
   1105    *
   1106    * @return Pointer value
   1107    */
   1108   _getPointer: function () {
   1109     return this._getStrNumber();
   1110   },
   1111 
   1112   /**
   1113    * Reads a time token value from current set data.
   1114    *
   1115    * @return Time value (Date)
   1116    */
   1117   _getTime: function () {
   1118     var str = this._getStrNumber();
   1119 
   1120     return new Date(parseInt(str, 10) * 1000);
   1121   },
   1122 
   1123   /**
   1124    * Reads an integer token value from current set data.
   1125    *
   1126    * @return Integer value
   1127    */
   1128   _getInt: function () {
   1129     var parsedData = new Uint8Array(this._getSlice(4));
   1130 
   1131     return (
   1132       ((parsedData[0] & 0xff) << 24) |
   1133       ((parsedData[1] & 0xff) << 16) |
   1134       ((parsedData[2] & 0xff) << 8) |
   1135       (parsedData[3] & 0xff)
   1136     );
   1137   },
   1138 
   1139   /**
   1140    * Reads a byte from current set data.
   1141    *
   1142    * @return Byte value (integer)
   1143    */
   1144   _getByte: function () {
   1145     var parsedData = new Uint8Array(this._getSlice(1));
   1146 
   1147     return parsedData[0];
   1148   },
   1149 
   1150   /**
   1151    * Reads a character token value from current set data.
   1152    *
   1153    * @return Character (string)
   1154    */
   1155   _getChar: function () {
   1156     return this._getByte();
   1157   },
   1158 
   1159   /**
   1160    * Reads a string token value from current set data.
   1161    *
   1162    * @return String value
   1163    */
   1164   _getString: function () {
   1165     var l = this._getInt();
   1166 
   1167     if (l > 0) {
   1168       var s = this._getSlice(l);
   1169       var parsedData = new Uint8Array(s);
   1170 
   1171       return WeeChatProtocol._uia2s(parsedData);
   1172     }
   1173 
   1174     return '';
   1175   },
   1176 
   1177   /**
   1178    * Reads a message header from current set data.
   1179    *
   1180    * @return Header object
   1181    */
   1182   _getHeader: function () {
   1183     var len = this._getInt();
   1184     var comp = this._getByte();
   1185 
   1186     return {
   1187       length: len,
   1188       compression: comp
   1189     };
   1190   },
   1191 
   1192   /**
   1193    * Reads a message header ID from current set data.
   1194    *
   1195    * @return Message ID (string)
   1196    */
   1197   _getId: function () {
   1198     return this._getString();
   1199   },
   1200 
   1201   /**
   1202    * Reads an arbitrary object token from current set data.
   1203    *
   1204    * @return Object value
   1205    */
   1206   _getObject: function () {
   1207     var self = this;
   1208     var type = this._getType();
   1209 
   1210     if (type) {
   1211       return {
   1212         type: type,
   1213         content: self._runType(type)
   1214       };
   1215     }
   1216   },
   1217 
   1218   /**
   1219    * Reads an hash table token from current set data.
   1220    *
   1221    * @return Hash table
   1222    */
   1223   _getHashTable: function () {
   1224     var self = this;
   1225     var typeKeys, typeValues, count;
   1226     var dict = {};
   1227 
   1228     typeKeys = this._getType();
   1229     typeValues = this._getType();
   1230     count = this._getInt();
   1231 
   1232     for (var i = 0; i < count; ++i) {
   1233       var key = self._runType(typeKeys);
   1234       var keyStr = self._objToString(key, typeKeys);
   1235       var value = self._runType(typeValues);
   1236       dict[keyStr] = value;
   1237     }
   1238 
   1239     return dict;
   1240   },
   1241 
   1242   /**
   1243    * Reads an array token from current set data.
   1244    *
   1245    * @return Array
   1246    */
   1247   _getArray: function () {
   1248     var self = this;
   1249     var type;
   1250     var count;
   1251     var values;
   1252 
   1253     type = this._getType();
   1254     count = this._getInt();
   1255     values = [];
   1256 
   1257     for (var i = 0; i < count; i++) {
   1258       values.push(self._runType(type));
   1259     }
   1260 
   1261     return values;
   1262   },
   1263 
   1264   /**
   1265    * Reads an infolist object from the current set of data
   1266    *
   1267    * @return Array
   1268    */
   1269   _getInfolist: function () {
   1270     var self = this;
   1271     var name;
   1272     var count;
   1273     var values;
   1274 
   1275     name = this._getString();
   1276     count = this._getInt();
   1277     values = [];
   1278 
   1279     for (var i = 0; i < count; i++) {
   1280       var itemcount = self._getInt();
   1281       var litem = [];
   1282       for (var j = 0; j < itemcount; j++) {
   1283         var item = {};
   1284         item[self._getString()] = self._runType(self._getType());
   1285         litem.push(item);
   1286       }
   1287       values.push(litem);
   1288     }
   1289 
   1290     return values;
   1291   },
   1292 
   1293   /**
   1294    * Reads a specified number of bytes from current set data.
   1295    *
   1296    * @param length Number of bytes to read
   1297    * @return Sliced array
   1298    */
   1299   _getSlice: function (length) {
   1300     if (this.dataAt + length > this._data.byteLength) {
   1301       return null;
   1302     }
   1303 
   1304     var slice = this._data.slice(this._dataAt, this._dataAt + length);
   1305 
   1306     this._dataAt += length;
   1307 
   1308     return slice;
   1309   },
   1310 
   1311   /**
   1312    * Sets the current data.
   1313    *
   1314    * @param data Current data
   1315    */
   1316   _setData: function (data) {
   1317     this._data = data;
   1318   },
   1319 
   1320   /**
   1321    * Add the ID to the previously formatted command
   1322    *
   1323    * @param id Command ID
   1324    * @param command previously formatted command
   1325    */
   1326   setId: function (id, command) {
   1327     return '(' + id + ') ' + command;
   1328   },
   1329 
   1330   /**
   1331    * Parses a WeeChat message.
   1332    *
   1333    * @param data Message data (ArrayBuffer)
   1334    * @return Message value
   1335    */
   1336   parse: function (data, optionsValues) {
   1337     var self = this;
   1338 
   1339     this._setData(data);
   1340     this._dataAt = 0;
   1341 
   1342     var header = this._getHeader();
   1343 
   1344     if (header.compression) {
   1345       var raw = new Uint8Array(data, 5); // skip first five bytes (header, 4B size, 1B compression flag)
   1346       var inflate = new Zlib.Inflate(raw);
   1347       var plain = inflate.decompress();
   1348       this._setData(plain.buffer);
   1349       this._dataAt = 0; // reset position in data, as the header is not part of the decompressed data
   1350     }
   1351 
   1352     var id = this._getId();
   1353     var objects = [];
   1354     var object = this._getObject();
   1355 
   1356     while (object) {
   1357       objects.push(object);
   1358       object = self._getObject();
   1359     }
   1360     var msg = {
   1361       header: header,
   1362       id: id,
   1363       objects: objects
   1364     };
   1365 
   1366     return msg;
   1367   }
   1368 };