Skip to content

Commit c4fe7fd

Browse files
committed
add types and finalize docs
1 parent b365ca3 commit c4fe7fd

File tree

5 files changed

+137
-73
lines changed

5 files changed

+137
-73
lines changed

README.md

Lines changed: 122 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
# ![messagepipe](./assets/logo-light.svg#gh-dark-mode-only)![messagepipe](./assets/logo-dark.svg#gh-light-mode-only)
22

3+
<a href="https://github.com/Marcisbee/messagepipe/actions">
4+
<img alt="CI" src="https://img.shields.io/github/workflow/status/Marcisbee/messagepipe/CI?style=flat-square" />
5+
</a>
6+
<a href="https://snyk.io/test/github/Marcisbee/messagepipe">
7+
<img alt="snyk" src="https://img.shields.io/snyk/vulnerabilities/github/Marcisbee/messagepipe?style=flat-square" />
8+
</a>
9+
<a href="https://www.npmjs.com/package/messagepipe">
10+
<img alt="npm" src="https://img.shields.io/npm/v/messagepipe.svg?style=flat-square" />
11+
</a>
12+
<a href="https://bundlephobia.com/result?p=messagepipe">
13+
<img alt="package size" src="https://img.shields.io/bundlephobia/minzip/messagepipe?style=flat-square" />
14+
</a>
15+
16+
<br />
17+
318
Formats message strings with number, date, plural, and select placeholders to create localized messages.
419

5-
* **Small.** Between 0.6 kilobytes and 1 kilobytes (minified and gzipped).
20+
* **Small.** Between 700 bytes and 1.3 kilobytes (minified and gzipped).
621
Zero dependencies.
7-
* **Fast.** Does absolute minimum amount of computations necessary.
22+
* **Fast.** Does absolute minimum amount of computations necessary. View [benchmarks](#benchmarks).
823
* **Tree Shakable.** Includes separate global transformers config that can be omitted.
924
* **Pipe syntax.** Transformer functions can customized and chained.
1025
* **View framework support.** Use React/Preact etc. components as transformers.
@@ -58,28 +73,28 @@ npm install messagepipe
5873
-message
5974
```
6075

61-
In one `message` there can only be one `selector`, but there can be unlimited number of `pipes` with unlimited number of `arguments` in them. It is possible to build dynamic `selector` (meaning `message` can be inside it), but it is not possible to build dynamic `pipes` except for `argument values`.
76+
In one [message](#message) there can only be one [selector](#selector), but there can be unlimited number of [pipes](#pipe) with unlimited number of [arguments](#argument) in them. It is possible to build dynamic [selector](#selector) (meaning [message](#message) can be inside it), but it is not possible to build dynamic [pipes](#pipe) except for [argument values](#argument).
6277

6378
So both of these are valid:
6479
1. `"Hello {agents.{index}.fistName}"`;
6580
2. `"{a} + {b} = {a | sum, sequence:{b}}"`.
6681
_(Note: sum is a custom transformer in this case)_.
6782

68-
### `message`
69-
Contains everything between `{` and `}` that in large includes 1 `selector` and n `pipes`.
83+
### Message
84+
Contains everything between `{` and `}` that in large includes 1 [selector](#selector) and n [pipes](#pipe).
7085

71-
### `selector`
86+
### Selector
7287
String value that points to value from given props object e.g.:
7388
- `"{name}"` + `{ name: 'john' }` => `"john"`;
7489
- `"{agents[0].name}"` + `{ agents: [{ name: 'john' }] }` => `"john"`
7590

76-
### `pipe`
91+
### Pipe
7792
A combination of 1 `transformer` and n `arguments` e.g.:
7893
- `"{name | capitalize}"`;
7994
- `"{name | reverse | capitalize}"`;
8095
- `"{a | sum, sequence:1, double}"` _(Note: argument "double" will pass `true` value to "sum" transformer)_.
8196

82-
### `transformer`
97+
### Transformer
8398
Function that can transform value that is being selected from given props.
8499

85100
Lets define "capitalize" transformer that would uppercase the first letter of any string:
@@ -99,7 +114,7 @@ const msgPipe = MessagePipe({
99114

100115
This would be valid use case for it: `"Greetings {name | capitalize}!"`.
101116

102-
### `argument`
117+
### Argument
103118
To allow more functionality, we can use arguments, that are passed to transformer function.
104119

105120
```ts
@@ -116,42 +131,119 @@ We can now use it like this:
116131
We can stack any number of arguments separated by `,` (comma).
117132

118133
### Global transformers
134+
There are number of already provided transformers, but they MUST be added to MessagePipe function when initiating. This is by design to help with tree shaking (although they don't contribute that much to package size, if there are additions in future, that won't hurt anyone).
119135

120136
### `defaultTransformers`
137+
```ts
138+
function defaultTransformers(): MessagePipeTransformers
139+
```
121140

122-
- #### select
123-
@TODO: Write about this
141+
#### select
142+
Selects what text to show based on incoming value.
124143

125-
- #### json
126-
@TODO: Write about this
144+
```ts
145+
const msg = compile('{gender | select, male:"He", female:"She", other:"They"} liked this.')
146+
147+
msg({ gender: 'male' }) // "He liked this"
148+
msg({ gender: 'female' }) // "She liked this"
149+
msg({ }) // "They liked this"
150+
```
151+
152+
#### json
153+
Runs value through `JSON.stringify`.
127154

128155
### `intlTransformers`
156+
```ts
157+
function intlTransformers(locale?: string): MessagePipeTransformers
158+
```
159+
160+
#### number
161+
Formats numbers using [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). All options are available as arguments in pipes.
162+
163+
```ts
164+
const msg = compile('{price | number}')
165+
166+
msg({ price: 123456.789 }) // "123,456.789"
167+
```
168+
169+
```ts
170+
const msg = compile('Price: {price | number, style:"currency", currency:"EUR"}')
171+
172+
msg({ price: 123 }) // "Price: 123,00 €"
173+
```
174+
175+
#### plural
176+
Selects correct text to show based on [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules). All options are available as arguments in pipes.
177+
178+
```ts
179+
const msg = compile('I have {fruits | plural, one:"1 fruit", other:"# fruits"}')
180+
181+
msg({ fruits: 0 }) // "I have 0 fruits"
182+
msg({ fruits: 1 }) // "I have 1 fruit"
183+
msg({ fruits: 2 }) // "I have 2 fruits"
184+
```
185+
186+
#### date
187+
Formats date using [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). All options are available as arguments in pipes.
129188

130-
- #### number
131-
@TODO: Write about this
189+
```ts
190+
const msg = compile('Todays date {now | date}')
132191
133-
- #### plural
134-
@TODO: Write about this
192+
msg({ now: new Date('1977-05-25') }) // "Todays date 25/05/1977"
193+
```
135194

136-
- #### date
137-
@TODO: Write about this
195+
#### time
196+
Formats time using [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). All options are available as arguments in pipes.
138197

139-
- #### time
140-
@TODO: Write about this
198+
```ts
199+
const msg = compile('Currently it is {now | time}')
141200
142-
- #### relativeTime
143-
@TODO: Write about this
201+
msg({ now: new Date('1983-05-25 16:42') }) // "Currently it is 16:42:00"
202+
```
144203

145204
## API
146205

147-
### @TODO: `MessagePipe`
148-
#### @TODO: `compile`
206+
### `MessagePipe`
207+
This is the main function that takes in all the transformers that will be available to all the messages.
149208

150-
#### @TODO: `compileRaw`
209+
```ts
210+
function MessagePipe(transformers?: MessagePipeTransformers): {
211+
compileRaw(message: string): (props?: Record<string, any>) => string[]
212+
compile(message: string): (props?: Record<string, any>) => string
213+
}
214+
```
151215
152-
## Framework integration
216+
Example usage:
217+
218+
```ts
219+
const messagePipe = MessagePipe({
220+
hello: (value) => `Hello ${value}!`,
221+
})
222+
```
223+
224+
Now all the messages that get compiled from `messagePipe` can use this transformer like so `"{name | hello}"`.
225+
226+
#### `compile`
227+
This is where given [message](#message) gets parsed and prepared for usage. It is very efficient compiler that does only 1 pass and prepares very tiny and performant function from it.
228+
229+
Given this message `"Hello {name | capitalize}!"`, compiler will output this function `(a) => "Hello " + capitalize(a.name) + "!"` and that is the only thing that runs when executing it. No hidden performance penalties.
153230
154-
Works with React and Preact out of the box. Just swap out `compile` with `compileRaw` and good to go. This works because it returns raw array of values that was the output of selectors and transformers.
231+
#### `compileRaw`
232+
This is practically the same as [compile](#compile) but instead of it returning one string, it returns array of all of the things as a separate chunks so that this compiler can be used as part of React component for example.
233+
234+
So from the example that was before, the output of that message would be `(a) => ["Hello ", capitalize(a.name), "!"]`.
235+
236+
## Benchmarks
237+
It is necessary for me that this library is as small and as fast as possible. Since this library compares directly with MessageFormat, I treated both as equal in benchmarks.
238+
239+
Message | MessageFormat | MessagePipe | Improvement
240+
|-|-|-|-|
241+
["Wow"](https://fastbench.dev/Format-%22Wow%22-JtYOF/0) | 926,368 ops/s | __1,847,253 ops/s__ | 2x
242+
["Hello {planet}"](https://fastbench.dev/Format-%22Hello-planet%22-tKWLnH/0) | 560,131 ops/s | __1,024,051 ops/s__ | 1.8x
243+
[select transformer](https://fastbench.dev/Format-select-MovWE/0) | 209,513 ops/s | __337,226 ops/s__ | 1.6x
244+
245+
## Framework integration
246+
Works with React and Preact out of the box. Just swap out [`compile`](#compile) with [`compileRaw`](#compile) and good to go. This works because it returns raw array of values that was the output of selectors and transformers.
155247
156248
```tsx
157249
import { MessagePipe } from 'messagepipe'
@@ -171,50 +263,14 @@ function App() {
171263
} // => "<div>Hello <a href="...">john</a>!</div>"
172264
```
173265
174-
Since we used `compileRaw`, library would output something like this: `['Hello ', [ReactElement], '!']`.
266+
Since we used [compileRaw](#compileraw), library would output something like this: `['Hello ', [ReactElement], '!']`.
175267
176268
This will work with any kind of framework or custom library.
177269
178270
# Motivation
179271
I was used to messageformat being the go to for this sort of stuff, but it has big flaws in the spec and library maintainers obviously wouldn't want to deviate from it. So the goal for messagepipe was to create NEW spec that solves all of the issues with it + must be faster & smaller.
180272
181-
One immediate flaw that messagepipe solves is ability to select nested values.
182-
183-
## @TODO:
184-
185-
- [ ] Write up spec
186-
- [x] General idea
187-
- `Hello {name}!` => `"Hello John!"`
188-
- [x] Selecting shallow values
189-
- `{name}` => `"John"`
190-
- [x] Selecting nested values
191-
- `{agent.name}` => `"John"`
192-
- [x] Selecting single value from array
193-
- `{agent.0.name}` => `"John"`
194-
- [x] Selecting multiple values from array
195-
- `{agent.*.name}` => `["John", "Lars"]`
196-
- [x] Using transformers
197-
- `{name | uppercase}` => `uppercase("John")`
198-
- [x] Using chained transformers
199-
- `{name | reverse | uppercase}` => `uppercase(reverse("John"))`
200-
- [x] Passing parameters to transformer
201-
- `{name | json, space:2}` => `json("John", {space:2})`
202-
- `{name | remove, text:"name", caseSensitive:true}` => `remove("John", {text:"name", caseSensitive:true})`
203-
- [ ] Escaping `"`, "|", `,` and `:` in argument names
204-
- `{name | select, "a\"2":2}`
205-
- `{name | select, "a:2":2}`
206-
- `{name | select, "a|2":2}`
207-
- `{name | select, "a,2":2}`
208-
- [ ] Escape `{` & `}`
209-
- `Escaped: \\{ \\}` => `"Escaped: { }"`
210-
- [x] Using selector inside selector
211-
- Nested selector in selector can only be used to define path of the selector
212-
- `{agent.{index}.name}` = `{agent[2].name}` => `"Lars"`
213-
- `{agent.{index}}` = `{agent[2]}` => `{"name":"Lars"}`
214-
- `{{index}}` = `{2}` => `undefined`
215-
- [x] Using selector inside pipes
216-
- Nested selector in pipes can only be used to construct argument values
217-
- `{name | json, space:{space}}` => `json("John", {space:2})`
273+
One immediate flaw that MessagePipe solves is ability to select nested values and build dynamic messages.
218274
219275
# License
220276
[MIT](LICENCE) &copy; [Marcis Bergmanis](https://twitter.com/marcisbee)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "messagepipe",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"description": "Formats message strings with number, date, plural, and select placeholders to create localized messages",
55
"main": "dist/index.js",
66
"module": "dist/index.esm.js",

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { defaultTransformers, intlTransformers } from './transformers';
2-
export { MessagePipe } from './messagepipe';
2+
export { MessagePipe, MessagePipeTransformer, MessagePipeTransformers } from './messagepipe';

src/messagepipe.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { parser } from './parser';
22

3-
export function MessagePipe(transformers: Record<string, any> = {}) {
4-
function compileRaw(message: string) {
3+
export interface MessagePipeTransformer<Input = string, Output = string> {
4+
(value: Input, options?: Record<string, any>): Output;
5+
}
6+
7+
export interface MessagePipeTransformers {
8+
[key: string]: MessagePipeTransformer;
9+
}
10+
11+
export function MessagePipe(transformers: MessagePipeTransformers = {}) {
12+
function compileRaw<Output = string[]>(message: string): (props?: Record<string, any>) => Output {
513
return Function('b', 'return (a)=>[' + parser(message).join(',') + ']')(transformers);
614
}
715

8-
function compile(message: string) {
16+
function compile<Output = string>(message: string): (props?: Record<string, any>) => Output {
917
return Function('b', 'return (a)=>' + parser(message).join('+'))(transformers);
1018
}
1119

0 commit comments

Comments
 (0)