Comparing version 1.2.3 to 1.2.4
103
.eslintrc.js
module.exports = { | ||
"env": { | ||
"commonjs": true, | ||
"es6": true, | ||
"node": true | ||
}, | ||
"extends": "eslint:recommended", | ||
"globals": { | ||
"Atomics": "readonly", | ||
"SharedArrayBuffer": "readonly" | ||
}, | ||
"parserOptions": { | ||
"ecmaVersion": 2018 | ||
}, | ||
"rules": { | ||
"indent": [ | ||
"error", | ||
2, | ||
{ | ||
"SwitchCase": 1 | ||
} | ||
], | ||
"linebreak-style": [ | ||
"error", | ||
"unix" | ||
], | ||
"quotes": [ | ||
"error", | ||
"double" | ||
], | ||
"semi": [ | ||
"error", | ||
"never" | ||
], | ||
"brace-style": "error", | ||
"space-before-blocks": "error", | ||
"no-case-declarations": "off", | ||
"no-trailing-spaces": "error", | ||
"key-spacing": ["error", { | ||
"beforeColon": false, | ||
"afterColon": true | ||
}], | ||
"prefer-template": "error", | ||
"semi-spacing": ["error", {"before": false, "after": true}], | ||
"no-fallthrough": ["error", { "commentPattern": "caution: falls through" }] | ||
} | ||
"env": { | ||
"commonjs": true, | ||
"es6": true, | ||
"node": true | ||
}, | ||
"extends": "eslint:recommended", | ||
"globals": { | ||
"Atomics": "readonly", | ||
"SharedArrayBuffer": "readonly" | ||
}, | ||
"parserOptions": { | ||
"ecmaVersion": 2018 | ||
}, | ||
"rules": { | ||
"brace-style": "error", | ||
"comma-spacing": ["error", {"before": false, "after": true}], | ||
"indent": [ | ||
"error", | ||
2, | ||
{ | ||
"SwitchCase": 1 | ||
} | ||
], | ||
"linebreak-style": [ | ||
"error", | ||
"unix" | ||
], | ||
"key-spacing": ["error", { | ||
"beforeColon": false, | ||
"afterColon": true | ||
}], | ||
"no-case-declarations": "off", | ||
"no-trailing-spaces": "error", | ||
"prefer-template": "error", | ||
"semi": [ | ||
"error", | ||
"never" | ||
], | ||
"semi-spacing": ["error", {"before": false, "after": true}], | ||
"space-before-blocks": "error", | ||
"spaced-comment": ["error", "always", { "exceptions": ["-", "+"] }], | ||
"quotes": [ | ||
"error", | ||
"double" | ||
] | ||
} | ||
} |
{ | ||
"name": "smartwrap", | ||
"version": "1.2.3", | ||
"version": "1.2.4", | ||
"description": "Textwrap for javascript/nodejs. Correctly handles wide characters (宽字符) and emojis (😃). Wraps strings with option to break on words.", | ||
@@ -26,2 +26,4 @@ "main": "src/main.js", | ||
"breakword": "^1.0.5", | ||
"grapheme-splitter": "^1.0.4", | ||
"strip-ansi": "^6.0.0", | ||
"wcwidth": "^1.0.1", | ||
@@ -36,4 +38,5 @@ "yargs": "^15.1.0" | ||
"grunt-mocha-test": "^0.13.3", | ||
"husky": "^4.2.3", | ||
"mocha": "^5.2.0" | ||
} | ||
} |
344
src/main.js
@@ -1,17 +0,12 @@ | ||
"use strict" | ||
const breakword = require("breakword") | ||
const stripansi = require("strip-ansi") | ||
const wcwidth = require("wcwidth") | ||
let wcwidth = require("wcwidth") | ||
let breakword = require("breakword") | ||
function smartWrap(input, options) { | ||
//in case a template literal was passed that has newling characters, | ||
//split string by newlines and process each resulting string | ||
const str = input.toString() | ||
const strArr = str.split("\n").map( string => { | ||
return wrap(string, options) | ||
}) | ||
const ANSIPattern = [ | ||
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", | ||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))" | ||
].join("|") | ||
const ANSIRegex = new RegExp(ANSIPattern, "g") | ||
return strArr.join("\n") | ||
} | ||
const defaults = () => { | ||
@@ -21,21 +16,12 @@ let obj = {} | ||
obj.breakword = false | ||
obj.calculateSpaceRemaining = function(obj) { | ||
return Math.max(obj.lineLength - obj.spacesUsed - obj.paddingLeft - obj.paddingRight, 0) | ||
} //function to set starting line length | ||
obj.currentLine = 0 //index of current line in 'lines[]' | ||
obj.input = [] //input string split by whitespace | ||
obj.lines = [ | ||
[] | ||
] //assume at least one line | ||
obj.minWidth = 2 //fallback to if width set too narrow | ||
obj.input = [] // input string split by whitespace | ||
obj.minWidth = 2 // fallback to if width set too narrow | ||
obj.paddingLeft = 0 | ||
obj.paddingRight = 0 | ||
obj.errorChar = "�" | ||
obj.returnFormat = "string" //or 'array' | ||
obj.skipPadding = false //set to true when padding set too wide for line length | ||
obj.spacesUsed = 0 //spaces used so far on current line | ||
obj.splitAt = [" ","\t"] | ||
obj.returnFormat = "string" // or 'array' | ||
obj.skipPadding = false // set to true when padding set too wide for line length | ||
obj.splitAt = [" ", "\t"] | ||
obj.trim = true | ||
obj.width = 10 | ||
obj.words = [] | ||
@@ -45,124 +31,262 @@ return obj | ||
function wrap(text,options) { | ||
options = options || {} | ||
const calculateSpaceRemaining = function(lineLength, spacesUsed, config) { | ||
return Math.max(lineLength - spacesUsed - config.paddingLeft - config.paddingRight, 0) | ||
} // function to set starting line length | ||
if (options.errorChar) { | ||
const validateInput = (text, options) => { | ||
// options validation | ||
let config = Object.assign({}, defaults(), options || {}) | ||
if (config.errorChar) { | ||
// only allow a single errorChar | ||
options.errorChar = options.errorChar.split('')[0] | ||
config.errorChar = config.errorChar.split("")[0] | ||
// errorChar must not be wide character | ||
if (wcwidth(options.errorChar) > 1) | ||
throw new Error(`Error character cannot be a wide character (${options.errorChar})`) | ||
if (wcwidth(config.errorChar) > 1) | ||
throw new Error(`Error character cannot be a wide character (${config.errorChar})`) | ||
} | ||
let wrapObj = Object.assign({},defaults(),options) | ||
//make sure correct sign on padding | ||
wrapObj.paddingLeft = Math.abs(wrapObj.paddingLeft) | ||
wrapObj.paddingRight = Math.abs(wrapObj.paddingRight) | ||
// make sure correct sign on padding | ||
config.paddingLeft = Math.abs(config.paddingLeft) | ||
config.paddingRight = Math.abs(config.paddingRight) | ||
wrapObj.lineLength = wrapObj.width - | ||
wrapObj.paddingLeft - | ||
wrapObj.paddingRight | ||
let lineLength = config.width | ||
- config.paddingLeft | ||
- config.paddingRight | ||
if(wrapObj.lineLength < wrapObj.minWidth) { | ||
//skip padding if lineLength too narrow | ||
wrapObj.skipPadding = true | ||
wrapObj.lineLength = wrapObj.minWidth | ||
if(lineLength < config.minWidth) { | ||
// skip padding if lineLength too narrow | ||
config.skipPadding = true | ||
lineLength = config.minWidth | ||
} | ||
//Break input into array of characters split by whitespace and/or tabs | ||
let wordArray = [] | ||
//to trim or not to trim... | ||
let modifiedText = text.toString() | ||
if(wrapObj.trim) { | ||
modifiedText = modifiedText.trim() | ||
// to trim or not to trim... | ||
if(config.trim) { | ||
text = text.trim() | ||
} | ||
if(!wrapObj.breakword){ | ||
//break string into words | ||
if(wrapObj.splitAt.indexOf("\t")!==-1) { | ||
//split at both spaces and tabs | ||
wordArray = modifiedText.split(/ |\t/i) | ||
return { text, config, lineLength } | ||
} | ||
const wrap = (input, options) => { | ||
let { text, config, lineLength } = validateInput(input, options) | ||
// array of characters split by whitespace and/or tabs | ||
let words = [] | ||
if(!config.breakword) { | ||
// break string into words | ||
if(config.splitAt.indexOf("\t")!==-1) { | ||
// split at both spaces and tabs | ||
words = text.split(/ |\t/i) | ||
} else{ | ||
//split at whitespace | ||
wordArray = modifiedText.split(" ") | ||
// split at whitespace | ||
words = text.split(" ") | ||
} | ||
} else { | ||
// do not break string into words | ||
words = [text] | ||
} | ||
else { | ||
//do not break string into words | ||
wordArray = [modifiedText] | ||
} | ||
//remove empty array elements | ||
wrapObj.words = wordArray.filter(val => { | ||
// remove empty array elements | ||
words = words.filter(val => { | ||
if (val.length > 0) { | ||
return true | ||
return true | ||
} | ||
}) | ||
let spaceRemaining, splitIndex, word, wordlength | ||
// assume at least one line | ||
let lines = [ | ||
[] | ||
] | ||
while(wrapObj.words.length > 0) { | ||
spaceRemaining = wrapObj.calculateSpaceRemaining(wrapObj) | ||
word = wrapObj.words.shift() | ||
let spaceRemaining, splitIndex, word | ||
let currentLine = 0 // index of current line in 'lines[]' | ||
let spacesUsed = 0 // spaces used so far on current line | ||
while(words.length > 0) { | ||
spaceRemaining = calculateSpaceRemaining(lineLength, spacesUsed, config) | ||
word = words.shift() | ||
let wordLength = wcwidth(word) | ||
switch(true) { | ||
// Too long for an empty line and is a single character | ||
case(wrapObj.lineLength < wordLength && [...word].length === 1): | ||
wrapObj.words.unshift(wrapObj.errorChar) | ||
break | ||
// Too long for an empty line, must be broken between 2 lines | ||
case(wrapObj.lineLength < wordLength): | ||
//Break it, then re-insert its parts into wrapObj.words | ||
//so can loop back to re-handle each word | ||
splitIndex = breakword(word,wrapObj.lineLength) | ||
// too long for an empty line and is a single character | ||
case(lineLength < wordLength && [...word].length === 1): | ||
words.unshift(config.errorChar) | ||
break | ||
// too long for an empty line, must be broken between 2 lines | ||
case(lineLength < wordLength): | ||
// break it, then re-insert its parts into words | ||
// so can loop back to re-handle each word | ||
splitIndex = breakword(word, lineLength) | ||
let splitWord = [...word] | ||
wrapObj.words.unshift(splitWord.slice(0, splitIndex + 1).join("")) | ||
wrapObj.words.splice(1,0,splitWord.slice(splitIndex + 1).join("")) //+1 for substr fn | ||
words.unshift(splitWord.slice(0, splitIndex + 1).join("")) | ||
words.splice(1, 0, splitWord.slice(splitIndex + 1).join("")) // +1 for substr fn | ||
break | ||
// Not enough space remaining in line, must be wrapped to next line | ||
// not enough space remaining in line, must be wrapped to next line | ||
case(spaceRemaining < wordLength): | ||
//add a new line to our array of lines | ||
wrapObj.lines.push([]) | ||
//note carriage to new line in counter | ||
wrapObj.currentLine++ | ||
//reset the spacesUsed to 0 | ||
wrapObj.spacesUsed = 0 | ||
// add a new line to our array of lines | ||
lines.push([]) | ||
// note carriage to new line in counter | ||
currentLine++ | ||
// reset the spacesUsed to 0 | ||
spacesUsed = 0 | ||
/* falls through */ | ||
// Fits on current line | ||
// fits on current line | ||
// eslint-disable-next-line | ||
default: | ||
//add word to line | ||
wrapObj.lines[wrapObj.currentLine].push(word) | ||
//reduce space remaining (add a space between words) | ||
wrapObj.spacesUsed += wordLength + 1 | ||
// add word to line | ||
lines[currentLine].push(word) | ||
// reduce space remaining (add a space between words) | ||
spacesUsed += wordLength + 1 | ||
} | ||
} | ||
if(wrapObj.returnFormat === "array") { | ||
return wrapObj.lines | ||
} else{ | ||
let lines = wrapObj.lines.map(function(line) { | ||
//restore spaces to line | ||
line = line.join(" ") | ||
//add padding to ends of line | ||
if(!wrapObj.skipPadding) { | ||
line = Array(wrapObj.paddingLeft+1).join(" ") + | ||
line + | ||
Array(wrapObj.paddingRight+1).join(" ") | ||
} | ||
return line | ||
lines = lines.map( line => { | ||
// restore spaces to line | ||
line = line.join(" ") | ||
// add padding to ends of line | ||
if(!config.skipPadding) { | ||
line = Array(config.paddingLeft + 1).join(" ") | ||
+ line | ||
+ Array(config.paddingRight + 1).join(" ") | ||
} | ||
return line | ||
}) | ||
return lines.join("\n") | ||
} | ||
const splitAnsiInput = (text) => { | ||
// get start and end positions for matches | ||
let matches = [] | ||
let textArr = [...text] | ||
/* eslint-disable */ | ||
while((result = ANSIRegex.exec(text)) !== null) { | ||
matches.push({ | ||
start: result.index, | ||
end: result.index + result[0].length, | ||
match: result[0], | ||
length: result[0].length | ||
}) | ||
//return as string | ||
return lines.join("\n") | ||
} | ||
/* eslint-enable */ | ||
if (matches.length < 1) return [] // we have no ANSI escapes, we're done here | ||
// add start and end positions for non matches | ||
matches = matches.reduce((prev, curr) => { | ||
// check if space exists between this and last match | ||
// get end of previous match | ||
let prevEnd = prev[prev.length -1] | ||
if (prevEnd.end < curr.start) { | ||
// insert placeholder | ||
prev.push({ | ||
start: prevEnd.end, | ||
end: curr.start, | ||
length: curr.start - prevEnd.end, | ||
expand: true | ||
}, curr) | ||
} else { | ||
prev.push(curr) | ||
} | ||
return prev | ||
}, [{start: 0, end: 0}]) | ||
.splice(1) // removes starting accumulator object | ||
// add trailing match if necessary | ||
let lastMatchEnd = matches[matches.length - 1].end | ||
if (lastMatchEnd < textArr.length - 1) { | ||
matches.push({ | ||
start: lastMatchEnd, | ||
end: textArr.length, | ||
expand: true | ||
}) | ||
} | ||
let savedArr = matches.map(match => { | ||
let value = text.substring(match.start, match.end) | ||
return (match.expand) ? [...value] : [value] | ||
}).flat(2) | ||
return savedArr | ||
} | ||
module.exports = smartWrap | ||
const restoreANSI = (savedArr, processedArr) => { | ||
return processedArr.map((char) => { | ||
let result | ||
if (char === "\n") { | ||
result = [char] | ||
} else { | ||
// add everything saved before character match | ||
let splicePoint = savedArr.findIndex(element => element === char ) + 1 | ||
result = savedArr.splice(0, splicePoint) | ||
} | ||
// add all following, consecutive closing tags in case linebreak inerted next | ||
const ANSIClosePattern = "^\\x1b\\[([0-9]+)*m" | ||
const ANSICloseRegex = new RegExp(ANSIClosePattern) // eslint-disable-line no-control-regex | ||
const closeCodes = ["0", "21", "22", "23", "24", "25", "27", "28", "29", "39", "49", "54", "55"] | ||
let match | ||
while (savedArr.length && (match = savedArr[0].match(ANSICloseRegex))) { | ||
if (!closeCodes.includes(match[1])) break | ||
result.push(savedArr.shift()) | ||
} | ||
return result.join("") | ||
}).concat(savedArr) | ||
} | ||
module.exports = (input, options) => { | ||
// in case a template literal was passed that has newling characters, | ||
// split string by newlines and process each resulting string | ||
let str = input.toString() | ||
// save input ANSI escape codes to be restored later | ||
const savedANSI = splitAnsiInput(str) | ||
// strip ANSI | ||
str = stripansi(str) | ||
// convert input to array, each element a line | ||
const linesArr = str.split("\n").map( string => { | ||
return wrap(string, options) | ||
}) | ||
// return linesArr.join("\n") | ||
// --- following code re-applies ANSI | ||
// convert line arrays to a single string broken by return characters | ||
const wrappedStr = linesArr.join("\n") | ||
// and an ANSI closing character, so style never bleeds | ||
// const wrappedStr = linesArr.join(`\u001b\[0m\n`) | ||
// break that line into single characters | ||
let wrappedArr = [...wrappedStr] | ||
// restore input ANSI escape codes if needed | ||
wrappedArr = (savedANSI.length > 0) ? restoreANSI(savedANSI, wrappedArr) : wrappedArr | ||
return wrappedArr.join("") | ||
} |
@@ -15,3 +15,3 @@ #!/usr/bin/env node | ||
yargs.option("minWidth", { | ||
choices: [1,2], | ||
choices: [1, 2], | ||
default: 2, | ||
@@ -29,3 +29,3 @@ describe: "Never change this unless you are certin you are not using wide characters and you want a column 1 space wide, then change to 1" | ||
yargs.option("splitAt", { | ||
default: [" ","\t"], | ||
default: [" ", "\t"], | ||
describe: "Characters at which to split input" | ||
@@ -50,3 +50,3 @@ }) | ||
//create options object | ||
// create options object | ||
let options = {}; | ||
@@ -69,7 +69,7 @@ [ | ||
process.stdin.on("data", function(chunk) { | ||
let out = Smartwrap(chunk,options) | ||
let out = Smartwrap(chunk, options) | ||
console.log(out) | ||
}) | ||
//yargs = yargs('h').argv; | ||
// yargs = yargs('h').argv; | ||
yargs.argv = yargs.help("h").argv |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
50319
18
675
5
7
+ Addedgrapheme-splitter@^1.0.4
+ Addedstrip-ansi@^6.0.0
+ Addedgrapheme-splitter@1.0.4(transitive)