Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 95 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,117 @@
<h1 align="center">simple-wcswidth</h1>
<h3 align="center"> 🖥️ 💬 Simplified JS/TS implementation of wcwidth/wcswidth written by Markus Kuhn in C</h3>

[![codecov](https://codecov.io/gh/console-table-printer/simple-wcswidth/graph/badge.svg?token=X4QPKTCB6A)](https://codecov.io/gh/console-table-printer/simple-wcswidth)
[![npm version](https://badge.fury.io/js/simple-wcswidth.svg)](https://badge.fury.io/js/simple-wcswidth)
[![install size](https://packagephobia.now.sh/badge?p=simple-wcswidth@latest)](https://packagephobia.now.sh/result?p=simple-wcswidth)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
<p align="center">
<a href="https://www.npmjs.com/package/simple-wcswidth"><img src="https://img.shields.io/npm/v/simple-wcswidth.svg" alt="npm version"></a>
<a href="https://codecov.io/gh/console-table-printer/simple-wcswidth"><img src="https://codecov.io/gh/console-table-printer/simple-wcswidth/graph/badge.svg?token=X4QPKTCB6A" alt="codecov"></a>
<a href="https://packagephobia.now.sh/result?p=simple-wcswidth"><img src="https://packagephobia.now.sh/badge?p=simple-wcswidth@latest" alt="install size"></a>
<a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release"></a>
<a href="https://github.com/console-table-printer/simple-wcswidth/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/simple-wcswidth.svg" alt="license"></a>
</p>

## Installation

```bash
npm install simple-wcswidth
```

## Why another wcswidth?

1. 💙 **Types included** - Full TypeScript support out of the box
2. 🤏 **Minimal size** - Installation size kept as small as possible
3. 🐒 **Zero dependencies** - No unnecessary dependencies
4. 🤖 **Well tested** - Automated testing on multiple Node.js versions
5. 📦 **Dual package** - Supports both ES modules and CommonJS
6. 🎯 **Simple API** - Easy to use with clear documentation

# Why another wcswidth?
## Example Usage

1. 💙 Types included
2. 🤏 Installation Size kept as min possible
3. 🐒 No Unnecessary dependency added
4. 🤖 Tested Automatically and Regularly on different versions of node, including current LTS and stable
### ES Modules (Recommended)
```javascript
import { wcwidth, wcswidth } from 'simple-wcswidth';

# Example Usage
// Basic usage
console.log(wcswidth('Hello')); // 5
console.log(wcswidth('你好')); // 4
console.log(wcswidth('Hello 世界')); // 9

```js
const { wcswidth, wcwidth } = require('simple-wcswidth');
// Character width
console.log(wcwidth('A'.charCodeAt(0))); // 1
console.log(wcwidth('世'.charCodeAt(0))); // 2
```

### CommonJS
```javascript
const { wcwidth, wcswidth } = require('simple-wcswidth');

console.log(wcswidth('Yes 重要')); // 8
console.log(wcswidth('请你')); // 4
console.log(wcswidth('Hi')); // 2
```

console.log(wcwidth('请'.charCodeAt(0))); // 2
### Practical Example: Creating aligned text in terminal

```javascript
function alignText(text, width) {
const actualWidth = wcswidth(text);
const padding = Math.max(0, width - actualWidth);
return text + ' '.repeat(padding);
}

// Create a simple table
console.log('+----------+---------------+');
console.log('| ' + alignText('Name', 8) + ' | ' + alignText('Country', 13) + ' |');
console.log('+----------+---------------+');
console.log('| ' + alignText('John', 8) + ' | ' + alignText('USA', 13) + ' |');
console.log('| ' + alignText('李明', 8) + ' | ' + alignText('China', 13) + ' |');
console.log('| ' + alignText('Юрий', 8) + ' | ' + alignText('Russia', 13) + ' |');
console.log('+----------+---------------+');
```

# What is simplified here?
Output:
```
+----------+---------------+
| Name | Country |
+----------+---------------+
| John | USA |
| 李明 | China |
| Юрий | Russia |
+----------+---------------+
```

## API Reference

### `wcswidth(str: string): number`

Returns the width of a string in terminal columns.

**Parameters:**
- `str` - The string to measure

**Returns:**
- The width in terminal columns, or `-1` if the string contains control characters

### `wcwidth(ucs: number): number`

Returns the width of a single character in terminal columns.

**Parameters:**
- `ucs` - The Unicode code point of the character

**Returns:**
- `0` for null character
- `-1` for control characters
- `0` for non-spacing characters
- `1` for normal characters
- `2` for wide characters (East Asian)

## What is simplified here?

In the original [C code](https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c) there were 2 versions of `wcswidth()` I have included here only for first one, which is applicable for general use.

About the second one(WHICH I DIDNT INCLUDE HERE), useful for users of CJK legacy encodings who want to migrate to UCS without changing the traditional terminal character-width behaviour. It is not otherwise recommended for general use.

# Info Taken from Markus Kuhn's [C code](https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c)
## Info Taken from Markus Kuhn's [C code](https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c)

This is an implementation of [wcwidth()](http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html) and [wcswidth()](http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html) (defined in
IEEE Std 1002.1-2001) for Unicode.
Expand Down Expand Up @@ -82,7 +161,7 @@ routines has avoided to do so far.

http://www.unicode.org/unicode/reports/tr11/

# LICENSE
## License

MIT

Expand Down
45 changes: 45 additions & 0 deletions benchmark/benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { Suite } = require('benchmark');
const { wcwidth, wcswidth } = require('../dist/index.js');

const suite = new Suite();

// Benchmark wcwidth
suite.add('wcwidth - ASCII', () => {
wcwidth('A'.charCodeAt(0));
});

suite.add('wcwidth - CJK', () => {
wcwidth('世'.charCodeAt(0));
});

// Benchmark wcswidth
suite.add('wcswidth - ASCII only', () => {
wcswidth('Hello World');
});

suite.add('wcswidth - Mixed', () => {
wcswidth('Hello 世界');
});

suite.add('wcswidth - CJK only', () => {
wcswidth('你好世界');
});

// Long string test
suite.add('wcswidth - Long ASCII (100 chars)', () => {
wcswidth('A'.repeat(100));
});

suite.add('wcswidth - Long CJK (100 chars)', () => {
wcswidth('世'.repeat(100));
});

// Run benchmarks
suite
.on('cycle', (event) => {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ async: true });
188 changes: 188 additions & 0 deletions examples/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>simple-wcswidth Demo</title>
<style>
body {
font-family: monospace;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.grid {
display: grid;
grid-template-columns: auto auto auto;
gap: 10px;
margin-bottom: 30px;
}
.box {
border: 1px solid #ccc;
padding: 5px;
background-color: #f9f9f9;
}
.highlight {
background-color: #ff9;
}
.terminal {
background-color: #222;
color: #eee;
padding: 10px;
border-radius: 5px;
margin: 20px 0;
font-family: 'Courier New', monospace;
}
.interactive {
margin: 30px 0;
}
input {
font-family: monospace;
padding: 5px;
width: 300px;
}
button {
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>simple-wcswidth Demo</h1>
<p>This demo shows how terminal width is calculated for different characters. Try typing in the input field below to see the width calculation in action.</p>

<div class="interactive">
<h2>Interactive Demo</h2>
<input type="text" id="textInput" placeholder="Type text here..." value="Hello 世界">
<button id="calculateBtn">Calculate Width</button>
<div id="result" class="terminal"></div>
</div>

<h2>Examples</h2>
<div id="demo"></div>

<h2>Terminal Visualization</h2>
<div id="terminal-demo" class="terminal"></div>

<script type="module">
// Import from CDN for demo purposes
import { wcwidth, wcswidth } from 'https://cdn.jsdelivr.net/npm/simple-wcswidth@latest/dist/index.js';

// Define example strings
const examples = [
'Hello',
'你好',
'Hello 世界',
'こんにちは',
'1234567890',
'12345',
'ASCII + 你好 + ١٢٣',
'Control char: \u0000'
];

// Create example grid
const demo = document.getElementById('demo');
const grid = document.createElement('div');
grid.className = 'grid';

// Headers
['String', 'Visual', 'Width'].forEach(header => {
const cell = document.createElement('div');
cell.textContent = header;
cell.style.fontWeight = 'bold';
grid.appendChild(cell);
});

// Examples
examples.forEach(str => {
// String cell
const strCell = document.createElement('div');
strCell.className = 'box';
strCell.textContent = JSON.stringify(str);
grid.appendChild(strCell);

// Visual cell
const visualCell = document.createElement('div');
visualCell.className = 'box';
visualCell.textContent = str;
grid.appendChild(visualCell);

// Width cell
const widthCell = document.createElement('div');
widthCell.className = 'box';
const width = wcswidth(str);
widthCell.textContent = `${width} columns`;
grid.appendChild(widthCell);
});

demo.appendChild(grid);

// Terminal visualization
function visualizeInTerminal(text) {
const width = wcswidth(text);
let result = '';

// Top border
result += '+' + '-'.repeat(width) + '+\n';

// Content
result += '|' + text + ' '.repeat(Math.max(0, width - wcswidth(text))) + '|\n';

// Bottom border
result += '+' + '-'.repeat(width) + '+\n';

return result;
}

// Terminal demo
const terminalDemo = document.getElementById('terminal-demo');
terminalDemo.innerHTML = examples.slice(0, 4).map(visualizeInTerminal).join('\n').replace(/\n/g, '<br>');

// Interactive demo
const textInput = document.getElementById('textInput');
const calculateBtn = document.getElementById('calculateBtn');
const result = document.getElementById('result');

function updateResult() {
const text = textInput.value;
const width = wcswidth(text);

let output = `Text: "${text}"\n`;
output += `Width: ${width} columns\n\n`;

// Character breakdown
output += 'Character breakdown:\n';
for (let i = 0; i < text.length; i++) {
const char = text[i];
const code = char.charCodeAt(0);
const charWidth = wcwidth(code);
output += `- "${char}" (U+${code.toString(16).padStart(4, '0')}): ${charWidth} column${charWidth !== 1 ? 's' : ''}\n`;
}

// Visualization
output += '\nVisualization:\n';
output += visualizeInTerminal(text);

result.innerHTML = output.replace(/\n/g, '<br>');
}

calculateBtn.addEventListener('click', updateResult);
textInput.addEventListener('keyup', event => {
if (event.key === 'Enter') {
updateResult();
}
});

// Initialize with default value
updateResult();
</script>
</body>
</html>
Loading
Loading