rman 2.21.0 → 2.22.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/builtins/platforms/node/services/clean.service.js +42 -7
- package/commands/list.command.js +2 -1
- package/commands/version.command.d.ts +10 -0
- package/commands/version.command.js +9 -1
- package/constants.js +1 -1
- package/package.json +1 -1
- package/services/version-plan.service.d.ts +6 -0
- package/services/version-plan.service.js +67 -4
- package/utils/fit-table.d.ts +9 -0
- package/utils/fit-table.js +92 -0
|
@@ -119,7 +119,9 @@ async function remove(file, dryRun) {
|
|
|
119
119
|
function pruneEmptyDirs(dir, includeSelf, dryRun) {
|
|
120
120
|
if (dryRun || !fs.existsSync(dir))
|
|
121
121
|
return;
|
|
122
|
-
|
|
122
|
+
/** Not through a link: a link to a directory is not a directory of this one to prune - removing
|
|
123
|
+
* it as one is `ENOTDIR`, and walking into it reaches somewhere else entirely. */
|
|
124
|
+
const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true, followSymbolicLinks: false });
|
|
123
125
|
entries.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
|
|
124
126
|
for (const rel of entries) {
|
|
125
127
|
const abs = path.join(dir, rel);
|
|
@@ -140,23 +142,48 @@ function pruneEmptyDirs(dir, includeSelf, dryRun) {
|
|
|
140
142
|
* matching files *inside* an otherwise-deleted directory, leaving the rest of it gone and the
|
|
141
143
|
* protected files (and their now non-empty parent) in place.
|
|
142
144
|
*/
|
|
145
|
+
/* **A symbolic link is removed as a link, and never followed.** A build directory routinely holds
|
|
146
|
+
* one - `build/node_modules/@scope/sibling` pointing at another package's own build - and walking
|
|
147
|
+
* through it deleted *that* package's output, then failed `rmdir` on the link with `ENOTDIR`.
|
|
148
|
+
* Measured on `panates/syncbridge`: cleaning `syncbuild` emptied `common/build`, and the run
|
|
149
|
+
* reported only `0 succeeded, 1 failed`. fast-glob follows links by default, so every glob here
|
|
150
|
+
* says `followSymbolicLinks: false`. */
|
|
143
151
|
async function cleanGlobs(dirname, include, exclude, dryRun) {
|
|
144
152
|
if (!include.length)
|
|
145
153
|
return [];
|
|
146
|
-
const matches = await fg(include, {
|
|
154
|
+
const matches = await fg(include, {
|
|
155
|
+
cwd: dirname,
|
|
156
|
+
ignore: exclude,
|
|
157
|
+
onlyFiles: false,
|
|
158
|
+
dot: true,
|
|
159
|
+
absolute: true,
|
|
160
|
+
followSymbolicLinks: false,
|
|
161
|
+
});
|
|
147
162
|
const protectedFiles = exclude.length
|
|
148
|
-
? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true }))
|
|
163
|
+
? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true, followSymbolicLinks: false }))
|
|
149
164
|
: new Set();
|
|
150
165
|
const removed = [];
|
|
151
166
|
for (const m of matches) {
|
|
152
|
-
|
|
167
|
+
/** `lstat`, not `stat`: a link is removed as the link it is, and never read as the thing it
|
|
168
|
+
* points at. */
|
|
169
|
+
const stat = await fs.promises.lstat(m).catch(() => undefined);
|
|
153
170
|
if (!stat)
|
|
154
171
|
continue;
|
|
155
172
|
if (stat.isDirectory()) {
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
/** Files *and* links inside, and a link never entered - see `cleanGlobs`' own note. */
|
|
174
|
+
const inside = await fg('**', {
|
|
175
|
+
cwd: m,
|
|
176
|
+
onlyFiles: false,
|
|
177
|
+
dot: true,
|
|
178
|
+
absolute: true,
|
|
179
|
+
followSymbolicLinks: false,
|
|
180
|
+
});
|
|
181
|
+
for (const f of inside) {
|
|
158
182
|
if (protectedFiles.has(f))
|
|
159
183
|
continue;
|
|
184
|
+
const entry = await fs.promises.lstat(f).catch(() => undefined);
|
|
185
|
+
if (!entry || entry.isDirectory())
|
|
186
|
+
continue;
|
|
160
187
|
await remove(f, dryRun);
|
|
161
188
|
removed.push(path.relative(dirname, f));
|
|
162
189
|
}
|
|
@@ -208,6 +235,8 @@ async function cleanTsArtifacts(pkg, dryRun) {
|
|
|
208
235
|
onlyFiles: true,
|
|
209
236
|
dot: true,
|
|
210
237
|
absolute: true,
|
|
238
|
+
/** A link to a directory elsewhere is not this package's source to sweep. */
|
|
239
|
+
followSymbolicLinks: false,
|
|
211
240
|
});
|
|
212
241
|
const removed = [];
|
|
213
242
|
for (const f of files) {
|
|
@@ -277,6 +306,7 @@ async function cleanTsBuildInfo(dirname, dryRun) {
|
|
|
277
306
|
onlyFiles: true,
|
|
278
307
|
dot: true,
|
|
279
308
|
absolute: true,
|
|
309
|
+
followSymbolicLinks: false,
|
|
280
310
|
});
|
|
281
311
|
for (const f of files)
|
|
282
312
|
await remove(f, dryRun);
|
|
@@ -310,7 +340,12 @@ async function cleanPackage(pkg, item, panelEnabled, dryRun, logger) {
|
|
|
310
340
|
}
|
|
311
341
|
catch (e) {
|
|
312
342
|
item.status = 'failed';
|
|
313
|
-
|
|
343
|
+
const message = e?.message ?? String(e);
|
|
344
|
+
item.log.push(message);
|
|
345
|
+
/** The panel replays `log` in its recap; with no panel nothing does, and the run said only
|
|
346
|
+
* `0 succeeded, 1 failed` - which is how the link bug above went unexplained. */
|
|
347
|
+
if (!panelEnabled)
|
|
348
|
+
logger.error(colors.red('clean'), colors.cyan(item.name), message);
|
|
314
349
|
throw e;
|
|
315
350
|
}
|
|
316
351
|
finally {
|
package/commands/list.command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import colors from 'ansi-colors';
|
|
2
2
|
import EasyTable from 'easy-table';
|
|
3
3
|
import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
4
|
+
import { fitTable } from '../utils/fit-table.js';
|
|
4
5
|
import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
5
6
|
import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
|
|
6
7
|
/** `[options...]` is yargs-meaningless - its variadic marker is two dots, and this command declares
|
|
@@ -185,7 +186,7 @@ function printTable(items, withRoot) {
|
|
|
185
186
|
table.cell('Path', it.location);
|
|
186
187
|
table.newRow();
|
|
187
188
|
}
|
|
188
|
-
console.log(table.toString().trim());
|
|
189
|
+
console.log(fitTable(table.toString().trim(), process.stdout.isTTY ? process.stdout.columns : undefined));
|
|
189
190
|
console.log('');
|
|
190
191
|
console.log(colors.gray(`${items.length - (withRoot ? 1 : 0)} Package(s) found`));
|
|
191
192
|
}
|
|
@@ -70,6 +70,11 @@ declare const config: {
|
|
|
70
70
|
* `--message` is `commitMessage`'s flag and is declared above; the rest are settings a repository
|
|
71
71
|
* states once, not things a single run overrides.
|
|
72
72
|
*/
|
|
73
|
+
skip: {
|
|
74
|
+
target: "config";
|
|
75
|
+
describe: string;
|
|
76
|
+
type: "boolean";
|
|
77
|
+
};
|
|
73
78
|
commitMessage: {
|
|
74
79
|
target: "config";
|
|
75
80
|
describe: string;
|
|
@@ -271,6 +276,11 @@ declare const versionCommand: (app: import("../index.js").RmanApplication) => {
|
|
|
271
276
|
* `--message` is `commitMessage`'s flag and is declared above; the rest are settings a repository
|
|
272
277
|
* states once, not things a single run overrides.
|
|
273
278
|
*/
|
|
279
|
+
skip: {
|
|
280
|
+
target: "config";
|
|
281
|
+
describe: string;
|
|
282
|
+
type: "boolean";
|
|
283
|
+
};
|
|
274
284
|
commitMessage: {
|
|
275
285
|
target: "config";
|
|
276
286
|
describe: string;
|
|
@@ -4,6 +4,7 @@ import EasyTable from 'easy-table';
|
|
|
4
4
|
import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
5
5
|
import { VersionPlanService } from '../services/version-plan.service.js';
|
|
6
6
|
import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
7
|
+
import { fitTable } from '../utils/fit-table.js';
|
|
7
8
|
import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
8
9
|
import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
|
|
9
10
|
/**
|
|
@@ -93,6 +94,13 @@ const config = {
|
|
|
93
94
|
* `--message` is `commitMessage`'s flag and is declared above; the rest are settings a repository
|
|
94
95
|
* states once, not things a single run overrides.
|
|
95
96
|
*/
|
|
97
|
+
skip: {
|
|
98
|
+
target: 'config',
|
|
99
|
+
describe: 'Whether this package takes a version at all. Unset, a package kept out of publishing ' +
|
|
100
|
+
'(publish.skip, or private) is left out; true leaves out one that publishes, false versions ' +
|
|
101
|
+
'one that does not.',
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
},
|
|
96
104
|
commitMessage: {
|
|
97
105
|
target: 'config',
|
|
98
106
|
describe: 'The commit message for each group this run commits - "{version}" is substituted when a ' +
|
|
@@ -304,7 +312,7 @@ function printPlan(entries) {
|
|
|
304
312
|
for (const e of members)
|
|
305
313
|
printPlanRow(table, e);
|
|
306
314
|
}
|
|
307
|
-
console.log(table.toString().trim());
|
|
315
|
+
console.log(fitTable(table.toString().trim(), process.stdout.isTTY ? process.stdout.columns : undefined));
|
|
308
316
|
}
|
|
309
317
|
/**
|
|
310
318
|
* The plan, split into the blocks a delimiter is drawn between: the **repository root** first, then
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.22.0';
|
package/package.json
CHANGED
|
@@ -71,6 +71,12 @@ export declare abstract class VersionPlanService {
|
|
|
71
71
|
* disagree about which commits are unreleased.
|
|
72
72
|
*/
|
|
73
73
|
getPlan(repository: Repository, options?: VersionPlanService.Options): Promise<VersionPlanService.Entry[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Why `pkg` takes no version at all, or `undefined` when it does: `.rmanrc "version.skip"` when
|
|
76
|
+
* it says so, and otherwise whether it has been kept out of publishing - `publish.skip`, or every
|
|
77
|
+
* target that ships it leaving it out (npm's `private`).
|
|
78
|
+
*/
|
|
79
|
+
protected notVersionedReason(pkg: Package): string | undefined;
|
|
74
80
|
/**
|
|
75
81
|
* The commit a package's changes are measured **since** - its last release boundary, or
|
|
76
82
|
* `undefined` for a package that has never been released (the caller then reads the whole
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { RmanApplication } from '../core/application.js';
|
|
3
3
|
import { assertOneScheme, semverScheme, VersionScheme } from '../core/classes/version-scheme.js';
|
|
4
|
+
import { skipReasonFor, targetsOf } from '../core/interfaces/publish-target.js';
|
|
4
5
|
import { GitHelper } from '../utils/git.js';
|
|
5
6
|
import { filterPackages } from '../utils/package-filter.js';
|
|
6
7
|
import { findLastReleaseVersion, formatCalendarVersion, usesCalendarVersion } from '../utils/release-version.js';
|
|
@@ -121,8 +122,27 @@ export class VersionPlanService {
|
|
|
121
122
|
* `usesCalendarVersion`'s "one line" case, and the root reported `0.0.5 -> 2.1.6` on the
|
|
122
123
|
* *semver* path instead of the calendar version it takes with four.
|
|
123
124
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
/** Not versioned at all - see `notVersionedReason`. Out before `eligible`, for the reason a
|
|
126
|
+
* skipped dirty package is: a number its group's other members get cannot come from it. */
|
|
127
|
+
const unversioned = new Map();
|
|
128
|
+
for (const pkg of packages) {
|
|
129
|
+
const reason = this.notVersionedReason(pkg);
|
|
130
|
+
if (reason)
|
|
131
|
+
unversioned.set(pkg, reason);
|
|
132
|
+
}
|
|
133
|
+
for (const [pkg, reason] of unversioned) {
|
|
134
|
+
entries.set(pkg.name, {
|
|
135
|
+
package: pkg,
|
|
136
|
+
groupKey: this.resolveGroupKey(pkg),
|
|
137
|
+
group: this.groupLabel(this.resolveGroupKey(pkg)),
|
|
138
|
+
status: 'skip',
|
|
139
|
+
from: pkg.version,
|
|
140
|
+
reason,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const versioned = packages.filter(pkg => !unversioned.has(pkg));
|
|
144
|
+
const eligible = options.ignoreDirty ? versioned.filter(pkg => !dirty.includes(pkg)) : versioned;
|
|
145
|
+
for (const pkg of options.ignoreDirty ? dirty.filter(d => !unversioned.has(d)) : []) {
|
|
126
146
|
entries.set(pkg.name, {
|
|
127
147
|
package: pkg,
|
|
128
148
|
groupKey: this.resolveGroupKey(pkg),
|
|
@@ -185,7 +205,8 @@ export class VersionPlanService {
|
|
|
185
205
|
if (!options.ignoreDirty) {
|
|
186
206
|
for (const pkg of dirty) {
|
|
187
207
|
const entry = entries.get(pkg.name);
|
|
188
|
-
|
|
208
|
+
/** A package that is not versioned has nothing to protect from a dirty tree. */
|
|
209
|
+
if (!entry || unversioned.has(pkg))
|
|
189
210
|
continue;
|
|
190
211
|
entry.status = 'error';
|
|
191
212
|
entry.reason = entry.reason ? `uncommitted local changes (${entry.reason})` : 'uncommitted local changes';
|
|
@@ -193,6 +214,46 @@ export class VersionPlanService {
|
|
|
193
214
|
}
|
|
194
215
|
return result;
|
|
195
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Why `pkg` takes no version at all, or `undefined` when it does: `.rmanrc "version.skip"` when
|
|
219
|
+
* it says so, and otherwise whether it has been kept out of publishing - `publish.skip`, or every
|
|
220
|
+
* target that ships it leaving it out (npm's `private`).
|
|
221
|
+
*/
|
|
222
|
+
/* **A version nothing publishes is a number nobody can install**, and that is the user's reason
|
|
223
|
+
* for reversing the old rule that `version` never reads `publish.skip`. Measured on
|
|
224
|
+
* `panates/syncbridge`: fourteen packages kept out of publishing were still bumped, tagged and
|
|
225
|
+
* written into changelogs on every release - tags with nothing on the registry behind them, and a
|
|
226
|
+
* version line that had jumped by the time publishing was switched back on.
|
|
227
|
+
*
|
|
228
|
+
* `version.skip` decides first, both ways: `true` holds back a package that does publish, `false`
|
|
229
|
+
* versions one that does not - an application deployed by other means, whose number still
|
|
230
|
+
* matters. A monorepo's root never reaches here; its version is the repository's release
|
|
231
|
+
* identity, decided by `buildRootEntry`.
|
|
232
|
+
*
|
|
233
|
+
* **A package no target ships to is still versioned**, deliberately. Tried the other way first,
|
|
234
|
+
* and it stopped versioning every package of a repository that has no publish target at all - one
|
|
235
|
+
* that versions with rman and publishes by other means, or a technology rman has no target for -
|
|
236
|
+
* which is most of the core's own spec fixtures. Only a package kept out *on purpose* is left
|
|
237
|
+
* out. */
|
|
238
|
+
notVersionedReason(pkg) {
|
|
239
|
+
const declared = pkg.config.version?.skip;
|
|
240
|
+
if (declared === true)
|
|
241
|
+
return 'excluded via .rmanrc "version.skip"';
|
|
242
|
+
if (declared === false)
|
|
243
|
+
return undefined;
|
|
244
|
+
if (pkg.config.publish?.skip)
|
|
245
|
+
return 'not published - .rmanrc "publish.skip"';
|
|
246
|
+
const app = pkg.repository?.app;
|
|
247
|
+
if (!app)
|
|
248
|
+
return undefined;
|
|
249
|
+
const targets = targetsOf(app, pkg);
|
|
250
|
+
if (!targets.length)
|
|
251
|
+
return undefined;
|
|
252
|
+
const reasons = targets.map(target => skipReasonFor(pkg, target));
|
|
253
|
+
if (reasons.every(Boolean))
|
|
254
|
+
return `not published - ${reasons[0]}`;
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
196
257
|
/**
|
|
197
258
|
* **Which planner answers for one package: its own technology's.**
|
|
198
259
|
*
|
|
@@ -468,7 +529,9 @@ export class VersionPlanService {
|
|
|
468
529
|
const source = worklist.shift();
|
|
469
530
|
for (const pkg of packages) {
|
|
470
531
|
const entry = entries.get(pkg.name);
|
|
471
|
-
|
|
532
|
+
/** A skipped package stays skipped: it is out of the release on purpose (not published, or
|
|
533
|
+
* dirty under `--ignore-dirty`), and a dependency moving does not change that. */
|
|
534
|
+
if (entry.status === 'bump' || entry.status === 'skip' || entry.groupKey === source.groupKey)
|
|
472
535
|
continue;
|
|
473
536
|
if (!pkg.dependencies.includes(source.package))
|
|
474
537
|
continue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rendered `easy-table` fitted to a terminal `columns` wide: a row wider than that has its **last
|
|
3
|
+
* column** continued on further lines, each starting at that column, and a delimiter line is cut at
|
|
4
|
+
* the edge. Anything that already fits is left exactly as it was.
|
|
5
|
+
*
|
|
6
|
+
* `columns` is `undefined` when the output is not a terminal, and then nothing changes - a file or a
|
|
7
|
+
* pipe has no edge to wrap at, and a reader of one wants each row on one line.
|
|
8
|
+
*/
|
|
9
|
+
export declare function fitTable(text: string, columns: number | undefined): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rendered `easy-table` fitted to a terminal `columns` wide: a row wider than that has its **last
|
|
3
|
+
* column** continued on further lines, each starting at that column, and a delimiter line is cut at
|
|
4
|
+
* the edge. Anything that already fits is left exactly as it was.
|
|
5
|
+
*
|
|
6
|
+
* `columns` is `undefined` when the output is not a terminal, and then nothing changes - a file or a
|
|
7
|
+
* pipe has no edge to wrap at, and a reader of one wants each row on one line.
|
|
8
|
+
*/
|
|
9
|
+
/* **The terminal wrapping it is what this replaces**, and what that did to a table: measured on
|
|
10
|
+
* `panates/syncbridge`, `rman version`'s root row carries a 70-character reason, so the row and the
|
|
11
|
+
* delimiter under the header - as wide as the widest cell - both ran past the edge and continued at
|
|
12
|
+
* column 0, breaking the table into lines the eye could not follow.
|
|
13
|
+
*
|
|
14
|
+
* **Only the last column moves**, because it is the free-text one in every table rman prints
|
|
15
|
+
* (`Reason`, `Path`) - the columns before it are short, fixed values whose alignment is the point.
|
|
16
|
+
* Where it starts is read off the first delimiter line easy-table draws, the one place the column
|
|
17
|
+
* boundaries are spelled out without colour codes in the way.
|
|
18
|
+
*
|
|
19
|
+
* A row whose last column would get fewer than `MIN_WIDTH` characters is left alone: wrapping into a
|
|
20
|
+
* sliver is worse than the terminal's own wrap. */
|
|
21
|
+
export function fitTable(text, columns) {
|
|
22
|
+
if (!columns)
|
|
23
|
+
return text;
|
|
24
|
+
const lines = text.split('\n');
|
|
25
|
+
const delimiter = lines.find(line => /^[\s-]*-[\s-]*$/.test(line));
|
|
26
|
+
if (!delimiter)
|
|
27
|
+
return text;
|
|
28
|
+
const start = delimiter.search(/-+\s*$/);
|
|
29
|
+
const width = columns - start;
|
|
30
|
+
/** Every cell is padded to its column, the last one included, so a row of short text is still as
|
|
31
|
+
* wide as the widest - past the edge, those spaces wrapped into a blank line under each row.
|
|
32
|
+
* Trimmed and cut even where the last column is too narrow to wrap into. */
|
|
33
|
+
const trimmed = lines.map(line => (/^[\s-]+$/.test(line) ? line.slice(0, columns) : line.trimEnd()));
|
|
34
|
+
if (start <= 0 || width < MIN_WIDTH)
|
|
35
|
+
return trimmed.join('\n');
|
|
36
|
+
return trimmed
|
|
37
|
+
.flatMap(line => {
|
|
38
|
+
if (visibleLength(line) <= columns)
|
|
39
|
+
return [line];
|
|
40
|
+
const at = rawIndexOf(line, start);
|
|
41
|
+
const head = line.slice(0, at);
|
|
42
|
+
const tail = line.slice(at);
|
|
43
|
+
/** A cell painted as a whole (`colors.gray(reason)`) keeps its colour on every line. */
|
|
44
|
+
// eslint-disable-next-line no-control-regex
|
|
45
|
+
const paint = /^(?:\x1b\[[0-9;]*m)+/.exec(tail)?.[0] ?? '';
|
|
46
|
+
const chunks = wrapWords(stripAnsi(tail), width);
|
|
47
|
+
const pad = ' '.repeat(start);
|
|
48
|
+
return chunks.map((chunk, i) => (i ? pad : head) + (paint ? `${paint}${chunk}\x1b[0m` : chunk));
|
|
49
|
+
})
|
|
50
|
+
.join('\n');
|
|
51
|
+
}
|
|
52
|
+
const MIN_WIDTH = 20;
|
|
53
|
+
// eslint-disable-next-line no-control-regex
|
|
54
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
55
|
+
function stripAnsi(text) {
|
|
56
|
+
return text.replace(ANSI, '');
|
|
57
|
+
}
|
|
58
|
+
function visibleLength(text) {
|
|
59
|
+
return stripAnsi(text).length;
|
|
60
|
+
}
|
|
61
|
+
/** The index in `line` of its `visible`-th visible character, colour codes skipped. */
|
|
62
|
+
function rawIndexOf(line, visible) {
|
|
63
|
+
let seen = 0;
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < line.length && seen < visible) {
|
|
66
|
+
if (line[i] === '\x1b') {
|
|
67
|
+
const end = line.indexOf('m', i);
|
|
68
|
+
i = end < 0 ? line.length : end + 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
seen++;
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
/** Colour codes sitting right at the boundary belong to the cell after it. */
|
|
75
|
+
return i;
|
|
76
|
+
}
|
|
77
|
+
/** `text` cut at spaces into lines of at most `width` - a word longer than that on a line of its own. */
|
|
78
|
+
function wrapWords(text, width) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
let current = '';
|
|
81
|
+
for (const word of text.trim().split(/\s+/)) {
|
|
82
|
+
if (current && current.length + 1 + word.length > width) {
|
|
83
|
+
lines.push(current);
|
|
84
|
+
current = word;
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
current = current ? `${current} ${word}` : word;
|
|
88
|
+
}
|
|
89
|
+
if (current)
|
|
90
|
+
lines.push(current);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|