@cto.af/linewrap-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/bin/linewrap.js +258 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @cto.af/linewrap-cli
|
|
2
|
+
|
|
3
|
+
A command-line interface (CLI) for the
|
|
4
|
+
[@cto.af/linewrap](https://github.com/cto-af/linewrap) project.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install -g @cto.af/linewrap-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Invocation
|
|
13
|
+
|
|
14
|
+
```txt
|
|
15
|
+
Usage: linewrap [options] [...file]
|
|
16
|
+
|
|
17
|
+
Wrap some text, either from file, stdin, or given on the command line. Each
|
|
18
|
+
chunk of text is wrapped independently from one another, and streamed to stdout
|
|
19
|
+
(or an outFile, if given). Command line arguments with -t/--text are processed
|
|
20
|
+
before files.
|
|
21
|
+
|
|
22
|
+
Arguments:
|
|
23
|
+
...file files to wrap and concatenate. Use "-" for
|
|
24
|
+
stdin. Default: "-"
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-7,--example7 turn on the extra rules from Example 7 of UAX
|
|
28
|
+
#14
|
|
29
|
+
-c,--firstCol <value> If outdentFirst is specified, how many columns
|
|
30
|
+
was the first line already indented? If NaN,
|
|
31
|
+
use the indent width, in graphemes. If
|
|
32
|
+
outdentFirst is false, this is ignored Default:
|
|
33
|
+
"NaN"
|
|
34
|
+
-e,--encoding <encoding> encoding for files read or written. stdout is
|
|
35
|
+
always in the default encoding. (choices:
|
|
36
|
+
"ascii", "utf8", "utf-8", "utf16le", "ucs2",
|
|
37
|
+
"ucs-2", "base64", "base64url", "latin1",
|
|
38
|
+
"binary", "hex") Default: "utf8"
|
|
39
|
+
--ellipsis <string> What string to use when a word is longer than
|
|
40
|
+
the max width, and in overflow mode "clip"
|
|
41
|
+
Default: "…"
|
|
42
|
+
-h,--help display help for command
|
|
43
|
+
--html escape output for HTML
|
|
44
|
+
--hyphen <string> What string to use when a word is longer than
|
|
45
|
+
the max width, and in overflow mode "any"
|
|
46
|
+
Default: "-"
|
|
47
|
+
-i,--indent <string|number> indent each line with this text. If a number,
|
|
48
|
+
indent that many spaces Default: ""
|
|
49
|
+
--indentChar <string> if indent is a number, that many indentChars
|
|
50
|
+
will be inserted before each line Default: " "
|
|
51
|
+
--indentEmpty if the input string is empty, should we still
|
|
52
|
+
indent? Default: false
|
|
53
|
+
--isNewline <regex> a regular expression to replace newlines in the
|
|
54
|
+
input. Empty to leave newlines in place.
|
|
55
|
+
Default: "[^\\S\\r\\n\\v\\f\\x85\\u2028\
|
|
56
|
+
\u2029]*[\\r\\n\\v\\f\\x85\\u2028\\u2029]+\\s*"
|
|
57
|
+
-l,--locale <iso location> locale for grapheme segmentation. Has very
|
|
58
|
+
little effect at the moment
|
|
59
|
+
--newline <string> how to separate the lines of output Default:
|
|
60
|
+
"\n"
|
|
61
|
+
--newlineReplacement <string> when isNewline matches, replace with this
|
|
62
|
+
string Default: " "
|
|
63
|
+
-o,--outFile <file> output to a file instead of stdout
|
|
64
|
+
--outdentFirst Do not indent the first output line Default:
|
|
65
|
+
false
|
|
66
|
+
--overflow <style> what to do with words that are longer than
|
|
67
|
+
width. (choices: "visible", "clip",
|
|
68
|
+
"anywhere") Default: "visible"
|
|
69
|
+
-t,--text <value> wrap this chunk of text. If used, stdin is not
|
|
70
|
+
processed unless "-" is used explicitly. Can
|
|
71
|
+
be specified multiple times. Default: []
|
|
72
|
+
-v,--verbose turn on super-verbose information. Not useful
|
|
73
|
+
for anything but debugging underlying libraries
|
|
74
|
+
-w,--width <columns> maximum line length Default: "(your terminal
|
|
75
|
+
width or 80)"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
linewrap -w 4 -t "foo bar"
|
|
82
|
+
echo -n "foo bar" | linewrap -w 4
|
|
83
|
+
linewrap -o outputFileName.txt -w 4 inputFileName.txt
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
[](https://github.com/cto-af/linewrap-cli/actions/workflows/node.js.yml)
|
|
88
|
+
[](https://codecov.io/gh/cto-af/linewrap-cli)
|
package/bin/linewrap.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
import {inspect, promisify} from 'util'
|
|
5
|
+
import {LineWrap} from '@cto.af/linewrap'
|
|
6
|
+
import {fileURLToPath} from 'url'
|
|
7
|
+
import fs from 'fs'
|
|
8
|
+
import os from 'os'
|
|
9
|
+
import {parseArgsWithHelp} from 'minus-h'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {Parameters<generateHelp>[0]}
|
|
13
|
+
*/
|
|
14
|
+
const config = {
|
|
15
|
+
options: {
|
|
16
|
+
encoding: {
|
|
17
|
+
short: 'e',
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: 'utf8',
|
|
20
|
+
argumentName: 'encoding',
|
|
21
|
+
description: 'encoding for files read or written. stdout is always in the default encoding.',
|
|
22
|
+
choices: ['ascii', 'utf8', 'utf-8', 'utf16le', 'ucs2', 'ucs-2', 'base64', 'base64url', 'latin1', 'binary', 'hex'],
|
|
23
|
+
},
|
|
24
|
+
ellipsis: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: LineWrap.DEFAULT_OPTIONS.ellipsis,
|
|
27
|
+
argumentName: 'string',
|
|
28
|
+
description: 'What string to use when a word is longer than the max width, and in overflow mode "clip"',
|
|
29
|
+
},
|
|
30
|
+
example7: {
|
|
31
|
+
short: '7',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
description: 'turn on the extra rules from Example 7 of UAX #14',
|
|
34
|
+
},
|
|
35
|
+
firstCol: {
|
|
36
|
+
short: 'c',
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'NaN',
|
|
39
|
+
description: 'If outdentFirst is specified, how many columns was the first line already indented? If NaN, use the indent width, in graphemes. If outdentFirst is false, this is ignored',
|
|
40
|
+
},
|
|
41
|
+
html: {type: 'boolean', description: 'escape output for HTML'},
|
|
42
|
+
hyphen: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
default: LineWrap.DEFAULT_OPTIONS.hyphen,
|
|
45
|
+
argumentName: 'string',
|
|
46
|
+
description: 'What string to use when a word is longer than the max width, and in overflow mode "any"',
|
|
47
|
+
},
|
|
48
|
+
indent: {
|
|
49
|
+
short: 'i',
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: '',
|
|
52
|
+
argumentName: 'string|number',
|
|
53
|
+
description: 'indent each line with this text. If a number, indent that many spaces',
|
|
54
|
+
},
|
|
55
|
+
indentChar: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: LineWrap.DEFAULT_OPTIONS.indentChar,
|
|
58
|
+
argumentName: 'string',
|
|
59
|
+
description: 'if indent is a number, that many indentChars will be inserted before each line',
|
|
60
|
+
},
|
|
61
|
+
indentEmpty: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
default: LineWrap.DEFAULT_OPTIONS.indentEmpty,
|
|
64
|
+
description: 'if the input string is empty, should we still indent?',
|
|
65
|
+
},
|
|
66
|
+
isNewline: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
default: LineWrap.DEFAULT_OPTIONS.isNewline.source,
|
|
69
|
+
argumentName: 'regex',
|
|
70
|
+
description: 'a regular expression to replace newlines in the input. Empty to leave newlines in place.',
|
|
71
|
+
},
|
|
72
|
+
locale: {
|
|
73
|
+
short: 'l',
|
|
74
|
+
type: 'string',
|
|
75
|
+
// This would make the tests depend on current locale:
|
|
76
|
+
// default: LineWrap.DEFAULT_OPTIONS.locale,
|
|
77
|
+
argumentName: 'iso location',
|
|
78
|
+
description: 'locale for grapheme segmentation. Has very little effect at the moment',
|
|
79
|
+
},
|
|
80
|
+
newline: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
default: os.EOL,
|
|
83
|
+
argumentName: 'string',
|
|
84
|
+
description: 'how to separate the lines of output',
|
|
85
|
+
},
|
|
86
|
+
newlineReplacement: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
argumentName: 'string',
|
|
89
|
+
default: LineWrap.DEFAULT_OPTIONS.newlineReplacement,
|
|
90
|
+
description: 'when isNewline matches, replace with this string',
|
|
91
|
+
},
|
|
92
|
+
outdentFirst: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
default: !LineWrap.DEFAULT_OPTIONS.indentFirst,
|
|
95
|
+
description: 'Do not indent the first output line',
|
|
96
|
+
},
|
|
97
|
+
outFile: {
|
|
98
|
+
short: 'o',
|
|
99
|
+
type: 'string',
|
|
100
|
+
argumentName: 'file',
|
|
101
|
+
description: 'output to a file instead of stdout',
|
|
102
|
+
},
|
|
103
|
+
overflow: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
default: 'visible',
|
|
106
|
+
argumentName: 'style',
|
|
107
|
+
description: 'what to do with words that are longer than width.',
|
|
108
|
+
choices: ['visible', 'clip', 'anywhere'],
|
|
109
|
+
},
|
|
110
|
+
text: {
|
|
111
|
+
short: 't',
|
|
112
|
+
type: 'string',
|
|
113
|
+
multiple: true,
|
|
114
|
+
default: [],
|
|
115
|
+
description: 'wrap this chunk of text. If used, stdin is not processed unless "-" is used explicitly. Can be specified multiple times.',
|
|
116
|
+
},
|
|
117
|
+
verbose: {
|
|
118
|
+
short: 'v',
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
description: 'turn on super-verbose information. Not useful for anything but debugging underlying libraries',
|
|
121
|
+
},
|
|
122
|
+
width: {
|
|
123
|
+
short: 'w',
|
|
124
|
+
type: 'string',
|
|
125
|
+
default: String(process.stdout.columns ?? 80),
|
|
126
|
+
argumentName: 'columns',
|
|
127
|
+
description: 'maximum line length',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
allowPositionals: true,
|
|
131
|
+
argumentName: '...file',
|
|
132
|
+
argumentDescription: 'files to wrap and concatenate. Use "-" for stdin. Default: "-"',
|
|
133
|
+
description: 'Wrap some text, either from file, stdin, or given on the command line. Each chunk of text is wrapped independently from one another, and streamed to stdout (or an outFile, if given). Command line arguments with -t/--text are processed before files.',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read stdin to completion with the configured encoding.
|
|
138
|
+
*
|
|
139
|
+
* @returns {Promise<string>}
|
|
140
|
+
*/
|
|
141
|
+
function readStdin(opts, stream) {
|
|
142
|
+
// Below, d will be a string
|
|
143
|
+
stream.setEncoding(opts.encoding)
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let s = ''
|
|
146
|
+
stream.on('data', d => (s += d))
|
|
147
|
+
stream.on('end', () => resolve(s))
|
|
148
|
+
stream.on('error', reject)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ESCAPES = {
|
|
153
|
+
'&': '&',
|
|
154
|
+
'<': '<',
|
|
155
|
+
'>': '>',
|
|
156
|
+
'"': '"',
|
|
157
|
+
"'": ''',
|
|
158
|
+
'\xA0': ' ',
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Escape HTML
|
|
163
|
+
*
|
|
164
|
+
* @param {string} str String containing prohibited characters.
|
|
165
|
+
* @returns {string} Escaped string.
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
function htmlEscape(str) {
|
|
169
|
+
return str.replace(/[&<>\xA0]/g, m => ESCAPES[m])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const {
|
|
173
|
+
exit, stdin, stdout, stderr,
|
|
174
|
+
} = process
|
|
175
|
+
|
|
176
|
+
export async function main(
|
|
177
|
+
extraConfig,
|
|
178
|
+
options,
|
|
179
|
+
process = {exit, stdin, stdout, stderr}
|
|
180
|
+
) {
|
|
181
|
+
const {values, positionals} = parseArgsWithHelp({
|
|
182
|
+
...config,
|
|
183
|
+
...extraConfig,
|
|
184
|
+
}, {
|
|
185
|
+
width: config.options.width.default,
|
|
186
|
+
...options,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if ((values.text.length === 0) && (positionals.length === 0)) {
|
|
190
|
+
positionals.push('-')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Always a valid string, due to choices enforcement
|
|
194
|
+
/** @type synbol */
|
|
195
|
+
const overflow = {
|
|
196
|
+
visible: LineWrap.OVERFLOW_VISIBLE,
|
|
197
|
+
clip: LineWrap.OVERFLOW_CLIP,
|
|
198
|
+
anywhere: LineWrap.OVERFLOW_ANYWHERE,
|
|
199
|
+
}[values.overflow]
|
|
200
|
+
|
|
201
|
+
const outstream = values.outFile ?
|
|
202
|
+
fs.createWriteStream(values.outFile, values.encoding) :
|
|
203
|
+
process.stdout // Don't set encoding, will confuse terminal.
|
|
204
|
+
|
|
205
|
+
/** @type {ConstructorParameters<typeof LineWrap>[0]} */
|
|
206
|
+
const opts = {
|
|
207
|
+
escape: values.html ? htmlEscape : s => s,
|
|
208
|
+
ellipsis: values.ellipsis,
|
|
209
|
+
example7: Boolean(values.example7),
|
|
210
|
+
firstCol: parseInt(values.firstCol, 10),
|
|
211
|
+
hyphen: values.hyphen,
|
|
212
|
+
indent: parseInt(values.indent, 10) || values.indent,
|
|
213
|
+
indentChar: values.indentChar,
|
|
214
|
+
indentEmpty: values.indentEmpty,
|
|
215
|
+
indentFirst: !values.outdentFirst,
|
|
216
|
+
locale: values.locale,
|
|
217
|
+
newline: values.newline,
|
|
218
|
+
newlineReplacement: values.newlineReplacement,
|
|
219
|
+
overflow,
|
|
220
|
+
trim: !values.noTrim,
|
|
221
|
+
verbose: values.verbose,
|
|
222
|
+
width: parseInt(values.width, 10),
|
|
223
|
+
}
|
|
224
|
+
if (typeof values.isNewline === 'string') {
|
|
225
|
+
opts.isNewline = (values.isNewline.length === 0) ?
|
|
226
|
+
null :
|
|
227
|
+
new RegExp(values.isNewline, 'gu')
|
|
228
|
+
}
|
|
229
|
+
if (values.verbose) {
|
|
230
|
+
process.stdout.write(inspect(opts))
|
|
231
|
+
}
|
|
232
|
+
const w = new LineWrap(opts)
|
|
233
|
+
|
|
234
|
+
for (const t of values.text) {
|
|
235
|
+
outstream.write(w.wrap(t))
|
|
236
|
+
outstream.write(values.newline)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const f of positionals) {
|
|
240
|
+
const t = f === '-' ?
|
|
241
|
+
await readStdin(values, process.stdin) :
|
|
242
|
+
await fs.promises.readFile(f, values.encoding)
|
|
243
|
+
|
|
244
|
+
outstream.write(w.wrap(t))
|
|
245
|
+
outstream.write(values.newline)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Be careful to wait for the file to close, to ensure tests run
|
|
249
|
+
// correctly.
|
|
250
|
+
await promisify(outstream.end.bind(outstream))()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
254
|
+
main().catch(e => {
|
|
255
|
+
console.error(e)
|
|
256
|
+
process.exit(1)
|
|
257
|
+
})
|
|
258
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cto.af/linewrap-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Wrap lines using the Unicode Line Breaking algorithm from UAX #14",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"linewrap": "bin/linewrap.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "c8 mocha",
|
|
11
|
+
"lint": "eslint . --ext js,cjs",
|
|
12
|
+
"build": "npm run lint && npm run test && npm pack --dry-run"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"uax14",
|
|
16
|
+
"word-wrap",
|
|
17
|
+
"wordwrap",
|
|
18
|
+
"linebreak",
|
|
19
|
+
"unicode",
|
|
20
|
+
"grapheme",
|
|
21
|
+
"cluster",
|
|
22
|
+
"cli",
|
|
23
|
+
"command-line"
|
|
24
|
+
],
|
|
25
|
+
"author": "Joe Hildebrand <joe-github@cursive.net>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": "cto-af/linewrap-cli",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@cto.af/linewrap": "1.0.3",
|
|
30
|
+
"minus-h": "1.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cto.af/eslint-config": "1.1.2",
|
|
34
|
+
"@types/node": "20.3.0",
|
|
35
|
+
"c8": "7.14.0",
|
|
36
|
+
"eslint": "8.42.0",
|
|
37
|
+
"mocha": "10.2.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">= 16"
|
|
41
|
+
}
|
|
42
|
+
}
|