diff --git a/examples/ruleset.ts b/examples/ruleset.ts index 58b8162..68d7e25 100644 --- a/examples/ruleset.ts +++ b/examples/ruleset.ts @@ -17,7 +17,8 @@ import {Ruleset} from '../src/ruleset'; value.must.havePropertyWithType('name', 'string'), value.is.iterable(), value.must.be.an.ipv4addr(), - value.is.type('array') + value.is.type('array'), + value.is.an.html5Tag() ); console.debug(`Rules: ${ruleset.rules.length}`); diff --git a/src/block/an.ts b/src/block/an.ts index 5576beb..acd08f0 100644 --- a/src/block/an.ts +++ b/src/block/an.ts @@ -26,6 +26,7 @@ import {Block} from '../block'; import {type MatcherFactory} from '../matcher/factory'; import {matcherMkArray} from '../matcher/mk/array'; +import {matcherMkHtml5Tag} from '../matcher/mk/html5/tag'; import {matcherMkInt} from '../matcher/mk/int'; import {matcherMkIpv4Addr} from '../matcher/mk/ipv4addr'; import {matcherMkIpv6Addr} from '../matcher/mk/ipv6addr'; @@ -41,6 +42,7 @@ export class BlockAn extends Block> { public readonly int: MatcherFactory>; public readonly ipv4addr: MatcherFactory>; public readonly ipv6addr: MatcherFactory>; + public readonly html5Tag: MatcherFactory>; constructor(init: BlockInit) { super( @@ -56,5 +58,6 @@ export class BlockAn extends Block> { this.int = matcherMkInt(init); this.ipv4addr = matcherMkIpv4Addr(init); this.ipv6addr = matcherMkIpv6Addr(init); + this.html5Tag = matcherMkHtml5Tag(init); } } diff --git a/src/block/is.ts b/src/block/is.ts index 3d62edb..3e05c59 100644 --- a/src/block/is.ts +++ b/src/block/is.ts @@ -38,6 +38,8 @@ import {matcherMkBetween} from '../matcher/mk/between'; import {type Primitive} from '@toreda/types'; import {matcherMkIterable} from '../matcher/mk/iterable'; import {matcherMkTruthy} from '../matcher/mk/truthy'; +import {BlockA} from './a'; +import {BlockAn} from './an'; /** * Matchers following 'is' in rule statements. @@ -70,6 +72,9 @@ export class BlockIs extends Block> { public readonly empty: MatcherFactory>; public readonly divisibleBy: MatcherFactory>; public readonly type: MatcherFactory>; + public readonly an: BlockAn; + public readonly a: BlockA; + /** * Determine if `value` supports iteration, but does check for a specific iterable type. */ @@ -92,6 +97,17 @@ export class BlockIs extends Block> { init.stmt ); + this.an = new BlockAn({ + ...init, + name: 'an', + tracer: this.tracer + }); + this.a = new BlockA({ + ...init, + name: 'a', + tracer: this.tracer + }); + this.between = matcherMkBetween({ ...init, tracer: this.tracer, diff --git a/src/html5/tag.ts b/src/html5/tag.ts new file mode 100644 index 0000000..b35f7d0 --- /dev/null +++ b/src/html5/tag.ts @@ -0,0 +1,147 @@ +/** + * MIT License + * + * Copyright (c) 2019 - 2024 Toreda, Inc. + * + * 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 A 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. + * + */ + +/** + * @category Validators – Html5 + */ +export type Html5Tag = + | 'area' + | 'article' + | 'aside' + | 'audio' + | 'b' + | 'base' + | 'basefront' + | 'bdi' + | 'bdo' + | 'blockquote' + | 'body' + | 'br' + | 'button' + | 'canvas' + | 'caption' + | 'cite' + | 'code' + | 'col' + | 'colgroup' + | 'data' + | 'del' + | 'details' + | 'dfn' + | 'dialog' + | 'dir' + | 'div' + | 'dl' + | 'dt' + | 'em' + | 'embed' + | 'fieldset' + | 'figcaption' + | 'figure' + | 'font' + | 'footer' + | 'form' + | 'frame' + | 'frameset' + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'h5' + | 'h6' + | 'head' + | 'header' + | 'hgroup' + | 'hr' + | 'html' + | 'i' + | 'iframe' + | 'img' + | 'input' + | 'ins' + | 'kbd' + | 'keygen' + | 'label' + | 'legend' + | 'li' + | 'link' + | 'main' + | 'map' + | 'mark' + | 'menu' + | 'menuitem' + | 'meta' + | 'meter' + | 'nav' + | 'noframes' + | 'noscript' + | 'object' + | 'ol' + | 'optgroup' + | 'option' + | 'output' + | 'p' + | 'param' + | 'picture' + | 'pre' + | 'progress' + | 'q' + | 'rp' + | 'rt' + | 'ruby' + | 's' + | 'samp' + | 'script' + | 'section' + | 'select' + | 'small' + | 'source' + | 'span' + | 'strike' + | 'strong' + | 'style' + | 'sub' + | 'summary' + | 'sup' + | 'svg' + | 'table' + | 'tbody' + | 'td' + | 'template' + | 'textarea' + | 'tfoot' + | 'th' + | 'thead' + | 'time' + | 'title' + | 'tr' + | 'track' + | 'tt' + | 'u' + | 'ul' + | 'var' + | 'video' + | 'wbr' + | string; diff --git a/src/html5/tags.ts b/src/html5/tags.ts new file mode 100644 index 0000000..77191eb --- /dev/null +++ b/src/html5/tags.ts @@ -0,0 +1,121 @@ +import {type Html5Tag} from './tag'; + +export const html5Tags: Set = new Set([ + 'area', + 'article', + 'aside', + 'audio', + 'b', + 'base', + 'basefront', + 'bdi', + 'bdo', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'data', + 'del', + 'details', + 'dfn', + 'dialog', + 'dir', + 'div', + 'dl', + 'dt', + 'em', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'keygen', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', + 'mark', + 'menu', + 'menuitem', + 'meta', + 'meter', + 'nav', + 'noframes', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'param', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'script', + 'section', + 'select', + 'small', + 'source', + 'span', + 'strike', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'svg', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'title', + 'tr', + 'track', + 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr' +]); diff --git a/src/index.ts b/src/index.ts index bfbf9d9..5dc6898 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,8 @@ export {ErrorContext} from './error/context'; export {ErrorContextData} from './error/context/data'; export {errorMkCode} from './error/mk/code'; export {greaterThan} from './greater/than'; +export {Html5Tag} from './html5/tag'; +export {html5Tags} from './html5/tags'; export {isArray} from './is/array'; export {isArrayEmpty} from './is/array/empty'; export {isArrayNotEmpty} from './is/array/not/empty'; diff --git a/src/is/html5/tag.ts b/src/is/html5/tag.ts new file mode 100644 index 0000000..3d9650a --- /dev/null +++ b/src/is/html5/tag.ts @@ -0,0 +1,44 @@ +/** + * MIT License + * + * Copyright (c) 2019 - 2024 Toreda, Inc. + * + * 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 A 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. + * + */ + +import {type Html5Tag} from '../../html5/tag'; +import {html5Tags} from '../../html5/tags'; + +/** + * Determine if value is a strict boolean type. Does not return true for + * otherwise truthy non-boolean values. + * @param value + * @returns + * + * @category Validators – Html5 + */ +export function isHtml5Tag(value: unknown): value is Html5Tag { + if (typeof value !== 'string') { + return false; + } + + const lower = value.toLocaleLowerCase().trim(); + return html5Tags.has(lower); +} diff --git a/src/matcher/mk/html5/tag.ts b/src/matcher/mk/html5/tag.ts new file mode 100644 index 0000000..2828763 --- /dev/null +++ b/src/matcher/mk/html5/tag.ts @@ -0,0 +1,59 @@ +/** + * MIT License + * + * Copyright (c) 2019 - 2024 Toreda, Inc. + * + * 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 A 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. + * + */ + +import {isIpv6Addr} from '@toreda/strong-types'; +import {type BlockInit} from '../../../block/init'; +import {BlockLink} from '../../../block/link'; + +import {type Predicate} from '../../../predicate'; +import {type MatcherFactory} from '../../factory'; +import {isHtml5Tag} from '../../../is/html5/tag'; + +/** + * + * @category Matcher Factories + */ +export function matcherMkHtml5Tag( + init: BlockInit +): MatcherFactory> { + return () => { + // Link object MUST BE created during matcher func invocation. Moving it out into the surrounding closure + // will cause infinite recursion & stack overflow. + const link = new BlockLink(init); + + const func: Predicate = async (value?: InputT | null): Promise => { + return isHtml5Tag(value); + }; + + init.stmt.addMatcher({ + fn: func, + name: 'html5Tag', + flags: init.flags, + tracer: init.tracer + }); + + return link; + }; +} diff --git a/src/matcher/mk/less/or/equal/to.ts b/src/matcher/mk/less/or/equal/to.ts new file mode 100644 index 0000000..478f963 --- /dev/null +++ b/src/matcher/mk/less/or/equal/to.ts @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (c) 2019 - 2024 Toreda, Inc. + * + * 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 A 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. + * + */ + +import {type BlockInit} from '../../../../../block/init'; +import {BlockLink} from '../../../../../block/link'; +import {equalTo} from '../../../../../equal/to'; +import {lessThan} from '../../../../../less/than'; +import {type Predicate} from '../../../../../predicate'; +import {type MatcherFactory} from '../../../../factory'; +/** + * Create matcher for validation chain which determines if chain value is less + * than or equal to target. + * @param init + * @returns + * + * @category Matcher Predicate Factories + */ +export function matcherMkLessThanOrEqualTo( + init: BlockInit +): MatcherFactory> { + return (right: number): BlockLink => { + // Link object MUST BE created during matcher func invocation. Moving it out into the surrounding closure + // will cause infinite recursion & stack overflow. + const link = new BlockLink(init); + init.tracer.addParam(right); + + const func: Predicate = async (value?: InputT | null): Promise => { + if (equalTo(value, right)) { + return true; + } + + return lessThan(value, right); + }; + + init.stmt.addMatcher({ + fn: func, + name: '<=', + flags: init.flags, + tracer: init.tracer + }); + + return link; + }; +} diff --git a/tests/html5/tag.spec.ts b/tests/html5/tag.spec.ts new file mode 100644 index 0000000..7e3fbb6 --- /dev/null +++ b/tests/html5/tag.spec.ts @@ -0,0 +1,532 @@ +import {isHtml5Tag} from '../../src/is/html5/tag'; +import {type TestCase} from '../_lib/test/case'; +const EMPTY_ARRAY: unknown[] = []; +const EMPTY_OBJECT: unknown[] = []; + +const TEST_CASES: TestCase[] = [ + { + value: undefined, + label: 'undefined', + result: false + }, + { + value: null, + label: 'null', + result: false + }, + { + value: '', + label: 'empty string', + result: false + }, + { + value: 1, + result: false + }, + { + value: 0, + result: false + }, + { + value: 'area', + result: true + }, + { + value: 'aside', + result: true + }, + { + value: 'article', + result: true + }, + { + value: 'audio', + result: true + }, + { + value: 'b', + result: true + }, + { + value: 'base', + result: true + }, + { + value: 'basefront', + result: true + }, + { + value: 'bdi', + result: true + }, + { + value: 'bdo', + result: true + }, + { + value: 'blockquote', + result: true + }, + { + value: 'body', + result: true + }, + { + value: 'br', + result: true + }, + { + value: 'button', + result: true + }, + { + value: 'body', + result: true + }, + { + value: 'br', + result: true + }, + { + value: 'button', + result: true + }, + { + value: 'canvas', + result: true + }, + { + value: 'caption', + result: true + }, + { + value: 'cite', + result: true + }, + { + value: 'code', + result: true + }, + { + value: 'col', + result: true + }, + { + value: 'colgroup', + result: true + }, + { + value: 'data', + result: true + }, + { + value: 'del', + result: true + }, + { + value: 'details', + result: true + }, + { + value: 'dfn', + result: true + }, + { + value: 'dialog', + result: true + }, + { + value: 'dir', + result: true + }, + { + value: 'div', + result: true + }, + { + value: 'dl', + result: true + }, + { + value: 'dt', + result: true + }, + { + value: 'em', + result: true + }, + { + value: 'embed', + result: true + }, + { + value: 'fieldset', + result: true + }, + { + value: 'figcaption', + result: true + }, + { + value: 'figure', + result: true + }, + { + value: 'font', + result: true + }, + { + value: 'footer', + result: true + }, + { + value: 'form', + result: true + }, + { + value: 'frame', + result: true + }, + { + value: 'frameset', + result: true + }, + { + value: 'h1', + result: true + }, + { + value: 'h2', + result: true + }, + { + value: 'h3', + result: true + }, + { + value: 'h4', + result: true + }, + { + value: 'h5', + result: true + }, + { + value: 'h6', + result: true + }, + { + value: 'head', + result: true + }, + { + value: 'header', + result: true + }, + { + value: 'hgroup', + result: true + }, + { + value: 'hr', + result: true + }, + { + value: 'html', + result: true + }, + { + value: 'i', + result: true + }, + { + value: 'iframe', + result: true + }, + { + value: 'img', + result: true + }, + { + value: 'input', + result: true + }, + { + value: 'ins', + result: true + }, + { + value: 'kbd', + result: true + }, + { + value: 'keygen', + result: true + }, + { + value: 'label', + result: true + }, + { + value: 'legend', + result: true + }, + { + value: 'li', + result: true + }, + { + value: 'link', + result: true + }, + { + value: 'main', + result: true + }, + { + value: 'map', + result: true + }, + { + value: 'mark', + result: true + }, + { + value: 'menu', + result: true + }, + { + value: 'menuitem', + result: true + }, + { + value: 'meta', + result: true + }, + { + value: 'meter', + result: true + }, + { + value: 'nav', + result: true + }, + { + value: 'noframes', + result: true + }, + { + value: 'noscript', + result: true + }, + { + value: 'object', + result: true + }, + { + value: 'ol', + result: true + }, + { + value: 'optgroup', + result: true + }, + { + value: 'option', + result: true + }, + { + value: 'output', + result: true + }, + { + value: 'p', + result: true + }, + { + value: 'param', + result: true + }, + { + value: 'picture', + result: true + }, + { + value: 'pre', + result: true + }, + { + value: 'progress', + result: true + }, + { + value: 'q', + result: true + }, + { + value: 'rp', + result: true + }, + { + value: 'rt', + result: true + }, + { + value: 'ruby', + result: true + }, + { + value: 's', + result: true + }, + { + value: 'samp', + result: true + }, + { + value: 'script', + result: true + }, + { + value: 'section', + result: true + }, + { + value: 'select', + result: true + }, + { + value: 'small', + result: true + }, + { + value: 'source', + result: true + }, + { + value: 'span', + result: true + }, + { + value: 'strike', + result: true + }, + { + value: 'strong', + result: true + }, + { + value: 'style', + result: true + }, + { + value: 'sub', + result: true + }, + { + value: 'summary', + result: true + }, + { + value: 'sup', + result: true + }, + { + value: 'svg', + result: true + }, + { + value: 'table', + result: true + }, + { + value: 'tbody', + result: true + }, + { + value: 'td', + result: true + }, + { + value: 'template', + result: true + }, + { + value: 'textarea', + result: true + }, + { + value: 'tfoot', + result: true + }, + { + value: 'th', + result: true + }, + { + value: 'thead', + result: true + }, + { + value: 'time', + result: true + }, + { + value: 'title', + result: true + }, + { + value: 'tr', + result: true + }, + { + value: 'track', + result: true + }, + { + value: 'tt', + result: true + }, + { + value: 'u', + result: true + }, + { + value: 'ul', + result: true + }, + { + value: 'var', + result: true + }, + { + value: 'video', + result: true + }, + { + value: 'wbr', + result: true + }, + { + label: 'empty object', + value: EMPTY_OBJECT, + result: false + }, + { + label: 'empty array', + value: EMPTY_ARRAY, + result: false + } +]; + +describe('isHtml5Tag', () => { + for (const testCase of TEST_CASES) { + const label = testCase.label ?? testCase.value; + + it(`should return ${testCase.result} when input is ${label}`, () => { + const result = isHtml5Tag(testCase.value); + + expect(result).toBe(testCase.result); + }); + } +});