npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xeplr/rules

v1.0.6

Published

Conditional formatting, as a condition language. Renderer-agnostic: it answers whether a rule matches a value and how to format one. What to DO about a match belongs to whatever is drawing.

Downloads

924

Readme

@xeplr/rules

Conditional formatting, as a condition language. A rule says "when margin is under zero"; this package answers whether a rule matches a value, picks the first rule that matches, and formats numbers. What to do about a match — colour a bar, shade a table cell, style a metric — belongs to whatever is drawing, and is not interpreted here.

Use it anywhere a condition on data has to drive presentation: @xeplr/ui-charts compiles rules into chart callbacks, and a table cell, label or metric can use resolveThen directly. It has no dependencies, so a table asking "is this less than zero" does not have to import a charting library.

Install

npm i @xeplr/rules

No dependencies, no peers. Ships CommonJS and ESM entry points.

Quick start

import { resolveThen, formatNumber } from '@xeplr/rules'

const rules = [
  { field: 'margin', op: 'lt', value: 0,  then: { color: '#c00', fontWeight: 'bold' } },
  { field: 'margin', op: 'gt', value: 10, then: { color: '#0a0' } }
]
const readFrom = (row) => (rule) => row[rule.field]

resolveThen(rules, readFrom({ margin: -4 }))   // → { color: '#c00', fontWeight: 'bold' }
resolveThen(rules, readFrom({ margin: 12 }))   // → { color: '#0a0' }
resolveThen(rules, readFrom({ margin: 5 }))    // → null   (nothing matched)

formatNumber(1234.5, { style: 'currency', currency: 'USD', decimals: 2 })   // → '$1,234.50'

API

| export | signature | does | |---|---|---| | OPERATORS | Array<{ key, label, numeric?, second?, noValue? }> | The 12 operators, as data. | | operator | (key) → operator \| null | The OPERATORS entry for a key, or null. | | operatorsFor | (isNumeric) → operator[] | All operators when true; only the non-numeric ones when false. Always a new array. | | matches | (rule, actual) → boolean | Whether rule matches the value actual. Never throws. | | firstMatch | (rules, valueOf) → rule \| null | The first rule in rules for which matches(rule, valueOf(rule)) is true. | | resolveThen | (rules, valueOf) → object \| null | The first match's then ({} if it has none), or null if nothing matched. | | formatNumber | (value, format) → string \| null | A number formatted with Intl.NumberFormat; null for a non-number or no format. | | columnIsNumeric | (rows, key, sampleSize = 20) → boolean | Whether a column's values can answer a numeric comparison, judged from the first sampleSize rows. |

valueOf(rule) is how the caller says where a rule's field is read from — a row for a table, a parameter scope for a label (scope[':region']), a widget's one row for a metric. The condition never needs to know.

A rule

{ field, op, value, value2?, target?, series?, then: { … } }

| key | read by this package | | |---|---|---| | op | yes | An operator key, below. | | value | yes | What to compare against. Not used by isEmpty / notEmpty. | | value2 | yes | Second bound for between. | | then | returned by resolveThen | What to do about a match. Not interpreted — its vocabulary belongs to the renderer. | | field | no | The column to test — any column in the data, not only a displayed one. Your valueOf reads it. | | target, series | no | Part of the rule shape for renderers; ignored here. |

Operators

| key | label | flags | matches when | |---|---|---|---| | lt | is less than | numeric | both sides are numbers and actual < value | | lte | is at most | numeric | … actual <= value | | gt | is more than | numeric | … actual > value | | gte | is at least | numeric | … actual >= value | | between | is between | numeric, second | actual, value and value2 are all numbers and actual is within them, inclusive. Bounds may be given in either order. | | eq | is | | numbers: actual === value; otherwise case-insensitive text equality | | neq | is not | | the negation of eq | | contains | contains | | case-insensitive substring | | startsWith | starts with | | case-insensitive prefix | | endsWith | ends with | | case-insensitive suffix | | isEmpty | is blank | noValue | null, undefined, or a string that is empty after trimming | | notEmpty | is not blank | noValue | not blank |

Flags, for building a rule editor: numeric — only answers over numbers; second — needs value2; noValue — needs no value.

How values are compared

| rule | effect | |---|---| | A value is a number if it is a finite number, or a non-blank string for which Number(s) is finite | '2.5', '0.0', '1e3' count; NaN, Infinity, '' do not | | Numeric operators need both sides to be numbers | matches({ op: 'gt', value: 5 }, 'IPD') → false — not an error, not true | | eq / neq compare as numbers when both sides are | { op: 'eq', value: 0 } matches 0, '0' and '0.0' | | Otherwise text: null / undefined become '', everything else String(v).toLowerCase() | { op: 'eq', value: 'ipd' } matches 'IPD'; { op: 'eq', value: '' } matches null | | 0 is not blank | isEmpty is false for 0 | | Unknown op, a rule with no op, or no rule | false |

formatNumber(value, format)

| format key | default | | |---|---|---| | style | 'decimal' | 'decimal', 'currency' or 'percent' (anything else is treated as decimal) | | currency | 'USD' | ISO code, used when style: 'currency' | | decimals | Intl's default | sets both minimum and maximum fraction digits | | useGrouping | true | false to drop thousands separators | | locale | runtime default | passed to Intl.NumberFormat | | prefix, suffix | '' | added around the formatted number |

formatNumber(1234.5, { decimals: 2 })                                   // '1,234.50'
formatNumber(1234.5, { decimals: 0, useGrouping: false })               // '1235'
formatNumber(0.185, { style: 'percent', decimals: 1 })                  // '18.5%'   (percent multiplies by 100)
formatNumber(12, { decimals: 0, prefix: '~', suffix: ' units' })        // '~12 units'
formatNumber('IPD', { decimals: 2 })                                    // null
formatNumber(5, null)                                                   // null

Numeric strings are formatted like numbers. If Intl.NumberFormat throws (e.g. an invalid currency code), the result is String(n) with prefix and suffix.

columnIsNumeric(rows, key, sampleSize)

Looks at rows.slice(0, sampleSize || 20):

| sample | result | |---|---| | every non-blank value is a number (numeric strings count) | true | | any non-blank value is not a number | false | | rows present, but every value blank | false | | no rows ([], undefined) | true — offer every operator rather than hiding half the panel |

Pair it with operatorsFor(columnIsNumeric(rows, key)) to build the operator picker.

Errors

Nothing in this package throws on bad input. matches is total: a missing rule, an unknown operator or a non-numeric value in a numeric comparison is false. firstMatch / resolveThen accept undefined for rules. formatNumber returns null rather than NaN.

CommonJS / ESM

package.json exports sends each loader to a file with real exports of its kind:

| | resolves to | |---|---| | require('@xeplr/rules') | index.js | | import … from '@xeplr/rules' | index.mjs (also module) |

const { matches, OPERATORS } = require('@xeplr/rules')
import { matches, OPERATORS } from '@xeplr/rules'

Both re-export one source, src/rules.js. There is no default export on the ESM side.

Rules the code enforces

| rule | why | |---|---| | then is never interpreted | A chart sets ECharts channels, a table sets CSS, a DOM widget sets a style object — none share an implementation. Only the target knows what a legal property is. | | First matching rule wins | The convention every caller follows (firstMatch). The order of the list is the caller's decision. | | resolveThen returns null for no match, {} for a match with no then | A caller merging styles needs to tell "no rule applied" from "a rule applied and asked for nothing". | | Operators are data, flagged numeric | A UI builds its picker from them without a second list to keep in step, and does not offer "is more than" on a column of names — a control that can never be true. | | Numeric when both sides are numbers, text otherwise | Several database drivers return decimals as strings; refusing to compare them would hide exactly the columns most likely to want a rule. | | An unimplemented operator is not a match | Treating it as one would style rows for a condition that was never evaluated. | | Blank is null, undefined or whitespace — not 0 | Treating zero as absent hides every zero row. | | columnIsNumeric judges from rows, not a schema | The rows are what will actually be tested. Blanks say nothing either way. | | formatNumber lives here and uses Intl | "Two decimals, in pounds" is the same instruction wherever it lands; currency symbols, grouping and negatives differ by locale and are not worth reimplementing per surface. | | src/rules.js has no require or import (a test asserts it) | The package exists so any renderer can import it without dragging anything along. | | index.js is a static object literal; index.mjs has real ESM exports | Rollup and Node's lexer each guess CommonJS named exports differently — named imports failed in a bundle while passing under test, and vice versa. Separate entries remove the guessing. |

Tests

npm test

Runs node --test test/*.test.js — 16 tests with Node's built-in runner, no dependencies.

License

MIT