az
Version:
A NLP library for Russian language
889 lines (837 loc) • 45.3 kB
JavaScript
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = module.exports || {}) && (module.exports.Tokens = factory()) :
typeof define === 'function' && define.amd ? define('Az.Tokens', ['Az'], factory) :
(global.Az = global.Az || {}) && (global.Az.Tokens = factory())
}(this, function () { 'use strict';
/** @namespace Az **/
var TLDs = 'ac|ad|ae|aero|af|ag|ai|al|am|ao|aq|ar|arpa|as|asia|at|au|aw|ax|az|ba|bb|be|bf|bg|bh|bi|biz|bj|bm|bo|br|bs|bt|bv|bw|by|bz|ca|cat|cc|cd|cf|cg|ch|ci|cl|cm|cn|co|com|coop|cr|cu|cv|cw|cx|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|es|et|eu|fi|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|info|int|io|iq|ir|is|it|je|jo|jobs|jp|kg|ki|km|kn|kp|kr|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mn|mo|mobi|mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|na|name|nc|ne|net|nf|ng|nl|no|nr|nu|nz|om|org|pa|pe|pf|ph|pk|pl|pm|pn|post|pr|pro|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tr|travel|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|yt|امارات|հայ|বাংলা|бел|中国|中國|الجزائر|مصر|ею|გე|ελ|香港|भारत|بھارت|భారత్|ભારત|ਭਾਰਤ|ভারত|இந்தியா|ایران|ايران|عراق|الاردن|한국|қаз|ලංකා|இலங்கை|المغرب|мкд|мон|澳門|澳门|مليسيا|عمان|پاکستان|پاكستان|فلسطين|срб|рф|قطر|السعودية|السعودیة|السعودیۃ|السعوديه|سودان|新加坡|சிங்கப்பூர்|سورية|سوريا|ไทย|تونس|台灣|台湾|臺灣|укр|اليمن|xxx|zm|aaa|aarp|abarth|abb|abbott|abbvie|abc|able|abogado|abudhabi|academy|accenture|accountant|accountants|aco|active|actor|adac|ads|adult|aeg|aetna|afamilycompany|afl|africa|africamagic|agakhan|agency|aig|aigo|airbus|airforce|airtel|akdn|alfaromeo|alibaba|alipay|allfinanz|allstate|ally|alsace|alstom|americanexpress|americanfamily|amex|amfam|amica|amsterdam|analytics|android|anquan|anz|aol|apartments|app|apple|aquarelle|arab|aramco|archi|army|art|arte|asda|associates|athleta|attorney|auction|audi|audible|audio|auspost|author|auto|autos|avianca|aws|axa|azure|baby|baidu|banamex|bananarepublic|band|bank|bar|barcelona|barclaycard|barclays|barefoot|bargains|baseball|basketball|bauhaus|bayern|bbc|bbt|bbva|bcg|bcn|beats|beauty|beer|bentley|berlin|best|bestbuy|bet|bharti|bible|bid|bike|bing|bingo|bio|black|blackfriday|blanco|blockbuster|blog|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|boehringer|bofa|bom|bond|boo|book|booking|boots|bosch|bostik|boston|bot|boutique|box|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|bugatti|build|builders|business|buy|buzz|bzh|cab|cafe|cal|call|calvinklein|camera|camp|cancerresearch|canon|capetown|capital|capitalone|car|caravan|cards|care|career|careers|cars|cartier|casa|case|caseih|cash|casino|catering|catholic|cba|cbn|cbre|cbs|ceb|center|ceo|cern|cfa|cfd|chanel|channel|chase|chat|cheap|chintai|chloe|christmas|chrome|chrysler|church|cipriani|circle|cisco|citadel|citi|citic|city|cityeats|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|coach|codes|coffee|college|cologne|comcast|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cookingchannel|cool|corsica|country|coupon|coupons|courses|credit|creditcard|creditunion|cricket|crown|crs|cruise|cruises|csc|cuisinella|cymru|cyou|dabur|dad|dance|date|dating|datsun|day|dclk|dds|deal|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|dhl|diamonds|diet|digital|direct|directory|discount|discover|dish|diy|dnp|docs|dodge|dog|doha|domains|dot|download|drive|dstv|dtv|dubai|duck|dunlop|duns|dupont|durban|dvag|dwg|earth|eat|edeka|education|email|emerck|emerson|energy|engineer|engineering|enterprises|epost|epson|equipment|ericsson|erni|esq|estate|esurance|etisalat|eurovision|eus|events|everbank|exchange|expert|exposed|express|extraspace|fage|fail|fairwinds|faith|family|fan|fans|farm|farmers|fashion|fast|fedex|feedback|ferrari|ferrero|fiat|fidelity|fido|film|final|finance|financial|fire|firestone|firmdale|fish|fishing|fit|fitness|flickr|flights|flir|florist|flowers|flsmidth|fly|foo|foodnetwork|football|ford|forex|forsale|forum|foundation|fox|free|fresenius|frl|frogans|frontdoor|frontier|ftr|fujitsu|fujixerox|fun|fund|furniture|futbol|fyi|gal|gallery|gallo|gallup|game|games|gap|garden|gbiz|gdn|gea|gent|genting|george|ggee|gift|gifts|gives|giving|glade|glass|gle|global|globo|gmail|gmbh|gmo|gmx|godaddy|gold|goldpoint|golf|goo|goodhands|goodyear|goog|google|gop|got|gotv|grainger|graphics|gratis|green|gripe|group|guardian|gucci|guge|guide|guitars|guru|hair|hamburg|hangout|haus|hbo|hdfc|hdfcbank|health|healthcare|help|helsinki|here|hermes|hgtv|hiphop|hisamitsu|hitachi|hiv|hkt|hockey|holdings|holiday|homedepot|homegoods|homes|homesense|honda|honeywell|horse|host|hosting|hot|hoteles|hotmail|house|how|hsbc|htc|hughes|hyatt|hyundai|ibm|icbc|ice|icu|ieee|ifm|iinet|ikano|imamat|imdb|immo|immobilien|industries|infiniti|ing|ink|institute|insurance|insure|intel|international|intuit|investments|ipiranga|irish|iselect|ismaili|ist|istanbul|itau|itv|iveco|iwc|jaguar|java|jcb|jcp|jeep|jetzt|jewelry|jio|jlc|jll|jmp|jnj|joburg|jot|joy|jpmorgan|jprs|juegos|juniper|kaufen|kddi|kerryhotels|kerrylogistics|kerryproperties|kfh|kia|kim|kinder|kindle|kitchen|kiwi|koeln|komatsu|kosher|kpmg|kpn|krd|kred|kuokgroup|kyknet|kyoto|lacaixa|ladbrokes|lamborghini|lamer|lancaster|lancia|lancome|land|landrover|lanxess|lasalle|lat|latino|latrobe|law|lawyer|lds|lease|leclerc|lefrak|legal|lego|lexus|lgbt|liaison|lidl|life|lifeinsurance|lifestyle|lighting|like|lilly|limited|limo|lincoln|linde|link|lipsy|live|living|lixil|loan|loans|locker|locus|loft|lol|london|lotte|lotto|love|lpl|lplfinancial|ltd|ltda|lundbeck|lupin|luxe|luxury|macys|madrid|maif|maison|makeup|man|management|mango|market|marketing|markets|marriott|marshalls|maserati|mattel|mba|mcd|mcdonalds|mckinsey|med|media|meet|melbourne|meme|memorial|men|menu|meo|metlife|miami|microsoft|mini|mint|mit|mitsubishi|mlb|mls|mma|mnet|mobily|moda|moe|moi|mom|monash|money|monster|montblanc|mopar|mormon|mortgage|moscow|moto|motorcycles|mov|movie|movistar|msd|mtn|mtpc|mtr|multichoice|mutual|mutuelle|mzansimagic|nab|nadex|nagoya|naspers|nationwide|natura|navy|nba|nec|netbank|netflix|network|neustar|new|newholland|news|next|nextdirect|nexus|nfl|ngo|nhk|nico|nike|nikon|ninja|nissan|nissay|nokia|northwesternmutual|norton|now|nowruz|nowtv|nra|nrw|ntt|nyc|obi|observer|off|office|okinawa|olayan|olayangroup|oldnavy|ollo|omega|one|ong|onl|online|onyourside|ooo|open|oracle|orange|organic|orientexpress|origins|osaka|otsuka|ott|ovh|page|pamperedchef|panasonic|panerai|paris|pars|partners|parts|party|passagens|pay|payu|pccw|pet|pfizer|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pid|pin|ping|pink|pioneer|pizza|place|play|playstation|plumbing|plus|pnc|pohl|poker|politie|porn|pramerica|praxi|press|prime|prod|productions|prof|progressive|promo|properties|property|protection|pru|prudential|pub|pwc|qpon|quebec|quest|qvc|racing|raid|read|realestate|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|reliance|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|richardli|ricoh|rightathome|ril|rio|rip|rmit|rocher|rocks|rodeo|rogers|room|rsvp|ruhr|run|rwe|ryukyu|saarland|safe|safety|sakura|sale|salon|samsclub|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|sas|save|saxo|sbi|sbs|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scjohnson|scor|scot|seat|secure|security|seek|select|sener|services|ses|seven|sew|sex|sexy|sfr|shangrila|sharp|shaw|shell|shia|shiksha|shoes|shopping|shouji|show|showtime|shriram|silk|sina|singles|site|ski|skin|sky|skype|sling|smart|smile|sncf|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|spiegel|spot|spreadbetting|srl|srt|stada|staples|star|starhub|statebank|statefarm|statoil|stc|stcgroup|stockholm|storage|store|stream|studio|study|style|sucks|supersport|supplies|supply|support|surf|surgery|suzuki|swatch|swiftcover|swiss|sydney|symantec|systems|tab|taipei|talk|taobao|target|tatamotors|tatar|tattoo|tax|taxi|tci|tdk|team|tech|technology|telecity|telefonica|temasek|tennis|teva|thd|theater|theatre|theguardian|tiaa|tickets|tienda|tiffany|tips|tires|tirol|tjmaxx|tjx|tkmaxx|tmall|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|trade|trading|training|travelchannel|travelers|travelersinsurance|trust|trv|tube|tui|tunes|tushu|tvs|ubank|ubs|uconnect|unicom|university|uno|uol|ups|vacations|vana|vanguard|vegas|ventures|verisign|versicherung|vet|viajes|video|vig|viking|villas|vin|vip|virgin|visa|vision|vista|vistaprint|viva|vivo|vlaanderen|vodka|volkswagen|volvo|vote|voting|voto|voyage|vuelos|wales|walmart|walter|wang|wanggou|warman|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weibo|weir|whoswho|wien|wiki|williamhill|win|windows|wine|winners|wme|wolterskluwer|woodside|work|works|world|wow|wtc|wtf|xbox|xerox|xfinity|xihuan|xin|कॉम|セール|佛山|慈善|集团|在线|大众汽车|点看|คอม|八卦|موقع|一号店|公益|公司|香格里拉|网站|移动|我爱你|москва|католик|онлайн|сайт|联通|קום|时尚|微博|淡马锡|ファッション|орг|नेट|ストア|삼성|商标|商店|商城|дети|ポイント|新闻|工行|家電|كوم|中文网|中信|娱乐|谷歌|電訊盈科|购物|クラウド|通販|网店|संगठन|餐厅|网络|ком|诺基亚|食品|飞利浦|手表|手机|ارامكو|العليان|اتصالات|بازار|موبايلي|ابوظبي|كاثوليك|همراه|닷컴|政府|شبكة|بيتك|عرب|机构|组织机构|健康|рус|珠宝|大拿|みんな|グーグル|世界|書籍|网址|닷넷|コム|天主教|游戏|vermögensberater|vermögensberatung|企业|信息|嘉里大酒店|嘉里|广东|政务|xperia|xyz|yachts|yahoo|yamaxun|yandex|yodobashi|yoga|yokohama|you|youtube|yun|zappos|zara|zero|zip|zippo|zone|zuerich'.split('|');
var defaults = {
html: false,
wiki: false, // TODO: check all cases
markdown: false, // TODO: check all cases
hashtags: true,
mentions: true,
emails: true,
links: {
protocols: true,
www: false,
tlds: {}
}
};
/* TODO: add more named HTML entities */
var HTML_ENTITIES = { nbsp: ' ', quot: '"', gt: '>', lt: '<', amp: '&', ndash: '–' };
for (var i = 0; i < TLDs.length; i++) {
defaults.links.tlds[TLDs[i]] = true;
}
/**
* Токен, соответствующий некоторой подстроке в представленном на вход тексте.
*
* @constructor
* @property {string} type Тип токена.
* @property {string} subType Подтип токена.
* @property {number} st Индекс первого символа, входящего в токен.
* @property {number} en Индекс последнего символа, входящего в токен.
* @property {number} length Длина токена.
* @property {boolean} firstUpper True, если первый символ токена является заглавной буквой.
* @property {boolean} allUpper True, если все символы в токене являются заглавными буквами.
*/
var Token = function(source, st, length, index, firstUpper, allUpper, type, subType) {
this.source = source;
this.st = st;
this.length = length;
this.index = index;
this.firstUpper = firstUpper;
this.allUpper = allUpper;
this.type = type;
if (subType) {
this.subType = subType;
}
}
Token.prototype.toString = function() {
return (('_str' in this) && (this._str.length == this.length)) ? this._str : (this._str = this.source.substr(this.st, this.length));
}
Token.prototype.indexOf = function(str) {
if (str.length == 1) {
for (var i = 0; i < this.length; i++) {
if (this.source[this.st + i] == str) {
return i;
}
}
return -1;
}
return this.toString().indexOf(str);
}
Token.prototype.toLowerCase = function() {
return this.toString().toLocaleLowerCase();
}
Token.prototype.isCapitalized = function() {
return this.firstUpper && !this.allUpper;
}
Token.prototype.en = function() {
return this.st + this.length - 1;
}
/**
* Создает токенизатор текста с заданными опциями.
*
* @playground
* var Az = require('az');
* var tokens = Az.Tokens('Текст (от лат. textus — «ткань; сплетение, связь, паутина, сочетание») — зафиксированная на каком-либо материальном носителе человеческая мысль; в общем плане связная и полная последовательность символов.');
* tokens.done();
* @constructor
* @param {string} [text] Строка для разбивки на токены.
* @param {Object} [config] Опции, применяемые при разбивке.
* @param {boolean} [config.html=False] Распознавать и выделять в отдельные
* токены (типа TAG) HTML-теги. Кроме того, содержимое тегов <style>
* и <script> будет размечено как единый токен типа CONTENT.
* @param {boolean} [config.wiki=False] Распознавать и выделять в отдельные
* токены элементы вики-разметки.
* @param {boolean} [config.markdown=False] Распознавать и выделять в отдельные
* токены элементы Markdown-разметки.
* @param {boolean} [config.hashtags=True] Распознавать и выделять в отдельные
* токены хэштеги (строки, начинающиеся с символа «#»).
* @param {boolean} [config.mentions=True] Распознавать и выделять в отдельные
* токены упоминания (строки, начинающиеся с символа «@»).
* @param {boolean} [config.emails=True] Распознавать и выделять в отдельные
* токены е-мейлы (нет, распознавание всех корректных по RFC адресов не
* гарантируется).
* @param {Object} [config.links] Настройки распознавания ссылок. False, чтобы
* не распознавать ссылки совсем.
* @param {boolean} [config.links.protocols=True] Распознавать и выделять в отдельные
* токены ссылки с указанным протоколом (http://, https:// и вообще любым другим).
* @param {boolean} [config.links.www=False] Распознавать и выделять в отдельные
* токены ссылки, начинающиеся с «www.».
* @param {Object} [config.links.tlds] Объект, в котором ключами должны быть
* домены верхнего уровня, в которых будут распознаваться ссылки. По умолчанию
* текущий список всех таких доменов.
* @memberof Az
*/
var Tokens = function(text, config) {
if (this instanceof Tokens) {
this.tokens = [];
this.source = '';
if (typeof text == 'string') {
this.config = config ? Az.extend(defaults, config) : defaults;
this.append(text);
} else {
this.config = text ? Az.extend(defaults, text) : defaults;
}
this.index = -1;
} else {
return new Tokens(text, config);
}
}
Tokens.WORD = new String('WORD');
Tokens.NUMBER = new String('NUMBER');
Tokens.OTHER = new String('OTHER');
Tokens.DIGIT = new String('DIGIT');
Tokens.CYRIL = new String('CYRIL');
Tokens.LATIN = new String('LATIN');
Tokens.MIXED = new String('MIXED');
Tokens.PUNCT = new String('PUNCT');
Tokens.SPACE = new String('SPACE');
Tokens.MARKUP = new String('MARKUP');
Tokens.NEWLINE = new String('NEWLINE');
Tokens.EMAIL = new String('EMAIL');
Tokens.LINK = new String('LINK');
Tokens.HASHTAG = new String('HASHTAG');
Tokens.MENTION = new String('MENTION');
Tokens.TAG = new String('TAG');
Tokens.CONTENT = new String('CONTENT');
Tokens.SCRIPT = new String('SCRIPT');
Tokens.STYLE = new String('STYLE');
Tokens.COMMENT = new String('COMMENT');
Tokens.CLOSING = new String('CLOSING');
Tokens.TEMPLATE = new String('TEMPLATE');
Tokens.RANGE = new String('RANGE');
Tokens.ENTITY = new String('ENTITY');
/**
* Отправляет ещё один кусок текста на токенизацию. Таким образом вполне
* допустимо обрабатывать большие документы частями, многократно вызывая этот
* метод. При этом токен может начаться в одной части и продолжиться в
* следующей (а закончиться в ещё одной).
*
* @param {string} text Строка для разбивки на токены.
* @param {Object} [config] Опции, применяемые при разбивке. Перекрывают
* опции, заданные в конструкторе токенизатора.
* @see Tokens
*/
Tokens.prototype.append = function(text, config) {
'use strict';
// Для производительности:
// - как можно меньше операций конкатенции/разбивки строк
// - вместо сравнения всего токена, проверяем соответствующий ему символ в исходной строке
// - типы токенов - константы в Tokens, формально это строки, но сравниваем через === (как объекты)
config = config ? Az.extend(this.config, config) : this.config;
if (config.links && (config.links.tlds === true)) {
config.links.tlds = defaults.links.tlds;
}
var offs = this.source.length;
this.source += text;
var s = this.source, ts = this.tokens;
for (var i = offs; i < s.length; i++) {
var ch = s[i];
var code = s.charCodeAt(i);
var append = false;
var last = ts.length - 1;
var token = ts[last];
var st = i;
if (config.html && (ch == ';')) {
//
if ((last > 0) &&
(token.type === Tokens.WORD) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '&')) {
var name = token.toLowerCase();
if (name in HTML_ENTITIES) {
ch = HTML_ENTITIES[name];
code = ch.charCodeAt(0);
last -= 2;
token = ts[last];
ts.length = last + 1;
}
} else
// &x123AF5;
// &1234;
if ((last > 1) &&
((token.type === Tokens.NUMBER) ||
((token.type === Tokens.WORD) &&
(s[token.st] == 'x'))) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '#') &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '&')) {
if (s[token.st] == 'x') {
code = parseInt(token.toString().substr(1), 16);
} else {
code = parseInt(token.toString(), 10);
}
ch = String.fromCharCode(code);
last -= 3;
token = ts[last];
ts.length = last + 1;
}
}
var charType = Tokens.OTHER;
var charUpper = (ch.toLocaleLowerCase() != ch);
if (code >= 0x0400 && code <= 0x04FF) charType = Tokens.CYRIL;
if ((code >= 0x0041 && code <= 0x005A) || (code >= 0x0061 && code <= 0x007A) || (code >= 0x00C0 && code <= 0x024F)) charType = Tokens.LATIN;
if (code >= 0x0030 && code <= 0x0039) charType = Tokens.DIGIT;
if ((code <= 0x0020) || (code >= 0x0080 && code <= 0x00A0)) charType = Tokens.SPACE;
if ('‐-−‒–—―.…,:;?!¿¡()[]«»"\'’‘’“”/⁄'.indexOf(ch) > -1) charType = Tokens.PUNCT;
var tokenType = charType;
var tokenSubType = false;
if (charType === Tokens.CYRIL || charType === Tokens.LATIN) {
tokenType = Tokens.WORD;
tokenSubType = charType;
} else
if (charType === Tokens.DIGIT) {
tokenType = Tokens.NUMBER;
}
var lineStart = !token || (s[token.st + token.length - 1] == '\n');
if (config.wiki) {
if (lineStart) {
if (':;*#~|'.indexOf(ch) > -1) {
tokenType = Tokens.MARKUP;
tokenSubType = Tokens.NEWLINE;
}
}
if ('={[|]}'.indexOf(ch) > -1) {
tokenType = Tokens.MARKUP;
}
}
if (config.markdown) {
if (lineStart) {
if ('=-#>+-'.indexOf(ch) > -1) {
tokenType = Tokens.MARKUP;
tokenSubType = Tokens.NEWLINE;
}
}
if ('[]*~_`\\'.indexOf(ch) > -1) {
tokenType = Tokens.MARKUP;
}
}
if (token) {
if (config.wiki &&
(ch != "'") &&
(token.length == 1) &&
(s[token.st] == "'") &&
(last > 0) &&
(ts[last - 1].type === Tokens.WORD) &&
(ts[last - 1].subType === Tokens.LATIN)) {
ts[last - 1].length += token.length;
last -= 1;
ts.length = last + 1;
token = ts[last];
}
// Preprocess last token
if (config.links &&
config.links.tlds &&
((charType === Tokens.PUNCT) ||
(charType === Tokens.SPACE)) &&
(ts.length > 2) &&
(ts[last - 2].type === Tokens.WORD) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '.') &&
(ts[last].type === Tokens.WORD) &&
(token.toString() in config.links.tlds)) {
// Merge all subdomains
while ((last >= 2) &&
(ts[last - 2].type === Tokens.WORD) &&
(ts[last - 1].length == 1) &&
((s[ts[last - 1].st] == '.') ||
(s[ts[last - 1].st] == '@') ||
(s[ts[last - 1].st] == ':'))) {
last -= 2;
token = ts[last];
token.length += ts[last + 1].length + ts[last + 2].length;
token.allUpper = token.allUpper && ts[last + 1].allUpper && ts[last + 2].allUpper;
}
if (config.emails &&
(token.indexOf('@') > -1) &&
(token.indexOf(':') == -1)) {
// URL can contain a '@' but in that case it should be in form http://user@site.com or user:pass@site.com
// So if URL has a '@' but no ':' in it, we assume it's a email
token.type = Tokens.EMAIL;
} else {
token.type = Tokens.LINK;
if (ch == '/') {
append = true;
}
}
ts.length = last + 1;
} else
// Process next char (start new token or append to the previous one)
if (token.type === Tokens.LINK) {
if ((ch == ')') &&
(last >= 1) &&
(ts[last - 1].type === Tokens.MARKUP) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '(')) {
tokenType = Tokens.MARKUP;
} else
if ((charType !== Tokens.SPACE) && (ch != ',') && (ch != '<')) {
append = true;
}
} else
if (token.type === Tokens.EMAIL) {
if ((charType === Tokens.CYRIL) || (charType === Tokens.LATIN) || (ch == '.')) {
append = true;
}
} else
if ((token.type === Tokens.HASHTAG) || (token.type === Tokens.MENTION)) {
if ((charType === Tokens.CYRIL) ||
(charType == Tokens.LATIN) ||
(charType == Tokens.DIGIT) ||
(ch == '_') || ((ch == '@') && (token.indexOf('@') == -1))) {
append = true;
}
} else
if ((token.type === Tokens.TAG) && (token.quote || (s[token.en()] != '>'))) {
append = true;
if (token.quote) {
if ((ch == token.quote) && (s[token.en()] != '\\')) {
delete token.quote;
}
} else
if ((ch == '"') || (ch == "'")) {
token.quote = ch;
}
} else
if (token.type === Tokens.CONTENT) {
append = true;
if (token.quote) {
if ((ch == token.quote) && (s[token.en()] != '\\')) {
delete token.quote;
}
} else
if ((ch == '"') || (ch == "'")) {
token.quote = ch;
} else
if (ch == '>') {
if ((token.length >= 8) && (token.toString().substr(-8) == '</script')) {
token.length -= 8;
st -= 8;
append = false;
tokenType = Tokens.TAG;
tokenSubType = Tokens.CLOSING;
} else
if ((token.length >= 7) && (token.toString().substr(-7) == '</style')) {
token.length -= 7;
st -= 7;
append = false;
tokenType = Tokens.TAG;
tokenSubType = Tokens.CLOSING;
}
}
} else
if ((token.type === Tokens.TAG) &&
(token.type !== Tokens.CLOSING) &&
(token.length >= 8) &&
(token.toLowerCase().substr(1, 6) == 'script')) {
tokenType = Tokens.CONTENT;
tokenSubType = Tokens.SCRIPT;
} else
if ((token.type === Tokens.TAG) &&
(token.type !== Tokens.CLOSING) &&
(token.length >= 7) &&
(token.toLowerCase().substr(1, 5) == 'style')) {
tokenType = Tokens.CONTENT;
tokenSubType = Tokens.STYLE;
} else
if (config.html &&
(token.length == 1) &&
(s[token.st] == '<') &&
((charType === Tokens.LATIN) || (ch == '!') || (ch == '/'))) {
append = true;
token.type = Tokens.TAG;
if (ch == '!') {
token.subType = Tokens.COMMENT;
} else
if (ch == '/') {
token.subType = Tokens.CLOSING;
}
} else
if (token.type === Tokens.CONTENT) {
append = true;
} else
if ((token.type === Tokens.MARKUP) &&
(token.subType == Tokens.TEMPLATE) &&
((s[token.en()] != '}') ||
(s[token.en() - 1] != '}'))) {
append = true;
} else
if ((token.type === Tokens.MARKUP) &&
(token.type === Tokens.LINK) &&
(s[token.en()] != ')')) {
append = true;
} else
if ((token.type === Tokens.MARKUP) &&
(s[token.st] == '`') &&
(token.subType === Tokens.NEWLINE) &&
(charType === Tokens.LATIN)) {
append = true;
} else
if ((charType === Tokens.CYRIL) || (charType === Tokens.LATIN)) {
if (token.type === Tokens.WORD) {
append = true;
token.subType = (token.subType == charType) ? token.subType : Tokens.MIXED;
} else
if (token.type === Tokens.NUMBER) { // Digits + ending
append = true;
token.subType = (token.subType && token.subType != charType) ? Tokens.MIXED : charType;
} else
if (config.hashtags && (token.length == 1) && (s[token.st] == '#')) { // Hashtags
append = true;
token.type = Tokens.HASHTAG;
} else
if (config.mentions &&
(token.length == 1) &&
(s[token.st] == '@') &&
((last == 0) || (ts[last - 1].type === Tokens.SPACE))) { // Mentions
append = true;
token.type = Tokens.MENTION;
} else
if ((charType === Tokens.LATIN) &&
(token.length == 1) &&
((s[token.st] == "'") || (s[token.st] == '’'))) {
append = true;
token.type = Tokens.WORD;
token.subType = Tokens.LATIN;
} else
if ((token.length == 1) && (s[token.st] == '-')) { // -цать (?), 3-й
append = true;
if ((last > 0) && (ts[last - 1].type === Tokens.NUMBER)) {
token = ts[last - 1];
token.length += ts[last].length;
ts.length -= 1;
}
token.type = Tokens.WORD;
token.subType = charType;
}
} else
if (charType === Tokens.DIGIT) {
if (token.type === Tokens.WORD) {
append = true;
token.subType = Tokens.MIXED;
} else
if (token.type === Tokens.NUMBER) {
append = true;
} else
if ((token.length == 1) &&
((s[token.st] == '+') || (s[token.st] == '-'))) {
append = true;
if ((last > 0) && (ts[last - 1].type === Tokens.NUMBER)) {
token = ts[last - 1];
token.length += ts[last].length;
token.subType = Tokens.RANGE;
ts.length -= 1;
}
token.type = Tokens.NUMBER;
} else
if ((token.length == 1) &&
((s[token.st] == ',') || (s[token.st] == '.')) &&
(ts.length > 1) &&
(ts[last - 1].type === Tokens.NUMBER)) {
append = true;
token = ts[last - 1];
token.length += ts[last].length;
ts.length -= 1;
}
} else
if (charType === Tokens.SPACE) {
if (token.type === Tokens.SPACE) {
append = true;
}
} else
if ((token.type === Tokens.MARKUP) &&
(s[token.st] == ch) &&
('=-~:*#`\'>_'.indexOf(ch) > -1)) {
append = true;
} else
if (ch == '.') {
if (config.links &&
config.links.www &&
(token.length == 3) &&
(token.toLowerCase() == 'www')) { // Links without protocol but with www
append = true;
token.type = Tokens.LINK;
}
} else
if (config.wiki && (ch == "'") && (s[token.en()] == "'")) {
if (token.length > 1) {
token.length--;
st--;
tokenType = Tokens.MARKUP;
} else {
append = true;
token.type = Tokens.MARKUP;
}
} else
if ((ch == '-') ||
((token.subType == Tokens.LATIN) &&
((ch == '’') || (ch == "'")))) {
if (token.type === Tokens.WORD) {
append = true;
}
} else
if (ch == '/') {
if (config.links &&
config.links.protocols &&
(ts.length > 2) &&
(ts[last - 2].type === Tokens.WORD) &&
(ts[last - 2].subType == Tokens.LATIN) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == ':') &&
(ts[last].length == 1) &&
(s[ts[last].st] == '/')) { // Links (with protocols)
append = true;
token = ts[last - 2];
token.length += ts[last - 1].length + ts[last].length;
token.allUpper = token.allUpper && ts[last - 1].allUpper && ts[last].allUpper;
token.type = Tokens.LINK;
ts.length -= 2;
}
} else
if (config.html && ch == ';') {
if ((last > 0) &&
(token.type === Tokens.WORD) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '&')) {
append = true;
token = ts[last - 1];
token.length += ts[last].length;
token.allUpper = token.allUpper && ts[last - 1].allUpper;
token.type = Tokens.ENTITY;
ts.length -= 1;
} else
if ((last > 1) &&
((token.type === Tokens.WORD) ||
(token.type === Tokens.NUMBER)) &&
(ts[last - 1].length == 1) &&
(s[ts[last - 1].st] == '#') &&
(ts[last - 2].length == 1) &&
(s[ts[last - 2].st] == '&')) {
append = true;
token = ts[last - 2];
token.length += ts[last - 1].length + ts[last].length;
token.allUpper = token.allUpper && ts[last - 1].allUpper && ts[last].allUpper;
token.type = Tokens.ENTITY;
ts.length -= 2;
}
} else
if (config.markdown &&
(ch == '[') &&
(token.length == 1) &&
(s[token.st] == '!')) {
append = true;
token.type = Tokens.MARKUP;
} else
if (config.markdown &&
(ch == '(') &&
(token.length == 1) &&
(s[token.st] == ']')) {
tokenType = Tokens.MARKUP;
tokenSubType = Tokens.LINK;
} else
if (config.wiki &&
(ch == '{') &&
(token.length == 1) &&
(s[token.st] == '{')) {
append = true;
token.type = Tokens.MARKUP;
token.subType = Tokens.TEMPLATE;
} else
if (config.wiki &&
(ch == '[') &&
(token.length == 1) &&
(s[token.st] == '[')) {
append = true;
} else
if (config.wiki &&
(ch == ']') &&
(token.length == 1) &&
(s[token.st] == ']')) {
append = true;
} else
if (config.wiki && (ch == '|') && !lineStart) {
var found = -1;
for (var j = last - 1; j >= 0; j--) {
if ((ts[j].length == 2) &&
(s[ts[j].st] == '[') &&
(s[ts[j].st + 1] == '[')) {
found = j;
break;
}
if (((ts[j].length == 1) &&
(s[ts[j].st] == '|')) ||
ts[j].indexOf('\n') > -1) {
break;
}
}
if (found > -1) {
append = true;
for (var j = last - 1; j >= found; j--) {
token = ts[j];
token.length += ts[j + 1].length;
token.allUpper = token.allUpper && ts[j + 1].allUpper;
}
last = found;
ts.length = last + 1;
token.subType = Tokens.LINK;
}
}
}
if (append) {
token.length++;
token.allUpper = token.allUpper && charUpper;
} else {
token = new Token(s, st, i + 1 - st, ts.length, charUpper, charUpper, tokenType, tokenSubType);
ts.push(token);
}
}
return this;
}
function alwaysTrue() {
return true;
}
function getMatcher(filter, exclude) {
if (!filter) {
return alwaysTrue();
}
if (typeof filter == 'function') {
return filter;
}
var types = filter;
var exclusive;
if ('length' in filter) {
exclusive = !exclude;
types = {};
for (var i = 0; i < filter.length; i++) {
types[filter[i]] = true;
}
} else {
exclusive = exclude;
exclude = false;
}
return function(token, index, array) {
if (token.subType) {
var sub = token.type + '.' + token.subType;
if (sub in types) {
return types[sub] != exclude;
}
}
if (token.type in types) {
return types[token.type] != exclude;
} else {
return !exclusive;
}
}
}
/**
* Завершает токенизацию, возвращая список токенов.
*
* Эта и другие функции принимают последними параметрами filter и флаг exclude. Они
* служат для фильтрации токенов (потому что часто удобнее получать не все токены, а
* только определенную часть из них).
*
* Если в filter передана функция, то параметр exclude игнорируется, а filter вызывается
* аналогично коллбэку в методе Array.prototype.filter: ей передаются параметры
* token, index, array (текущий токен, его индекс и общий список токенов). Будут
* возвращены только токены, для которых функция вернет истинное значение.
*
* Если в filter передан массив (или объект с полем length), то возвращаются токены, типы
* которых либо входят в этот массив (exclude=false), либо не входят в него (exclude=true).
* Вместо типов можно использовать строки вида 'WORD.LATIN' (тип, символ «точка» и подтип).
*
* Если в filter передать объект, то ключами в нём должны быть типы токенов, а значениями -
* true или false в зависимости от того, включать такие токены в ответ или нет. Как и в случае случае
* с массивом, в качестве ключей можно использовать строки вида 'WORD.LATIN'.
* Здесь параметр exclude означает, следует ли ограничить выдачу только теми типами, которые
* перечислены в filter.
* Значения с указанием подтипа имеют больший приоритет, чем просто типы. Благодаря этому можно
* делать такие странные вещи:
*
* ```
* t.done({ 'WORD': false, 'WORD.LATIN': true }, false);
* ```
* (то есть вернуть все теги, кроме тегов с типом WORD, но включить теги с подтипом LATIN)
*
* @param {Function|String[]|Object} [filter] Типы токенов, по которым нужно
* отфильтровать результат (или функция для фильтрации).
* @param {boolean} [exclude=False] Инвертирует фильтр, т.е. возвращаются
* токены со всеми типами, за исключением перечисленных в filter.
* @returns {Token[]} Список токенов после фильтрации.
*/
Tokens.prototype.done = function(filter, exclude) {
// Finalize tokenizing, return list of tokens
// For now it just returns tokens, in the future there could be some additional work
if (!filter) {
return this.tokens;
}
var matcher = getMatcher(filter, exclude);
var list = [];
for (var i = 0; i < this.tokens.length; i++) {
if (matcher(this.tokens[i], i, this.tokens)) {
list.push(this.tokens[i]);
}
}
return list;
}
/**
* Подсчитывает текущее количество токенов.
*
* @param {Function|String[]|Object} [filter] См. описание метода done.
* @param {boolean} [exclude=False] См. описание метода done.
* @returns {Number} Число токенов после фильтрации.
*/
Tokens.prototype.count = function(filter, exclude) {
if (!filter) {
return this.tokens.length;
}
var matcher = getMatcher(filter, exclude);
var count = 0;
for (var i = 0; i < this.tokens.length; i++) {
if (matcher(this.tokens[i], i, this.tokens)) {
count++;
}
}
return count;
}
/**
* Получает следующий токен относительно текущей позиции.
*
* @param {boolean} moveIndex Следует ли переместить указатель к
* следующему токену (в противном случае следующий вызов nextToken вернет
* тот же результат)
* @param {Function|String[]|Object} [filter] См. описание метода done.
* @param {boolean} [exclude=False] См. описание метода done.
* @returns {Token|null} Следующий токен или null, если подходящих токенов
* впереди нет.
*/
Tokens.prototype.nextToken = function(moveIndex, filter, exclude) {
var matcher = getMatcher(filter, exclude);
var index = this.index;
index++;
while (index < this.tokens.length && matcher(this.tokens[index], index, this.tokens)) {
index++;
}
if (index < this.tokens.length) {
if (moveIndex) {
this.index = index;
}
return this.tokens[index];
}
return null;
}
/**
* Проверяет, является ли следующий (за исключением пробелов) токен знаком
* препинания или нет.
*
* @returns {Token|False} False, если следующий токен не является знаком
* препинания, либо сам токен в противном случае.
*/
Tokens.prototype.punctAhead = function() {
var token = this.nextToken(false, ['SPACE'], true);
return token && token.type == 'PUNCT' && token;
}
/**
* Получает предыдущий токен относительно текущей позиции.
*
* @param {boolean} moveIndex Следует ли переместить указатель к
* предыдущему токену (в противном случае следующий вызов prevToken вернет
* тот же результат)
* @param {Function|String[]|Object} [filter] См. описание метода done.
* @param {boolean} [exclude=False] См. описание метода done.
* @returns {Token|null} Предыдущий токен или null, если подходящих токенов
* позади нет.
*/
Tokens.prototype.prevToken = function(moveIndex, filter, exclude) {
var matcher = getMatcher(filter, exclude);
var index = this.index;
index--;
while (index >= 0 && matcher(this.tokens[index], index, this.tokens)) {
index--;
}
if (index >= 0) {
if (moveIndex) {
this.index = index;
}
return this.tokens[index];
}
return null;
}
/**
* Проверяет, является ли предыдущий (за исключением пробелов) токен знаком
* препинания или нет.
*
* @returns {Token|False} False, если предыдущий токен не является знаком
* препинания, либо сам токен в противном случае.
*/
Tokens.prototype.punctBehind = function() {
var token = this.prevToken(false, ['SPACE'], true);
return token && token.type == 'PUNCT' && token;
}
/**
* Проверяет, есть ли впереди текущей позиции токены, удовлетворяющие фильтру.
*
* @param {Function|String[]|Object} [filter] См. описание метода done.
* @param {boolean} [exclude=False] См. описание метода done.
* @returns {boolean} True если впереди есть хотя бы один подходящий токен,
* и False в противном случае.
*/
Tokens.prototype.hasTokensAhead = function(filter, exclude) {
return this.nextToken(false, filter, exclude) != null;
}
/**
* Проверяет, есть ли позади текущей позиции токены, удовлетворяющие фильтру.
*
* @param {Function|String[]|Object} [filter] См. описание метода done.
* @param {boolean} [exclude=False] См. описание метода done.
* @returns {boolean} True если позади есть хотя бы один подходящий токен,
* и False в противном случае.
*/
Tokens.prototype.hasTokensBehind = function(filter, exclude) {
return this.prevToken(false, filter, exclude) != null;
}
return Tokens;
}));