@git-stunts/git-warp 10.1.1
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/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- package/src/visualization/utils/unicode.js +52 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG renderer: generates an SVG string from a PositionedGraph.
|
|
3
|
+
*
|
|
4
|
+
* No jsdom or D3 dependency — pure string templating.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PADDING = 40;
|
|
8
|
+
|
|
9
|
+
const PALETTE = {
|
|
10
|
+
bg: '#1e1e2e',
|
|
11
|
+
nodeFill: '#313244',
|
|
12
|
+
nodeStroke: '#89b4fa',
|
|
13
|
+
nodeText: '#cdd6f4',
|
|
14
|
+
edgeStroke: '#a6adc8',
|
|
15
|
+
edgeLabel: '#bac2de',
|
|
16
|
+
arrowFill: '#a6adc8',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function escapeXml(str) {
|
|
20
|
+
return String(str)
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderDefs() {
|
|
28
|
+
return [
|
|
29
|
+
'<defs>',
|
|
30
|
+
' <marker id="arrowhead" markerWidth="10" markerHeight="7"',
|
|
31
|
+
` refX="10" refY="3.5" orient="auto" fill="${PALETTE.arrowFill}">`,
|
|
32
|
+
' <polygon points="0 0, 10 3.5, 0 7"/>',
|
|
33
|
+
' </marker>',
|
|
34
|
+
'</defs>',
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderStyle() {
|
|
39
|
+
return [
|
|
40
|
+
'<style>',
|
|
41
|
+
` .node rect { fill: ${PALETTE.nodeFill}; stroke: ${PALETTE.nodeStroke}; stroke-width: 2; rx: 6; }`,
|
|
42
|
+
` .node text { fill: ${PALETTE.nodeText}; font-family: monospace; font-size: 13px; dominant-baseline: central; text-anchor: middle; }`,
|
|
43
|
+
` .edge polyline { fill: none; stroke: ${PALETTE.edgeStroke}; stroke-width: 1.5; marker-end: url(#arrowhead); }`,
|
|
44
|
+
` .edge-label { fill: ${PALETTE.edgeLabel}; font-family: monospace; font-size: 11px; text-anchor: middle; }`,
|
|
45
|
+
'</style>',
|
|
46
|
+
].join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderNode(node) {
|
|
50
|
+
const { x, y, width, height } = node;
|
|
51
|
+
const label = escapeXml(node.label ?? node.id);
|
|
52
|
+
const cx = x + width / 2;
|
|
53
|
+
const cy = y + height / 2;
|
|
54
|
+
return [
|
|
55
|
+
`<g class="node">`,
|
|
56
|
+
` <rect x="${x}" y="${y}" width="${width}" height="${height}"/>`,
|
|
57
|
+
` <text x="${cx}" y="${cy}">${label}</text>`,
|
|
58
|
+
'</g>',
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sectionToPoints(section) {
|
|
63
|
+
const pts = [];
|
|
64
|
+
if (section.startPoint) {
|
|
65
|
+
pts.push(section.startPoint);
|
|
66
|
+
}
|
|
67
|
+
if (section.bendPoints) {
|
|
68
|
+
pts.push(...section.bendPoints);
|
|
69
|
+
}
|
|
70
|
+
if (section.endPoint) {
|
|
71
|
+
pts.push(section.endPoint);
|
|
72
|
+
}
|
|
73
|
+
return pts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderEdge(edge) {
|
|
77
|
+
const { sections } = edge;
|
|
78
|
+
if (!sections || sections.length === 0) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allPoints = [];
|
|
83
|
+
for (const s of sections) {
|
|
84
|
+
allPoints.push(...sectionToPoints(s));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (allPoints.length < 2) {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pointsStr = allPoints
|
|
92
|
+
.map((p) => `${p.x},${p.y}`)
|
|
93
|
+
.join(' ');
|
|
94
|
+
|
|
95
|
+
const lines = [
|
|
96
|
+
'<g class="edge">',
|
|
97
|
+
` <polyline points="${pointsStr}"/>`,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
if (edge.label) {
|
|
101
|
+
const midIdx = Math.floor((allPoints.length - 1) / 2);
|
|
102
|
+
const a = allPoints[midIdx];
|
|
103
|
+
const b = allPoints[Math.min(midIdx + 1, allPoints.length - 1)];
|
|
104
|
+
const midX = (a.x + b.x) / 2;
|
|
105
|
+
const midY = (a.y + b.y) / 2;
|
|
106
|
+
lines.push(
|
|
107
|
+
` <text class="edge-label" x="${midX}" y="${midY - 6}">${escapeXml(edge.label)}</text>`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
lines.push('</g>');
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Renders a PositionedGraph as an SVG string.
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} positionedGraph - PositionedGraph from runLayout()
|
|
119
|
+
* @param {{ title?: string }} [options]
|
|
120
|
+
* @returns {string} Complete SVG markup
|
|
121
|
+
*/
|
|
122
|
+
export function renderSvg(positionedGraph, options = {}) {
|
|
123
|
+
const { nodes = [], edges = [] } = positionedGraph;
|
|
124
|
+
const w = (positionedGraph.width ?? 400) + PADDING * 2;
|
|
125
|
+
const h = (positionedGraph.height ?? 300) + PADDING * 2;
|
|
126
|
+
|
|
127
|
+
const parts = [
|
|
128
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">`,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (options.title) {
|
|
132
|
+
parts.push(`<title>${escapeXml(options.title)}</title>`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
parts.push(`<rect width="100%" height="100%" fill="${PALETTE.bg}"/>`);
|
|
136
|
+
parts.push(renderDefs());
|
|
137
|
+
parts.push(renderStyle());
|
|
138
|
+
|
|
139
|
+
// Translate content to account for padding
|
|
140
|
+
parts.push(`<g transform="translate(${PADDING},${PADDING})">`);
|
|
141
|
+
|
|
142
|
+
// Edges first (behind nodes)
|
|
143
|
+
for (const edge of edges) {
|
|
144
|
+
const rendered = renderEdge(edge);
|
|
145
|
+
if (rendered) {
|
|
146
|
+
parts.push(rendered);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Nodes on top
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
parts.push(renderNode(node));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
parts.push('</g>');
|
|
156
|
+
parts.push('</svg>');
|
|
157
|
+
|
|
158
|
+
return parts.join('\n');
|
|
159
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import stripAnsiLib from 'strip-ansi';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Strips ANSI escape codes from a string.
|
|
5
|
+
* Used primarily for snapshot testing to get deterministic output.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} str - The string potentially containing ANSI escape codes
|
|
8
|
+
* @returns {string} The string with all ANSI codes removed
|
|
9
|
+
*/
|
|
10
|
+
export function stripAnsi(str) {
|
|
11
|
+
return stripAnsiLib(str);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default { stripAnsi };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a date as a human-readable relative time string (e.g. "5m ago", "3d ago").
|
|
3
|
+
*
|
|
4
|
+
* @param {string|number|Date} date - The date to format (any value accepted by `new Date()`)
|
|
5
|
+
* @returns {string} Relative time string, or 'unknown' if the date is invalid
|
|
6
|
+
*/
|
|
7
|
+
export function timeAgo(date) {
|
|
8
|
+
const ts = new Date(date).getTime();
|
|
9
|
+
if (isNaN(ts)) {
|
|
10
|
+
return 'unknown';
|
|
11
|
+
}
|
|
12
|
+
const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
13
|
+
|
|
14
|
+
if (seconds < 60) {return `${seconds}s ago`;}
|
|
15
|
+
const minutes = Math.floor(seconds / 60);
|
|
16
|
+
if (minutes < 60) {return `${minutes}m ago`;}
|
|
17
|
+
const hours = Math.floor(minutes / 60);
|
|
18
|
+
if (hours < 24) {return `${hours}h ago`;}
|
|
19
|
+
const days = Math.floor(hours / 24);
|
|
20
|
+
return `${days}d ago`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Formats a duration in milliseconds as a human-readable string (e.g. "150ms", "3s", "2m 30s").
|
|
25
|
+
*
|
|
26
|
+
* @param {number} ms - Duration in milliseconds
|
|
27
|
+
* @returns {string} Formatted duration string
|
|
28
|
+
*/
|
|
29
|
+
export function formatDuration(ms) {
|
|
30
|
+
if (typeof ms !== 'number' || Number.isNaN(ms) || ms < 0) { return 'unknown'; }
|
|
31
|
+
if (ms < 1000) { return `${ms}ms`; }
|
|
32
|
+
const seconds = Math.floor(ms / 1000);
|
|
33
|
+
if (seconds < 60) { return `${seconds}s`; }
|
|
34
|
+
const minutes = Math.floor(seconds / 60);
|
|
35
|
+
if (minutes < 60) { return `${minutes}m ${seconds % 60}s`; }
|
|
36
|
+
const hours = Math.floor(minutes / 60);
|
|
37
|
+
return `${hours}h ${minutes % 60}m`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default { timeAgo, formatDuration };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import stringWidth from 'string-width';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Truncates a string to a maximum display width, appending an ellipsis if needed.
|
|
5
|
+
* Handles wide characters (CJK, emoji) correctly via string-width.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} str - The string to truncate
|
|
8
|
+
* @param {number} maxWidth - Maximum display width in columns
|
|
9
|
+
* @param {string} [ellipsis='…'] - The ellipsis character(s) to append when truncating
|
|
10
|
+
* @returns {string} The truncated string, or original if it fits within maxWidth
|
|
11
|
+
*/
|
|
12
|
+
export function truncate(str, maxWidth, ellipsis = '…') {
|
|
13
|
+
const ellipsisWidth = stringWidth(ellipsis);
|
|
14
|
+
|
|
15
|
+
// Guard degenerate cases
|
|
16
|
+
if (maxWidth <= 0) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
if (maxWidth <= ellipsisWidth) {
|
|
20
|
+
return ellipsis.slice(0, maxWidth);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (stringWidth(str) <= maxWidth) {
|
|
24
|
+
return str;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let result = '';
|
|
28
|
+
let width = 0;
|
|
29
|
+
|
|
30
|
+
for (const char of str) {
|
|
31
|
+
const charWidth = stringWidth(char);
|
|
32
|
+
if (width + charWidth + ellipsisWidth > maxWidth) {break;}
|
|
33
|
+
result += char;
|
|
34
|
+
width += charWidth;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result + ellipsis;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default { truncate };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import stringWidth from 'string-width';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Right-pads a string to a target display width.
|
|
5
|
+
* Handles wide characters correctly via string-width.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} str - The string to pad
|
|
8
|
+
* @param {number} width - Target display width in columns
|
|
9
|
+
* @param {string} [char=' '] - The padding character
|
|
10
|
+
* @returns {string} The padded string, or original if already >= width
|
|
11
|
+
*/
|
|
12
|
+
export function padRight(str, width, char = ' ') {
|
|
13
|
+
const currentWidth = stringWidth(str);
|
|
14
|
+
if (currentWidth >= width) {return str;}
|
|
15
|
+
return str + char.repeat(width - currentWidth);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Left-pads a string to a target display width.
|
|
20
|
+
* Handles wide characters correctly via string-width.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} str - The string to pad
|
|
23
|
+
* @param {number} width - Target display width in columns
|
|
24
|
+
* @param {string} [char=' '] - The padding character
|
|
25
|
+
* @returns {string} The padded string, or original if already >= width
|
|
26
|
+
*/
|
|
27
|
+
export function padLeft(str, width, char = ' ') {
|
|
28
|
+
const currentWidth = stringWidth(str);
|
|
29
|
+
if (currentWidth >= width) {return str;}
|
|
30
|
+
return char.repeat(width - currentWidth) + str;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Centers a string within a target display width.
|
|
35
|
+
* Handles wide characters correctly via string-width.
|
|
36
|
+
* Extra padding goes to the right when the total padding is odd.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} str - The string to center
|
|
39
|
+
* @param {number} width - Target display width in columns
|
|
40
|
+
* @param {string} [char=' '] - The padding character
|
|
41
|
+
* @returns {string} The centered string, or original if already >= width
|
|
42
|
+
*/
|
|
43
|
+
export function center(str, width, char = ' ') {
|
|
44
|
+
const currentWidth = stringWidth(str);
|
|
45
|
+
if (currentWidth >= width) {return str;}
|
|
46
|
+
const padding = width - currentWidth;
|
|
47
|
+
const left = Math.floor(padding / 2);
|
|
48
|
+
const right = padding - left;
|
|
49
|
+
return char.repeat(left) + str + char.repeat(right);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default { padRight, padLeft, center, stringWidth };
|