From 79a85ce5b6b225bf351672a7d1552d6b10cc9711 Mon Sep 17 00:00:00 2001 From: Kevin Lin Date: Sat, 21 Sep 2019 01:27:51 +0800 Subject: [PATCH] Migrate to typescript (#6) * Add typescript-related packages * "@babel/preset-typescript" and "typescript" to build src * "ts-node" to run unit test * Add types for function parameters * Add class `Article`, `Board` * prevent sending 0 length message * Move .babelrc to babel.config.js --- .babelrc | 10 -- babel.config.js | 18 +++ package.json | 17 ++- src/config.ts | 3 + src/index.js | 2 - src/index.ts | 4 + src/sites/ptt/Article.ts | 47 +++++++ src/sites/ptt/Board.ts | 45 ++++++ src/sites/ptt/{bot.js => bot.ts} | 182 ++++++++++--------------- src/sites/ptt/{config.js => config.ts} | 4 +- src/sites/ptt/index.js | 4 - src/sites/ptt/index.ts | 3 + src/sites/ptt/{parser.js => parser.ts} | 0 src/{socket.js => socket.ts} | 16 ++- src/utils/char.ts | 70 ++++++++++ src/utils/{decode.js => decode.ts} | 0 src/utils/{encode.js => encode.ts} | 0 src/utils/index.js | 4 - src/utils/index.ts | 4 + src/utils/{keymap.js => keymap.ts} | 0 test/{articles.js => articles.ts} | 0 test/{connection.js => connection.ts} | 0 test/{favorite.js => favorite.ts} | 0 test/global.ts | 10 ++ test/{index.js => index.ts} | 8 +- test/mocha.opts | 1 + tsconfig.json | 25 ++++ tsconfig.test.json | 13 ++ yarn.lock | 72 +++++++++- 29 files changed, 419 insertions(+), 143 deletions(-) delete mode 100644 .babelrc create mode 100644 babel.config.js create mode 100644 src/config.ts delete mode 100644 src/index.js create mode 100644 src/index.ts create mode 100644 src/sites/ptt/Article.ts create mode 100644 src/sites/ptt/Board.ts rename src/sites/ptt/{bot.js => bot.ts} (73%) rename src/sites/ptt/{config.js => config.ts} (80%) delete mode 100644 src/sites/ptt/index.js create mode 100644 src/sites/ptt/index.ts rename src/sites/ptt/{parser.js => parser.ts} (100%) rename src/{socket.js => socket.ts} (78%) create mode 100644 src/utils/char.ts rename src/utils/{decode.js => decode.ts} (100%) rename src/utils/{encode.js => encode.ts} (100%) delete mode 100644 src/utils/index.js create mode 100644 src/utils/index.ts rename src/utils/{keymap.js => keymap.ts} (100%) rename test/{articles.js => articles.ts} (100%) rename test/{connection.js => connection.ts} (100%) rename test/{favorite.js => favorite.ts} (100%) create mode 100644 test/global.ts rename test/{index.js => index.ts} (55%) create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json diff --git a/.babelrc b/.babelrc deleted file mode 100644 index b8f4633..0000000 --- a/.babelrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "presets": [ - "@babel/env", - ], - "plugins": [ - "@babel/plugin-proposal-export-default-from", - ["@babel/plugin-proposal-class-properties", { "loose": false }], - ["@babel/plugin-transform-runtime", { "regenerator": true }] - ] -} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..9595503 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,18 @@ +"use strict"; + +module.exports = function(api) { + api.cache(true); + const config = { + presets: [ + "@babel/env", + "@babel/preset-typescript" + ], + plugins: [ + "@babel/plugin-proposal-export-default-from", + ["@babel/plugin-proposal-class-properties", { loose: false }], + "@babel/plugin-proposal-object-rest-spread", + ["@babel/plugin-transform-runtime", { regenerator: true }], + ] + } + return config; +} diff --git a/package.json b/package.json index fe7d5b7..a5e3f8b 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "version": "0.8.1", "description": "A Node client for fetching data from ptt.cc.", "main": "./dist/index.js", + "types": "./dist/index.d.js", "scripts": { - "test": "mocha ./test/index.js", - "build": "$npm_execpath run build:babel", - "build:babel": "cross-env NODE_ENV=production babel ./src --out-dir ./dist" + "type:check": "tsc", + "type:declaration": "tsc --noEmit false --emitDeclarationOnly --declaration --declarationDir ./dist", + "test": "cross-env TS_NODE_PROJECT=tsconfig.test.json mocha ./test/index.ts", + "build": "$npm_execpath run build:babel && $npm_execpath run type:declaration", + "build:babel": "cross-env NODE_ENV=production babel ./src --out-dir ./dist --extensions '.ts'" }, "repository": { "type": "git", @@ -38,10 +41,16 @@ "@babel/node": "^7.2.2", "@babel/plugin-proposal-class-properties": "^7.4.0", "@babel/plugin-proposal-export-default-from": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.5", "@babel/plugin-transform-runtime": "^7.4.0", "@babel/preset-env": "^7.4.2", + "@babel/preset-typescript": "^7.3.3", "@babel/register": "^7.4.0", + "@types/mocha": "^5.2.7", + "@types/node": "^12.7.1", "cross-env": "^5.2.0", - "mocha": "^6.1.4" + "mocha": "^6.1.4", + "ts-node": "^8.3.0", + "typescript": "^3.5.3" } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..4837366 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,3 @@ +export default interface Config { + [key: string]: any; +}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 102a4b9..0000000 --- a/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export default from './sites/ptt'; -export ptt from './sites/ptt'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7f610d8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export { default as default } from './sites/ptt'; +export { default as ptt } from './sites/ptt'; +export { default as Config } from './config'; +export { default as Socket } from './socket'; diff --git a/src/sites/ptt/Article.ts b/src/sites/ptt/Article.ts new file mode 100644 index 0000000..b8797aa --- /dev/null +++ b/src/sites/ptt/Article.ts @@ -0,0 +1,47 @@ +import {substrWidth} from '../../utils/char'; + +export class Article { + boardname: string; + sn: number; + push: string; + date: string; + timestamp: string; + author: string; + status: string; + title: string; + fixed: boolean; + private _content: string[] = []; + + constructor() { + } + + static fromLine(line: string): Article { + let article = new Article(); + article.sn =+substrWidth('dbcs', line, 1, 7).trim(); + article.push = substrWidth('dbcs', line, 9, 2).trim(); + article.date = substrWidth('dbcs', line, 11, 5).trim(); + article.author = substrWidth('dbcs', line, 17, 12).trim(); + article.status = substrWidth('dbcs', line, 30, 2).trim(); + article.title = substrWidth('dbcs', line, 32 ).trim(); + article.fixed = substrWidth('dbcs', line, 1, 7).trim().includes('★'); + return article; + } + + get content(): ReadonlyArray { + return this._content; + } + set content(data: ReadonlyArray) { + this._content = data.slice(); + } + /** + * @deprecated + */ + get lines(): ReadonlyArray { + return this.content; + } + set lines(data: ReadonlyArray) { + this.content = data; + } +}; + +export default Article; diff --git a/src/sites/ptt/Board.ts b/src/sites/ptt/Board.ts new file mode 100644 index 0000000..88babdf --- /dev/null +++ b/src/sites/ptt/Board.ts @@ -0,0 +1,45 @@ +import {substrWidth} from '../../utils/char'; + +export class Board { + boardname: string; + bn: number; + read: boolean; + category: string; + title: string; + users: string; + admin: string; + folder: boolean = false; + divider: boolean = false; + + constructor() { + } + + static fromLine(line: string): Board { + let board = new Board(); + board.bn =+substrWidth('dbcs', line, 3, 4).trim(); + board.read = substrWidth('dbcs', line, 8, 2).trim() === ''; + board.boardname = substrWidth('dbcs', line, 10, 12).trim(); + board.category = substrWidth('dbcs', line, 23, 4).trim(); + switch (board.boardname) { + case 'MyFavFolder': + board.title = substrWidth('dbcs', line, 30); + board.users = ''; + board.admin = ''; + board.folder = true; + break; + case '------------': + board.title = substrWidth('dbcs', line, 30); + board.users = ''; + board.admin = ''; + board.divider = true; + break; + default: + board.title = substrWidth('dbcs', line, 30, 31); + board.users = substrWidth('dbcs', line, 62, 5).trim(); + board.admin = substrWidth('dbcs', line, 67 ).trim(); + break; + } + return board; + } + +}; diff --git a/src/sites/ptt/bot.js b/src/sites/ptt/bot.ts similarity index 73% rename from src/sites/ptt/bot.js rename to src/sites/ptt/bot.ts index 2a9229c..7d29b09 100644 --- a/src/sites/ptt/bot.js +++ b/src/sites/ptt/bot.ts @@ -1,23 +1,29 @@ import EventEmitter from 'eventemitter3'; import sleep from 'sleep-promise'; import Terminal from 'terminal.js'; -import decode from '../../utils/decode'; -import encode from '../../utils/encode'; -import key from '../../utils/keymap'; +import Socket from '../../socket'; +import { + decode, + encode, + keymap as key, +} from '../../utils'; import { getWidth, indexOfWidth, substrWidth, } from '../../utils/char'; +import Config from '../../config'; import defaultConfig from './config'; +import {Article} from './Article'; +import {Board} from './Board'; -class Condition{ - typeWord; - criteria; +class Condition { + private typeWord: string; + private criteria: string; - constructor(type, criteria){ + constructor(type: 'push'|'author'|'title', criteria: string) { switch (type) { case 'push': this.typeWord = 'Z'; @@ -34,7 +40,7 @@ class Condition{ this.criteria = criteria; } - toSearchString() { + toSearchString(): string { return `${this.typeWord}${this.criteria}`; } } @@ -58,35 +64,37 @@ class Bot extends EventEmitter { this.conditions.push(new Condition(type, criteria)); } }; + + private config: Config; + private term: Terminal; + private _state: any; + private currentCharset: string; + private socket: Socket; + private preventIdleHandler: ReturnType; - constructor(config) { + constructor(config?: Config) { super(); this.config = {...defaultConfig, ...config}; this.init(); } - init() { + async init(): Promise { const { config } = this; - this._term = new Terminal(config.terminal); + this.term = new Terminal(config.terminal); this._state = { ...Bot.initialState }; - this._term.state.setMode('stringWidth', 'dbcs'); + this.term.state.setMode('stringWidth', 'dbcs'); this.currentCharset = 'big5'; - let Socket; switch (config.protocol.toLowerCase()) { case 'websocket': case 'ws': case 'wss': - Socket = require("../../socket").default; break; case 'telnet': case 'ssh': default: - Socket = null; - } - - if (Socket === null) { - throw `Invalid protocol: ${config.protocol}`; + throw `Invalid protocol: ${config.protocol}`; + break; } const socket = new Socket(config); @@ -112,20 +120,20 @@ class Bot extends EventEmitter { this.currentCharset = this.config.charset; } const msg = decode(data, this.currentCharset); - this._term.write(msg); - this.emit('redraw', this._term.toString()); + this.term.write(msg); + this.emit('redraw', this.term.toString()); }) .on('error', (err) => { }); this.socket = socket; } - get state() { + get state(): any { return {...this._state}; } getLine = (n) => { - return this._term.state.getLine(n); + return this.term.state.getLine(n); }; async getLines() { @@ -158,19 +166,26 @@ class Bot extends EventEmitter { return lines; } - async send(msg) { + send(msg: string): Promise { this.config.preventIdleTimeout && this.preventIdle(this.config.preventIdleTimeout); - return new Promise(resolve => { + return new Promise((resolve, reject) => { if (this.state.connect) { - this.socket.send(encode(msg, this.currentCharset)); - this.once('message', msg => { - resolve(msg); - }); + if (msg.length > 0) { + this.socket.send(encode(msg, this.currentCharset)); + this.once('message', msg => { + resolve(msg); + }); + } else { + console.warn(`Sending message with 0-length`); + resolve(); + } + } else { + reject(); } }); } - preventIdle(timeout) { + preventIdle(timeout: number): void { clearTimeout(this.preventIdleHandler); if (this.state.login) { this.preventIdleHandler = setTimeout(async () => { @@ -180,7 +195,7 @@ class Bot extends EventEmitter { } } - async login(username, password, kick=true) { + async login(username: string, password: string, kick: boolean=true): Promise { if (this.state.login) return; username = username.replace(/,/g, ''); if (this.config.charset === 'utf8') { @@ -188,7 +203,7 @@ class Bot extends EventEmitter { } await this.send(`${username}${key.Enter}${password}${key.Enter}`); let ret; - while ((ret = await this._checkLogin(kick)) === null) { + while ((ret = await this.checkLogin(kick)) === null) { await sleep(400); } if (ret) { @@ -203,7 +218,7 @@ class Bot extends EventEmitter { return ret; } - async logout() { + async logout(): Promise { if (!this.state.login) return; await this.send(`G${key.Enter}Y${key.Enter.repeat(2)}`); this._state.login = false; @@ -211,7 +226,7 @@ class Bot extends EventEmitter { return true; } - async _checkLogin(kick) { + private async checkLogin(kick: boolean): Promise { const { getLine } = this; if (getLine(21).str.includes("密碼不對或無此帳號")) { @@ -239,48 +254,40 @@ class Bot extends EventEmitter { return null; } - _checkArticleWithHeader() { + private checkArticleWithHeader(): boolean { const authorArea = substrWidth('dbcs', this.getLine(0).str, 0, 6).trim(); return authorArea === "作者"; } - setSearchCondition(type, criteria) { + setSearchCondition(type: string, criteria: string): void { this.searchCondition.add(type, criteria); } - resetSearchCondition() { + resetSearchCondition(): void { this.searchCondition.init(); } - isSearchConditionSet() { + isSearchConditionSet(): boolean { return (this.searchCondition.conditions.length !== 0); } - async getArticles(boardname, offset=0) { + async getArticles(boardname: string, offset: number=0): Promise { await this.enterBoard(boardname); if (this.isSearchConditionSet()){ let searchString = this.searchCondition.conditions.map(condition => condition.toSearchString()).join(key.Enter); await this.send(`${searchString}${key.Enter}`); } - offset |= 0; if (offset > 0) { offset = Math.max(offset-9, 1); await this.send(`${key.End}${key.End}${offset}${key.Enter}`); } const { getLine } = this; - let articles = []; + let articles: Article[] = []; for(let i=3; i<=22; i++) { const line = getLine(i).str; - const article = { - sn: substrWidth('dbcs', line, 1, 7).trim() | 0, - push: substrWidth('dbcs', line, 9, 2).trim(), - date: substrWidth('dbcs', line, 11, 5).trim(), - author: substrWidth('dbcs', line, 17, 12).trim(), - status: substrWidth('dbcs', line, 30, 2).trim(), - title: substrWidth('dbcs', line, 32 ).trim(), - fixed: substrWidth('dbcs', line, 1, 7).trim().includes('★'), - }; + const article = Article.fromLine(line); + article.boardname = boardname; articles.push(article); } // fix sn @@ -299,7 +306,7 @@ class Bot extends EventEmitter { return articles.reverse(); } - async getArticle(boardname, sn) { + async getArticle(boardname: string, sn: number, article: Article = new Article()): Promise
{ await this.enterBoard(boardname); if (this.isSearchConditionSet()){ let searchString = this.searchCondition.conditions.map(condition => condition.toSearchString()).join(key.Enter); @@ -309,17 +316,12 @@ class Bot extends EventEmitter { await this.send(`${sn}${key.Enter}${key.Enter}`); - const hasHeader = this._checkArticleWithHeader(); + const hasHeader = this.checkArticleWithHeader(); - let article = { - sn, - author: "", - title: "", - timestamp: "", - lines: [], - }; + article.sn = sn; + article.boardname = boardname; - if (this._checkArticleWithHeader()) { + if (hasHeader) { article.author = substrWidth('dbcs', getLine(0).str, 7, 50).trim(); article.title = substrWidth('dbcs', getLine(1).str, 7 ).trim(); article.timestamp = substrWidth('dbcs', getLine(2).str, 7 ).trim(); @@ -331,17 +333,14 @@ class Bot extends EventEmitter { return article; } - async getFavorite(offsets=[]) { - if (typeof offsets === "string") { - offsets |= 0; - } + async getFavorite(offsets: number|number[]=[]) { if (typeof offsets === "number") { offsets = [offsets]; } await this.enterFavorite(offsets); const { getLine } = this; - const favorites = []; + const favorites: Board[] = []; while (true) { let stopLoop = false; @@ -351,43 +350,11 @@ class Bot extends EventEmitter { stopLoop = true; break; } - let favorite = { - bn: substrWidth('dbcs', line, 3, 4).trim() | 0, - read: substrWidth('dbcs', line, 8, 2).trim() === '', - boardname: substrWidth('dbcs', line, 10, 12).trim(), - category: substrWidth('dbcs', line, 23, 4).trim(), - title: substrWidth('dbcs', line, 30, 31), - users: substrWidth('dbcs', line, 62, 5).trim(), - admin: substrWidth('dbcs', line, 67 ).trim(), - folder: false, - divider: false, - }; + let favorite = Board.fromLine(line); if (favorite.bn !== favorites.length + 1) { stopLoop = true; break; } - switch (favorite.boardname) { - case 'MyFavFolder': - favorite = { - ...favorite, - title: substrWidth('dbcs', line, 30), - users: '', - admin: '', - folder: true, - }; - break; - case '------------': - favorite = { - ...favorite, - title: substrWidth('dbcs', line, 30), - users: '', - admin: '', - divider: true, - }; - break; - default: - break; - } favorites.push(favorite); } if (stopLoop) { @@ -400,9 +367,8 @@ class Bot extends EventEmitter { return favorites; } - async getMails(offset=0) { + async getMails(offset: number=0) { await this.enterMail(); - offset |= 0; if (offset > 0) { offset = Math.max(offset-9, 1); await this.send(`${key.End}${key.End}${offset}${key.Enter}`); @@ -414,7 +380,7 @@ class Bot extends EventEmitter { for(let i=3; i<=22; i++) { const line = getLine(i).str; const mail = { - sn: substrWidth('dbcs', line, 1, 5).trim() | 0, + sn: +substrWidth('dbcs', line, 1, 5).trim(), date: substrWidth('dbcs', line, 9, 5).trim(), author: substrWidth('dbcs', line, 15, 12).trim(), status: substrWidth('dbcs', line, 30, 2).trim(), @@ -427,13 +393,13 @@ class Bot extends EventEmitter { return mails.reverse(); } - async getMail(sn) { + async getMail(sn: number) { await this.enterMail(); const { getLine } = this; await this.send(`${sn}${key.Enter}${key.Enter}`); - const hasHeader = this._checkArticleWithHeader(); + const hasHeader = this.checkArticleWithHeader(); let mail = { sn, @@ -443,7 +409,7 @@ class Bot extends EventEmitter { lines: [], }; - if (this._checkArticleWithHeader()) { + if (this.checkArticleWithHeader()) { mail.author = substrWidth('dbcs', getLine(0).str, 7, 50).trim(); mail.title = substrWidth('dbcs', getLine(1).str, 7 ).trim(); mail.timestamp = substrWidth('dbcs', getLine(2).str, 7 ).trim(); @@ -455,12 +421,12 @@ class Bot extends EventEmitter { return mail; } - async enterIndex() { + async enterIndex(): Promise { await this.send(`${key.ArrowLeft.repeat(10)}`); return true; } - async enterBoard(boardname) { + async enterBoard(boardname: string): Promise { await this.send(`s${boardname}${key.Enter} ${key.Home}${key.End}`); boardname = boardname.toLowerCase(); const { getLine } = this; @@ -476,14 +442,14 @@ class Bot extends EventEmitter { return false; } - async enterFavorite(offsets=[]) { + async enterFavorite(offsets: number[]=[]): Promise { const enterOffsetMessage = offsets.map(offset => `${offset}${key.Enter.repeat(2)}`).join(); await this.send(`F${key.Enter}${key.Home}${enterOffsetMessage}`); return true; } - async enterMail() { + async enterMail(): Promise { await this.send(`M${key.Enter}R${key.Enter}${key.Home}${key.End}`); return true; } diff --git a/src/sites/ptt/config.js b/src/sites/ptt/config.ts similarity index 80% rename from src/sites/ptt/config.js rename to src/sites/ptt/config.ts index 62d8e32..2a50755 100644 --- a/src/sites/ptt/config.js +++ b/src/sites/ptt/config.ts @@ -1,4 +1,6 @@ -const config = { +import Config from '../../config'; + +const config: Config = { name: 'PTT', url: 'wss://ws.ptt.cc/bbs', charset: 'utf8', diff --git a/src/sites/ptt/index.js b/src/sites/ptt/index.js deleted file mode 100644 index e6f3aeb..0000000 --- a/src/sites/ptt/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export default from './bot'; -export bot from './bot'; -export config from './config'; - diff --git a/src/sites/ptt/index.ts b/src/sites/ptt/index.ts new file mode 100644 index 0000000..c32224f --- /dev/null +++ b/src/sites/ptt/index.ts @@ -0,0 +1,3 @@ +export { default as default } from './bot'; +export { default as bot } from './bot'; +export { default as config } from './config'; diff --git a/src/sites/ptt/parser.js b/src/sites/ptt/parser.ts similarity index 100% rename from src/sites/ptt/parser.js rename to src/sites/ptt/parser.ts diff --git a/src/socket.js b/src/socket.ts similarity index 78% rename from src/socket.js rename to src/socket.ts index 1e2f029..d509e4a 100644 --- a/src/socket.js +++ b/src/socket.ts @@ -1,19 +1,23 @@ import EventEmitter from 'eventemitter3'; +import Config from './config'; class Socket extends EventEmitter { + private _config: Config; + private _socket: WebSocket; + constructor(config) { super(); this._config = config; } - connect() { + connect(): void { let socket; if (typeof WebSocket === 'undefined') { throw new Error(`'WebSocket' is undefined. Do you include any websocket polyfill?`); } else if (WebSocket.length === 1) { socket = new WebSocket(this._config.url); } else { - const options = {}; + const options: any = {}; if (this._config.origin) options.origin = this._config.origin; socket = new WebSocket(this._config.url, options); @@ -32,20 +36,20 @@ class Socket extends EventEmitter { this.emit('message', data); data = []; }, this._config.timeout); - if (data.byteLength > this._config.blobSize) { - throw new Error(`Receive message length(${data.byteLength}) greater than buffer size(${this._config.blobSize})`); + if (currData.byteLength > this._config.blobSize) { + throw new Error(`Receive message length(${currData.byteLength}) greater than buffer size(${this._config.blobSize})`); } }); this._socket = socket; } - disconnect() { + disconnect(): void { const socket = this._socket; socket.close(); } - send(str) { + send(str: string): void { const socket = this._socket; if (socket.readyState == 1 /* OPEN */) { socket.send(str); diff --git a/src/utils/char.ts b/src/utils/char.ts new file mode 100644 index 0000000..1cb0316 --- /dev/null +++ b/src/utils/char.ts @@ -0,0 +1,70 @@ +import wcwidth from 'wcwidth'; + +export function dbcswidth(str: string): number { + return str.split("").reduce(function(sum, c) { + return sum + (c.charCodeAt(0) > 255 ? 2 : 1); + }, 0); +} +/** +* calculate width of string. +* @params {string} widthType - calculate width by wcwidth or String.length +* @params {string} str - string to calculate +*/ +export function getWidth(widthType: string, str: string): number { + switch (widthType) { + case 'length': + return str.length; + case 'wcwidth': + return wcwidth(str); + case 'dbcs': + return dbcswidth(str); + default: + throw `Invalid widthType "${widthType}"`; + } +} + +/** +* calculate the position that the prefix of string is a specific width +* @params {string} widthType - calculate width by wcwidth or String.length +* @params {string} str - string to calculate +* @params {number} width - the width of target string +*/ +export function indexOfWidth(widthType: string, str: string, width?: number): number { + if (widthType === 'length') + return getWidth(widthType, str); + for (var i = 0; i <= str.length; i++) { + if (getWidth(widthType, str.substr(0, i)) > width) + return i - 1; + } + return str.length; +} + +/** +* extract parts of string, beginning at the character at the specified position, +* and returns the specified width of characters. if the character is incomplete, +* it will be replaced by space. +* @params {string} widthType - calculate width by wcwidth or String.length +* @params {string} str - string to calculate +* @params {number} startWidth - the beginning position of string +* @params {number} width - the width of target string +*/ +export function substrWidth(widthType: string, str: string, startWidth: number, width?: number): string { + var ignoreWidth = typeof width === 'undefined'; + var length = width; + var start = startWidth; + var prefixSpace = 0, suffixSpace = 0; + if (widthType !== 'length') { + start = indexOfWidth(widthType, str, startWidth); + if (getWidth(widthType, str.substr(0, start)) < startWidth) { + start += 1; + prefixSpace = Math.max(getWidth(widthType, str.substr(0, start)) - startWidth, 0); + } + if (!ignoreWidth) { + length = indexOfWidth(widthType, str.substr(start), width - prefixSpace); + suffixSpace = Math.min(width, getWidth(widthType, str.substr(start))) - + (prefixSpace + getWidth(widthType, str.substr(start, length))); + } + } + var substr = ignoreWidth ? str.substr(start) : str.substr(start, length); + return " ".repeat(prefixSpace) + substr + " ".repeat(suffixSpace); +} diff --git a/src/utils/decode.js b/src/utils/decode.ts similarity index 100% rename from src/utils/decode.js rename to src/utils/decode.ts diff --git a/src/utils/encode.js b/src/utils/encode.ts similarity index 100% rename from src/utils/encode.js rename to src/utils/encode.ts diff --git a/src/utils/index.js b/src/utils/index.js deleted file mode 100644 index 063d65e..0000000 --- a/src/utils/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export char from './char'; -export decode from './decode'; -export encode from './encode'; -export keymap from './keymap'; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..af3887e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +export { default as char } from './char'; +export { default as decode } from './decode'; +export { default as encode } from './encode'; +export { default as keymap } from './keymap'; diff --git a/src/utils/keymap.js b/src/utils/keymap.ts similarity index 100% rename from src/utils/keymap.js rename to src/utils/keymap.ts diff --git a/test/articles.js b/test/articles.ts similarity index 100% rename from test/articles.js rename to test/articles.ts diff --git a/test/connection.js b/test/connection.ts similarity index 100% rename from test/connection.js rename to test/connection.ts diff --git a/test/favorite.js b/test/favorite.ts similarity index 100% rename from test/favorite.js rename to test/favorite.ts diff --git a/test/global.ts b/test/global.ts new file mode 100644 index 0000000..94544f1 --- /dev/null +++ b/test/global.ts @@ -0,0 +1,10 @@ +export {}; + +declare global { + namespace NodeJS { + interface Global { + WebSocket: any; + } + } +} + diff --git a/test/index.js b/test/index.ts similarity index 55% rename from test/index.js rename to test/index.ts index 5cd441f..8f2bd79 100644 --- a/test/index.js +++ b/test/index.ts @@ -1,9 +1,11 @@ +import './global'; + global.WebSocket = require('ws'); const tests = [ - './connection.js', - './articles.js', - './favorite.js', + './connection.ts', + './articles.ts', + './favorite.ts', ]; tests.forEach(test => { diff --git a/test/mocha.opts b/test/mocha.opts index 6c0bd45..bc44bf2 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ +--require ts-node/register --require @babel/register --reporter spec --timeout 30000 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c0668b6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "esnext", + "moduleResolution": "node", + "module": "commonjs", + "esModuleInterop": true, + "experimentalDecorators": true, + "noEmit": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ], + "typedocOptions": { + "mode": "modules", + "module": "commonjs", + "target": "ES6", + "out": "docs", + "exclude": [ + "src/**/index.ts" + ] + } +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..b53a251 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "esnext", + "moduleResolution": "node", + "module": "commonjs", + "esModuleInterop": true, + "experimentalDecorators": true, + "noEmit": true + }, + "include": [ + "test/*.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index f694e0a..79d8c5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -367,6 +367,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-typescript@^7.2.0": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991" + integrity sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -614,6 +621,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.3.2": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.5.5.tgz#6d862766f09b2da1cb1f7d505fe2aedab6b7d4b8" + integrity sha512-pehKf4m640myZu5B2ZviLaiBlxMCjSZ1qTEO459AXKX5GnPueyulJeCqZFs1nz/Ya2dDzXQ1NxZ/kKNWyD4h6w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.5.5" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.2.0" + "@babel/plugin-transform-unicode-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" @@ -687,6 +703,14 @@ js-levenshtein "^1.1.3" semver "^5.5.0" +"@babel/preset-typescript@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz#88669911053fa16b2b276ea2ede2ca603b3f307a" + integrity sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.3.2" + "@babel/register@^7.4.0", "@babel/register@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.5.5.tgz#40fe0d474c8c8587b28d6ae18a03eddad3dac3c1" @@ -739,6 +763,16 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@types/mocha@^5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + +"@types/node@^12.7.1": + version "12.7.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.1.tgz#3b5c3a26393c19b400844ac422bd0f631a94d69d" + integrity sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -792,6 +826,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" + integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1191,6 +1230,11 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + electron-to-chromium@^1.3.191: version "1.3.219" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.219.tgz#b8bc7c72fc6d5d5eeee57288eba4bfec5f070fa8" @@ -1853,6 +1897,11 @@ make-dir@^2.0.0: pify "^4.0.1" semver "^5.6.0" +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + map-age-cleaner@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -2603,7 +2652,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.9: +source-map-support@^0.5.6, source-map-support@^0.5.9: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== @@ -2776,6 +2825,22 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +ts-node@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.3.0.tgz#e4059618411371924a1fb5f3b125915f324efb57" + integrity sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +typescript@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" + integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== + uao-js@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uao-js/-/uao-js-1.0.1.tgz#e28647fdb1b78b90ce6a011237431c51cee99eea" @@ -2972,3 +3037,8 @@ yargs@^12.0.5: which-module "^2.0.0" y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" + +yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==