Skip to content
Merged
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
17 changes: 17 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ This file contains the documentation on the notable changes and bug fixes, and
is formatted following this [standard](https://keepachangelog.com/en/1.0.0/).
This project also adheres to [Semantic Versioning](https://semver.org/).

## [2.2.0] - 2026-02-22

**Added**:

- Added support for mononyms (enabled via `Config.mono` flag); only available only for
`Namefully(string | string[] | Names[])` or `NameBuilder`.
- Updated FullName parsing to handle new JsonName structure (now hierarchical)
- Added more practical examples

**Fixed**:

- Refactored deserialization logic to accommodate flexible name formats
- Adjusted error handling for invalid name inputs
- Fixed deserialization issue when casting separators

## [2.1.0] - 2025-12-30

**Added**:
Expand Down Expand Up @@ -204,6 +219,8 @@ This project also adheres to [Semantic Versioning](https://semver.org/).

Initial version

[2.2.0]: https://github.com/ralflorent/namefully/compare/v2.1.0...v2.2.0
[2.1.0]: https://github.com/ralflorent/namefully/compare/v2.0.2...v2.1.0
[2.0.2]: https://github.com/ralflorent/namefully/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/ralflorent/namefully/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/ralflorent/namefully/compare/v1.3.0...v2.0.0
Expand Down
4 changes: 2 additions & 2 deletions example/advanced.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Name, NameBuilder, Title } from '../src/index';
import { Name, NameBuilder } from 'namefully';

function main() {
const builder = NameBuilder.of(Name.first('Nikola'), Name.last('Tesla'));
Expand All @@ -11,7 +11,7 @@ function main() {
builder.add(Name.prefix('Mr'));

// Build the name with options if needed
name = builder.build({ title: Title.US });
name = builder.build({ title: 'US' });
console.log(name.full); // Mr. Nikola Tesla
}

Expand Down
82 changes: 82 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Namefully | Demo</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
h1,
h2 {
font-family: Lato;
}

table {
text-align: left;
margin-top: 1em;
}
</style>
</head>
<body>
<div>
<h1>How to use namefully</h1>
<h2>Simple case</h2>
<p>Given the name <i id="name-1">John Smith</i>, use only the first name:</p>
<div id="simple-case"></div>

<!-- Advanced use case -->
<h2>It can also do that</h2>
<p>
Let us say you want to distinguish the name parts of a full name like this
<i id="name-2">Mr,Smith,John,Ben,Ph.D</i> (comma-separated values), you can achieve the following:
</p>
<div id="advanced-case"></div>
<script type="module">
import { Namefully, Separator } from 'https://esm.sh/namefully';

// Simple case
const name = new Namefully(document.getElementById('name-1').innerText);
const simpleDiv = document.getElementById('simple-case');
simpleDiv.innerHTML = `<pre>Hello, ${name.first}!</pre>`;

const advancedDiv = document.getElementById('advanced-case');
const contents = createWithOptionalParamsUseCase();
advancedDiv.innerHTML = `${contents.join('')}`;

// Use case with optional params
function createWithOptionalParamsUseCase() {
const nameEl = document.getElementById('name-2');
const options = [
{
separator: Separator.COMMA,
orderedBy: 'lastName',
title: 'US',
ending: true,
},
// Add more options here to test.
];

return options.map((option) => {
const name = new Namefully(nameEl.innerText, option);
const rows = [
`<tr> <th>Prefix:</th> <td>${name.prefix} </td></tr>`,
`<tr> <th>First name:</th> <td>${name.first} </td></tr>`,
`<tr> <th>Middle name:</th> <td>${name.middle} </td></tr>`,
`<tr> <th>Last name:</th> <td>${name.last} </td></tr>`,
`<tr> <th>Suffix:</th> <td>${name.suffix} </td></tr>`,
`<tr> <th>Full name:</th> <td>${name.full}</td></tr>`,
`<tr> <th>Birth name:</th> <td>${name.birth} </td></tr>`,
`<tr> <th>Short version:</th> <td>${name.short} </td></tr>`,
`<tr> <th>Flat version:</th> <td>${name.zip()} </td></tr>`,
`<tr> <th>Initials:</th> <td>${name.initials().join(' ')} </td></tr>`,
`<tr> <th>Public:</th> <td>${name.public} </td></tr>`,
`<tr> <th>Salutation:</th> <td>${name.salutation} </td></tr>`,
`<tr> <th>Formatted:</th> <td>${name.format('f (m) l')} </td></tr>`,
];
return `<table>${rows.join('')}</table>`;
});
}
</script>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion example/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import namefully from '../src/index';
import namefully from 'namefully';

function main() {
// Gives a simple name some super power.
Expand Down
17 changes: 17 additions & 0 deletions example/mononym.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import namefully from 'namefully';

function main() {
const name = namefully('Plato', { mono: true });
console.log(name.length); // 5
console.log(name.first); // Plato
console.log(name.middle); // undefined
console.log(name.last); // <ZWSP>
console.log(name.public); // Plato
console.log(name.initials()); // ['P']
console.log(name.format('L, f m')); // <ZWSP>, Plato
console.log(name.shorten()); // Plato
console.log(name.zip()); // P.
console.log(name.toUpperCase()); // PLATO
}

main();
28 changes: 28 additions & 0 deletions example/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import namefully, { Config, FullName, Parser } from 'namefully';

class CustomParser extends Parser<string> {
constructor(
raw: string,
public separator = '|',
) {
super(raw);
}

parse(options: Partial<Config>): FullName {
const [fn, ln] = this.raw.split(this.separator, 2);
return new FullName(options).setFirstName(fn.trim()).setLastName(ln.trim());
}
}

function main() {
const name = namefully(new CustomParser('Juan | García | Jr.'));
console.log(name.full); // Juan García
console.log(name.first); // Juan
console.log(name.initials()); // ['J', 'G']
console.log(name.format('L, f m')); // García, Juan
console.log(name.size); // 2
console.log(name.zip()); // Juan G.
console.log(name.toUpperCase()); // JUAN GARCÍA
}

main();
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ralflorent/namefully",
"version": "2.1.0",
"version": "2.2.0",
"description": "Handle personal names in a particular order, way, or shape.",
"keywords": ["format", "parse", "human", "personal", "family", "name"],
"exports": "./src/index.ts",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "namefully",
"version": "2.1.0",
"version": "2.2.0",
"description": "Handle personal names in a particular order, way, or shape.",
"author": "Ralph Florent",
"license": "MIT",
Expand Down Expand Up @@ -30,7 +30,7 @@
"format": "prettier --write src example",
"lint": "eslint src",
"test": "jest",
"test:cov": "jest --collectCoverage",
"test:cov": "jest --coverage",
"prebuild": "shx rm -rf dist",
"build:esm": "tsc -p tsconfig.json",
"build:cjs": "tsc -p tsconfig.cjs.json",
Expand Down
32 changes: 28 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![JSR Version][jsr-version]][jsr-url]
[![CI build][ci-img]][ci-url]
[![MIT License][license-img]][license-url]
[![DeepWiki][deepwiki-img]][deepwiki-url]

Human name handling made easy.
[Try it live](https://stackblitz.com/edit/namefully).
Expand Down Expand Up @@ -204,6 +205,24 @@ const name = new Namefully(
);
```

### mono

`boolean | Namon` - default: `false`

Enables support for mononyms (i.e., single word names). You may also use `Namon`
to assign which name type is being used to represent the mononym.

> You should know that this goes against the original design philosophy of the library,
> which is intentionally opinionated around shaping and organizing multiple name components.
> Treating a single token as a "full" name makes a lot of the existing API semantics
> somewhat meaningless.

```ts
const name = new Namefully('Plato', { mono: true }); // throws an exception without this flag.
console.log(name.full); // Plato
console.log(name.initials()); // ['P']
```

To sum it all up, the default values are:

```ts
Expand All @@ -213,7 +232,8 @@ To sum it all up, the default values are:
title: Title.UK,
ending: false,
bypass: true,
surname: Surname.FATHER
surname: Surname.FATHER,
mono: false
}
```

Expand All @@ -227,8 +247,10 @@ import { Config, FullName, Namefully, Parser } from 'namefully';
// Suppose you want to cover this '#' separator
class SimpleParser extends Parser<string> {
parse(options: Partial<Config>): FullName {
const [firstName, lastName] = this.raw.split('#');
return FullName.parse({ firstName, lastName }, Config.merge(options));
const [fn, ln] = this.raw.split('#', 2);
return new FullName(options)
.setFirstName(fn.trim())
.setLastName(ln.trim());
}
}

Expand Down Expand Up @@ -303,7 +325,7 @@ So, this utility understands the name parts as follows:

`namefully` does not support certain use cases:

- mononame: `Plato` - a workaround is to set the mononame as both first and last name;
- mononame: `Plato` - enable the mononym flag `Config.mono` to support this.
- nickname: `Dwayne "The Rock" Johnson` - use custom parser instead.
- multiple prefixes or suffixes: `Prof. Dr. Einstein`.

Expand All @@ -326,6 +348,8 @@ The underlying content of this utility is licensed under [MIT License][license-u
[ci-url]: https://github.com/ralflorent/namefully/actions/workflows/ci.yml
[license-img]: https://img.shields.io/npm/l/namefully
[license-url]: https://opensource.org/licenses/MIT
[deepwiki-img]: https://deepwiki.com/badge.svg
[deepwiki-url]: https://deepwiki.com/ralflorent/namefully

[contributing-url]: https://github.com/ralflorent/namefully/blob/main/CONTRIBUTING.md
[examples]: https://github.com/ralflorent/namefully/tree/main/example
Expand Down
5 changes: 3 additions & 2 deletions src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ export class NameBuilder extends Builder<Name, Namefully> {
this.prebuild?.();

const names = [...this.queue];
ArrayNameValidator.create().validate(names);
const config = Config.merge(options as Partial<Config>);
if (!config.mono) ArrayNameValidator.create().validate(names);

this.instance = new Namefully(names, options);
this.instance = new Namefully(names, config);
this.postbuild?.(this.instance);

return this.instance;
Expand Down
28 changes: 14 additions & 14 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NameOrder, Separator, Title, Surname } from './types.js';
import { NameOrder, Namon, Separator, Title, Surname } from './types.js';

const defaultName = 'default';
const copyAlias = '_copy';
Expand Down Expand Up @@ -39,6 +39,7 @@ export class Config {
#ending: boolean;
#bypass: boolean;
#surname: Surname;
#mono: boolean | Namon;

/** Cache for multiple instances. */
private static cache = new Map<string, Config>();
Expand Down Expand Up @@ -91,6 +92,11 @@ export class Config {
return this.#surname;
}

/** Whether to parse a single word name as a mononym. */
get mono(): boolean | Namon {
return this.#mono;
}

/** The name of the cached configuration. */
get name(): string {
return this.#name;
Expand All @@ -104,6 +110,7 @@ export class Config {
ending = false,
bypass = true,
surname = Surname.FATHER,
mono: boolean | Namon = false,
) {
this.#name = name;
this.#orderedBy = orderedBy;
Expand All @@ -112,6 +119,7 @@ export class Config {
this.#ending = ending;
this.#bypass = bypass;
this.#surname = surname;
this.#mono = mono;
}

/**
Expand Down Expand Up @@ -139,6 +147,7 @@ export class Config {
config.#ending = other.ending ?? config.ending;
config.#bypass = other.bypass ?? config.bypass;
config.#surname = other.surname ?? config.surname;
config.#mono = other.mono ?? config.mono;
return config;
}
}
Expand All @@ -153,14 +162,15 @@ export class Config {
* be named `default_copy`.
*/
copyWith(options: Partial<Config> = {}): Config {
const { name, orderedBy, separator, title, ending, bypass, surname } = options;
const { name, orderedBy, separator, title, ending, bypass, surname, mono } = options;
const config = Config.create(this.#genNewName(name ?? this.name + copyAlias));
config.#orderedBy = orderedBy ?? this.orderedBy;
config.#separator = separator ?? this.separator;
config.#title = title ?? this.title;
config.#ending = ending ?? this.ending;
config.#bypass = bypass ?? this.bypass;
config.#surname = surname ?? this.surname;
config.#mono = mono ?? this.mono;
return config;
}

Expand All @@ -177,21 +187,11 @@ export class Config {
this.#ending = false;
this.#bypass = true;
this.#surname = Surname.FATHER;
this.#mono = false;
Config.cache.set(this.name, this);
}

/**
* Alters the name order between the first and last name, and rearrange the
* order of appearance of a name set.
* @deprecated use `update()` method instead.
*/
updateOrder(orderedBy: NameOrder): void {
this.update({ orderedBy });
}

/**
* Allows the possibility to alter some options after creating a name set.
*/
/** Allows the possibility to alter behavior-related options after creating a name set. */
update({ orderedBy, title, ending }: Partial<Pick<Config, 'orderedBy' | 'title' | 'ending'>>): void {
const config = Config.cache.get(this.name);
if (!config) return;
Expand Down
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const VERSION = '2.1.0';
export const VERSION = '2.2.0';
export const MIN_NUMBER_OF_NAME_PARTS = 2;
export const MAX_NUMBER_OF_NAME_PARTS = 5;
export const ALLOWED_FORMAT_TOKENS = ` .,_-()[]<>'"bBfFlLmMnNoOpPsS$`;
export const ZERO_WIDTH_SPACE = String.fromCharCode(8203);
Loading