@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.
Files changed (3) hide show
  1. package/README.md +88 -0
  2. package/bin/linewrap.js +258 -0
  3. 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
+ [![Tests](https://github.com/cto-af/linewrap-cli/actions/workflows/node.js.yml/badge.svg)](https://github.com/cto-af/linewrap-cli/actions/workflows/node.js.yml)
88
+ [![codecov](https://codecov.io/gh/cto-af/linewrap-cli/branch/main/graph/badge.svg?token=D0hvqMS3Wx)](https://codecov.io/gh/cto-af/linewrap-cli)
@@ -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
+ '&': '&amp;',
154
+ '<': '&lt;',
155
+ '>': '&gt;',
156
+ '"': '&quot;',
157
+ "'": '&apos;',
158
+ '\xA0': '&nbsp;',
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
+ }