@jackens/nnn
v2026.11.17
Published
Jackens’ JavaScript helpers.
Maintainers
Readme
nnn
A collection of Jackens’ JavaScript helper utilities (version: 2026.11.17).
Installation
bun i @jackens/nnnor
npm i @jackens/nnnUsage
import {
type CNode,
type HArgs,
type HArgs1,
c,
h,
rwd,
s,
svgUse,
} from '@jackens/nnn' // or './node_modules/@jackens/nnn/nnn.js'Exports
CNode: Represents a CSS rule node for thechelper. Keys are CSS properties or nested selectors.HArgs: Tuple argument type for thehandshelpers.HArgs1: Single argument type for thehandshelpers.c: A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.h: A lightweight HyperScript-style helper for creating and modifyingHTMLElements (see alsos).rwd: A responsive web design helper that generates CSS rules for a grid-like layout of floating elements.s: A lightweight HyperScript-style helper for creating and modifyingSVGElements (see alsoh).svgUse: Shorthand for creating an<svg>element with a<use>child referencing a symbol (e.g. an icon) by ID.
CNode
type CNode = {
[attributeOrSelector: string]: string | number | CNode | null | undefined
}Represents a CSS rule node for the c helper. Keys are CSS properties or nested selectors.
HArgs
type HArgs = [string | Node, ...HArgs1[]]Tuple argument type for the h and s helpers.
HArgs1
type HArgs1 = Record<PropertyKey, unknown> | null | undefined | Node | string | number | HArgsSingle argument type for the h and s helpers.
c
const c: (node: CNode, splitter?: string) => stringA minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.
node
An object describing CSS rules.
Keys are selectors or at-rules; values are either CSS property values or nested rule objects.
Nested selectors are appended to the parent selector as is (e.g. '.c' inside a → a.c), so a leading space is needed for a descendant combinator (e.g. ' .c' → a .c).
If a nested selector contains &, every & is replaced with the parent selector instead (e.g. '&:hover' → a:hover, '.c &' → .c a).
A selector key may contain comma-separated alternatives (e.g. '.b,.c'); when nested, each alternative is combined with each parent alternative (cartesian product), producing a comma-joined selector list (e.g. 'a.b,a.c').
Commas inside brackets or quotes (e.g. ':is(.b,.c)', '[title="b,c"]') do not separate alternatives.
Whitespace after a separating comma belongs to the next alternative, so like a leading space of the first one it makes a descendant combinator when nested (e.g. ' .b, .c' → a .b,a .c, '.b, .c' → a.b,a .c); leading whitespace of a resulting selector is ignored (e.g. 'h1, h2' → h1,h2, '.b, &:hover' → a.b,a:hover).
At-rules (e.g. '@media …') wrap their nested rules; an at-rule nested in a selector wraps that selector (e.g. { a: { '@media(…)': { color: 'red' } } } → @media(…){a{color:red}}).
Inside a top-level :root rule nested keys are concatenated into property names (e.g. { ':root': { '--': { bg: 'red' } } } → :root{--bg:red}).
Properties without a selector (top-level or directly in a top-level at-rule, e.g. @font-face) are emitted as bare declarations.
null/undefined values are skipped.
splitter
A delimiter used to create unique keys (default: '$$').
The substring from splitter to the end of a key is ignored (e.g. src$$1 → src).
An empty splitter disables this feature.
Returns
A CSS string representing the compiled rules.
Usage Examples
Nested selectors split a rule while preserving property order:
const actual = c({
a: {
color: 'red',
margin: 1,
'.c': { margin: 2, padding: 2 },
padding: 1,
},
})
const expected = `
a{
color:red;
margin:1
}
a.c{
margin:2;
padding:2
}
a{
padding:1
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Selectors can be nested at any depth:
const actual = c({
a: {
'.b': {
color: 'red',
margin: 1,
'.c': { margin: 2, padding: 2 },
padding: 1,
},
},
})
const expected = `
a.b{
color:red;
margin:1
}
a.b.c{
margin:2;
padding:2
}
a.b{
padding:1
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)At-rules, keyframes, media queries and repeated keys with the $$ suffix:
const actual = c({
'@font-face$$1': {
'font-family': 'Jackens',
src$$1: 'url(otf/jackens.otf)',
src$$2: "url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg')",
'font-weight': 'normal',
'font-style': 'normal',
},
'@font-face$$2': {
'font-family': 'C64',
src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
},
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
div: {
border: 'solid red 1px',
'.c1': { 'background-color': '#000' },
' .c1': { 'background-color': 'black' },
'.c2': { 'background-color': 'rgb(0,0,0)' },
},
'@media(min-width:200px)': {
div: { margin: 0, padding: 0 },
span: { color: '#000' },
},
})
const expected = `
@font-face{
font-family:Jackens;
src:url(otf/jackens.otf);
src:url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg');
font-weight:normal;
font-style:normal
}
@font-face{
font-family:C64;
src:url(fonts/C64_Pro_Mono-STYLE.woff)
}
@keyframes spin{
0%{
transform:rotate(0deg)
}
100%{
transform:rotate(360deg)
}
}
div{
border:solid red 1px
}
div.c1{
background-color:#000
}
div .c1{
background-color:black
}
div.c2{
background-color:rgb(0,0,0)
}
@media(min-width:200px){
div{
margin:0;
padding:0
}
span{
color:#000
}
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Comma-separated nested selectors are expanded:
const actual = c({
a: {
'.b,.c': {
margin: 1,
'.d': {
margin: 2,
},
},
},
})
const expected = `
a.b,a.c{
margin:1
}
a.b.d,a.c.d{
margin:2
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Comma-separated top-level selectors are expanded in nested rules:
const actual = c({
'.b,.c': {
margin: 1,
'.d': {
margin: 2,
},
},
})
const expected = `
.b,.c{
margin:1
}
.b.d,.c.d{
margin:2
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Comma-separated selectors at both levels produce all combinations:
const actual = c({
'.a,.b': {
margin: 1,
'.c,.d': {
margin: 2,
},
},
})
const expected = `
.a,.b{
margin:1
}
.a.c,.a.d,.b.c,.b.d{
margin:2
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)CSS custom properties from nested -- objects:
const actual = c({
':root$$monokai': {
'--': {
bg: '#faf4f2',
fg: '#29242a',
comment: '#918c8e',
identifier: {
1: '#7058be',
2: '#269d69',
3: '#1c8ca8',
4: '#29242a',
},
keyword: {
1: '#e14775',
2: '#7058be',
3: '#1c8ca8',
},
number: '#7058be',
operator: '#e14775',
punctuation: '#918c8e',
string: '#cc7a0a',
},
},
'@media only screen and (prefers-color-scheme: dark)$$monokai': {
':root': {
'--': {
bg: '#2d2a2e',
fg: '#fcfcfa',
comment: '#727072',
identifier: {
1: '#ae81ff',
2: '#a9dc76',
3: '#66d9ef',
4: '#fcfcfa',
},
keyword: {
1: '#ff6188',
2: '#ae81ff',
3: '#66d9ef',
},
number: '#ae81ff',
operator: '#ff6188',
punctuation: '#727072',
string: '#ffd866',
},
},
},
})
const expected = `
:root{
--bg:#faf4f2;
--fg:#29242a;
--comment:#918c8e;
--identifier1:#7058be;
--identifier2:#269d69;
--identifier3:#1c8ca8;
--identifier4:#29242a;
--keyword1:#e14775;
--keyword2:#7058be;
--keyword3:#1c8ca8;
--number:#7058be;
--operator:#e14775;
--punctuation:#918c8e;
--string:#cc7a0a
}
@media only screen and (prefers-color-scheme: dark){
:root{
--bg:#2d2a2e;
--fg:#fcfcfa;
--comment:#727072;
--identifier1:#ae81ff;
--identifier2:#a9dc76;
--identifier3:#66d9ef;
--identifier4:#fcfcfa;
--keyword1:#ff6188;
--keyword2:#ae81ff;
--keyword3:#66d9ef;
--number:#ae81ff;
--operator:#ff6188;
--punctuation:#727072;
--string:#ffd866
}
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)& is replaced with the parent selector:
const actual = c({
a: {
color: 'red',
'&:hover': { color: 'blue' },
'.dark &': { color: 'white' },
'.b,& .c': { margin: 1 },
},
})
const expected = `
a{
color:red
}
a:hover{
color:blue
}
.dark a{
color:white
}
a.b,a .c{
margin:1
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Commas inside brackets or quotes do not split selectors:
const actual = c({
':is(.a,.b)': {
'.c,.d': { margin: 1 },
'[title="x,y"]': { margin: 2 },
},
})
const expected = `
:is(.a,.b).c,:is(.a,.b).d{
margin:1
}
:is(.a,.b)[title="x,y"]{
margin:2
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Whitespace after a comma is kept, but leading whitespace of a resulting selector is ignored:
const actual = c({
'h1, h2': {
margin: 0,
' .a, .b': { margin: 1 },
'.c, .d , &:hover': { margin: 2 },
},
})
const expected = `
h1,h2{
margin:0
}
h1 .a,h1 .b,h2 .a,h2 .b{
margin:1
}
h1.c,h1 .d,h1:hover,h2.c,h2 .d,h2:hover{
margin:2
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)At-rules nested in selectors wrap these selectors:
const actual = c({
a: {
color: 'red',
'@media(min-width:600px)': {
color: 'blue',
'.b': { margin: 1 },
},
},
})
const expected = `
a{
color:red
}
@media(min-width:600px){
a{
color:blue
}
a.b{
margin:1
}
}
`.replace(/\n\s*/g, '')
expect(actual).toBe(expected)Declarations without a selector are separated from subsequent rules:
expect(c({ color: 'red', margin: 0 })).toBe('color:red;margin:0')
expect(c({ color: 'red', a: { margin: 0 }, padding: 0 })).toBe('color:red;a{margin:0}padding:0')null and undefined values are skipped:
const actual = c({
a: {
color: 'red',
margin: null,
padding: undefined,
'.b': null,
},
':root': {
'--': { bg: null, fg: 'black' },
},
})
expect(actual).toBe('a{color:red}:root{--fg:black}')Custom splitter (an empty one disables splitting):
expect(c({ a: { 'src#1': 'url(a.otf)', 'src#2': 'url(a.woff)' } }, '#')).toBe('a{src:url(a.otf);src:url(a.woff)}')
expect(c({ a: { b$$c: 1 } }, '')).toBe('a{b$$c:1}')h
const h: {
<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
<N extends Node>(node: N, ...args: HArgs1[]): N
(tag: string, ...args: HArgs1[]): HTMLElement
(node: Node, ...args: HArgs1[]): Node
}A lightweight HyperScript-style helper for creating and modifying HTMLElements (see also s).
tagOrNode
If a string, it is treated as the tag name for a new element.
If a Node, that node is modified in place.
args
Additional arguments processed as follows:
Object: maps attributes/properties.- Keys starting with
$set element properties (without the$prefix); if the value is a plain object (created by{…}orObject.create(null)), it is shallow-merged into the existing property instead of replacing it (useful e.g. for$style), other values (including arrays and class instances) replace the property. - Other keys set attributes: a key prefixed with
xlink:,xml:orxmlns:sets the attribute viasetAttributeNSin the corresponding namespace; any other key sets it viasetAttribute. A value oftruesets an empty attribute (e.g.checked=""), a value offalseremoves the attribute, a value ofnull/undefinedleaves the attribute unchanged.
- Keys starting with
null/undefined: ignored.Node: appended as a child.string/number: converted to aTextnode and appended.HArgsarray: processed recursively (creates or modifies a child node and appends it).
Returns
The created HTMLElement or the modified node.
Usage Examples
Creating elements, appending children and setting properties:
const b = h('b')
expect(b.outerHTML).toBe('<b></b>')
const i = h('i', 'text')
h(b, i)
expect(i.outerHTML).toBe('<i>text</i>')
expect(b.outerHTML).toBe('<b><i>text</i></b>')
h(i, { $className: 'some class' })
expect(i.outerHTML).toBe('<i class="some class">text</i>')
expect(b.outerHTML).toBe('<b><i class="some class">text</i></b>')Text content from strings, numbers and $innerText:
expect(h('span', 'text').outerHTML).toBe('<span>text</span>')
expect(h('span', { $innerText: 'text' }).outerHTML).toBe('<span>text</span>')
expect(h('span', '42').outerHTML).toBe('<span>42</span>')
expect(h('span', 42).outerHTML).toBe('<span>42</span>')style attribute vs $style property (string or object):
expect(h('div', { style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin:0;padding:0"></div>')
expect(h('div', { $style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin: 0px; padding: 0px;"></div>')
expect(h('div', { $style: { margin: 0, padding: 0 } }).outerHTML).toBe(
'<div style="margin: 0px; padding: 0px;"></div>'
)value attribute vs $value property:
const input1 = h('input', { value: 42 })
const input2 = h('input', { $value: '42' })
expect(input1.value).toBe('42')
expect(input2.value).toBe('42')
expect(input1.outerHTML).toBe('<input value="42">')
expect(input2.outerHTML).toBe('<input>')checked attribute vs $checked property:
const checkbox1 = h('input', {
type: 'checkbox',
checked: true,
})
const checkbox2 = h('input', {
type: 'checkbox',
$checked: true,
})
expect(checkbox1.checked).toBe(true)
expect(checkbox2.checked).toBe(true)
expect(checkbox1.outerHTML).toBe('<input type="checkbox" checked="">')
expect(checkbox2.outerHTML).toBe('<input type="checkbox">')Object values of $-prefixed properties are merged:
const div = h('div') as any
expect(div.key).toBeUndefined()
h(div, { $key: { one: 1 } })
expect(div.key).toEqual({ one: 1 })
h(div, { $key: { two: 2 } })
expect(div.key).toEqual({ one: 1, two: 2 })Class attribute, appending text and nested children given as arrays:
const elemWithClass = h('div', { class: 'test' })
expect(elemWithClass.getAttribute('class')).toBe('test')
const elemWithText = h('div', 'initial')
h(elemWithText, ' more')
expect(elemWithText.outerHTML).toBe('<div>initial more</div>')
const elemWithNested = h('div', ['span', 'hello'], ['b', 'world'])
expect(elemWithNested.outerHTML).toBe('<div><span>hello</span><b>world</b></div>')false removes an attribute, null and undefined leave it unchanged:
const input = h('input', { type: 'checkbox', checked: true, title: 'title' })
h(input, { checked: false, title: null, type: undefined })
expect(input.outerHTML).toBe('<input type="checkbox" title="title">')Only plain objects are merged into $-prefixed properties:
const date = new Date(0)
const div = h('div', { $date: date, $list: [1, 2] }) as any
expect(div.date).toBe(date)
h(div, { $list: [3] })
expect(div.list).toEqual([3])Namespaced attributes:
const div = h('div', { 'xml:lang': 'pl', 'ng:x': 1 })
expect(div.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
expect(div.getAttribute('ng:x')).toBe('1')Return types:
expectTypeOf(h('input')).toEqualTypeOf<HTMLInputElement>()
expectTypeOf(h('my-element')).toEqualTypeOf<HTMLElement>()
expectTypeOf(h(document.createTextNode(''))).toEqualTypeOf<Text>()rwd
const rwd: (
root: CNode,
selector: string,
cellWidthPx: number,
cellHeightPx: number,
...specs: [columns: number, width?: number, height?: number][]
) => voidA responsive web design helper that generates CSS rules for a grid-like layout of floating elements.
It modifies root in place: it sets base styles for selector and adds a @media(min-width:…px) rule for each breakpoint.
root
The CSS root object to populate (see c).
selector
The CSS selector for the grid item.
cellWidthPx
The base cell width in pixels (a positive number).
cellHeightPx
The base cell height in pixels (a positive number).
specs
An array of breakpoint specifications, each a tuple of positive integers:
columns: number of cells per row; the breakpoint ismin-width: cellWidthPx * columns(1modifies the base rule).width(optional, default1, at mostcolumns): number of horizontal cells the element spans.height(optional, default1): number of vertical cells the element spans.
Throws
A RangeError if any of the numeric arguments is out of range.
Usage Examples
Responsive grid cell widths and heights with media queries:
const style: CNode = {
body: {
margin: 0,
},
'.r6': {
border: 'solid red 1px',
'.no-border': {
border: 'none',
},
},
}
rwd(style, '.r6', 200, 50, [6], [3], [1, 1, 2])
expect(style).toEqual({
body: {
margin: 0,
},
'.r6': {
border: 'solid red 1px',
'.no-border': {
border: 'none',
},
'box-sizing': 'border-box',
display: 'block',
float: 'left',
width: '100%',
height: '100px',
},
'@media(min-width:600px)': {
'.r6': {
width: 'calc(100% / 3)',
height: '50px',
},
},
'@media(min-width:1200px)': {
'.r6': {
width: 'calc(50% / 3)',
height: '50px',
},
},
})Widths that divide evenly do not use calc; existing at-rules are extended:
const style: CNode = {
'@media(min-width:400px)': {
'.r2': { margin: 0 },
},
}
rwd(style, '.r2', 100, 40, [4, 2], [2, 2, 3])
expect(style).toEqual({
'@media(min-width:400px)': {
'.r2': {
margin: 0,
width: '50%',
height: '40px',
},
},
'.r2': {
'box-sizing': 'border-box',
display: 'block',
float: 'left',
width: '100%',
height: '40px',
},
'@media(min-width:200px)': {
'.r2': {
width: '100%',
height: '120px',
},
},
})Invalid arguments throw a RangeError:
expect(() => rwd({}, '.x', 0, 50)).toThrow(RangeError)
expect(() => rwd({}, '.x', 100, Infinity)).toThrow(RangeError)
expect(() => rwd({}, '.x', 100, 50, [0])).toThrow(RangeError)
expect(() => rwd({}, '.x', 100, 50, [2, 3])).toThrow(RangeError)
expect(() => rwd({}, '.x', 100, 50, [2, 1, 1.5])).toThrow(RangeError)s
const s: {
<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
<N extends Node>(node: N, ...args: HArgs1[]): N
(tag: string, ...args: HArgs1[]): SVGElement
(node: Node, ...args: HArgs1[]): Node
}A lightweight HyperScript-style helper for creating and modifying SVGElements (see also h).
tagOrNode
If a string, it is treated as the tag name for a new element.
If a Node, that node is modified in place.
args
Additional arguments processed as follows:
Object: maps attributes/properties.- Keys starting with
$set element properties (without the$prefix); if the value is a plain object (created by{…}orObject.create(null)), it is shallow-merged into the existing property instead of replacing it, other values (including arrays and class instances) replace the property. - Other keys set attributes: a key prefixed with
xlink:,xml:orxmlns:sets the attribute viasetAttributeNSin the corresponding namespace (e.g.xml:lang); any other key sets it viasetAttribute. A value oftruesets an empty attribute, a value offalseremoves the attribute, a value ofnull/undefinedleaves the attribute unchanged.
- Keys starting with
null/undefined: ignored.Node: appended as a child.string/number: converted to aTextnode and appended.HArgsarray: processed recursively (creates or modifies a child node and appends it).
Returns
The created SVGElement or the modified node.
Usage Examples
Elements (including nested ones) are created in the SVG namespace:
const svg = s('svg', ['g', ['circle', { r: 1 }]])
expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg')
expect(svg.querySelector('circle')?.namespaceURI).toBe('http://www.w3.org/2000/svg')
expect(svg.outerHTML).toBe('<svg><g><circle r="1"></circle></g></svg>')Namespaced xlink: attribute set to true gets an empty value:
const svg = s('svg', { 'xlink:href': true })
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('')Namespaced xlink: attribute set to false is removed:
const svg = s('svg', { 'xlink:href': '#id' })
s(svg, { 'xlink:href': false })
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBeNull()Namespaced xlink: attribute with a string value:
const svg = s('svg', { 'xlink:href': 'http://example.com' })
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('http://example.com')Namespaced xlink: attribute with a number value:
const svg = s('svg', { 'xlink:title': 42 })
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'title')).toBe('42')Namespaced xml: and xmlns: attributes:
const svg = s('svg', { 'xml:lang': 'pl', 'xmlns:xlink': 'http://www.w3.org/1999/xlink' })
expect(svg.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
expect(svg.getAttributeNS('http://www.w3.org/2000/xmlns/', 'xlink')).toBe('http://www.w3.org/1999/xlink')Return types:
expectTypeOf(s('circle')).toEqualTypeOf<SVGCircleElement>()
expectTypeOf(s('my-element')).toEqualTypeOf<SVGElement>()svgUse
const svgUse: (id: string, ...args: HArgs1[]) => SVGSVGElementShorthand for creating an <svg> element with a <use> child referencing a symbol (e.g. an icon) by ID.
Equivalent to:
s('svg', ['use', { href: '#' + id }], ...args)id
The ID of the symbol to reference (without the # prefix).
args
Additional arguments passed to the outer <svg> element.
Returns
An SVGSVGElement containing a <use> element.
Usage Examples
SVG use element referencing a symbol:
const svg = svgUse('icon-home')
expect(svg.outerHTML).toBe('<svg><use href="#icon-home"></use></svg>')Additional attributes are set on the outer svg element:
const svg = svgUse('icon-settings', { width: 24, height: 24, class: 'icon' })
expect(svg.outerHTML).toBe('<svg width="24" height="24" class="icon"><use href="#icon-settings"></use></svg>')Additional children are appended to the outer svg element:
const svg = svgUse('icon-star', ['title', 'Star'])
expect(svg.outerHTML).toBe('<svg><use href="#icon-star"></use><title>Star</title></svg>')License
The MIT License (MIT)
Copyright (c) 2016+ Jackens
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
