yummies 7.12.0 → 7.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -87
- package/async.cjs +17 -0
- package/async.cjs.map +1 -1
- package/async.d.ts +17 -0
- package/async.js +17 -0
- package/async.js.map +1 -1
- package/common.cjs.map +1 -1
- package/common.d.ts +18 -0
- package/common.js.map +1 -1
- package/complex.cjs.map +1 -1
- package/complex.d.ts +66 -0
- package/complex.js.map +1 -1
- package/cookie.cjs.map +1 -1
- package/cookie.d.ts +18 -0
- package/cookie.js.map +1 -1
- package/css.cjs +16 -0
- package/css.cjs.map +1 -1
- package/css.d.ts +17 -0
- package/css.js +16 -0
- package/css.js.map +1 -1
- package/data.cjs.map +1 -1
- package/data.d.ts +18 -0
- package/data.js.map +1 -1
- package/date-time.cjs +16 -0
- package/date-time.cjs.map +1 -1
- package/date-time.d.ts +17 -0
- package/date-time.js +16 -0
- package/date-time.js.map +1 -1
- package/device.cjs +17 -0
- package/device.cjs.map +1 -1
- package/device.d.ts +17 -0
- package/device.js +17 -0
- package/device.js.map +1 -1
- package/encodings.cjs.map +1 -1
- package/encodings.d.ts +17 -0
- package/encodings.js.map +1 -1
- package/errors.cjs +16 -0
- package/errors.cjs.map +1 -1
- package/errors.d.ts +17 -0
- package/errors.js +16 -0
- package/errors.js.map +1 -1
- package/file.cjs +16 -0
- package/file.cjs.map +1 -1
- package/file.d.ts +16 -0
- package/file.js +16 -0
- package/file.js.map +1 -1
- package/format.cjs.map +1 -1
- package/format.d.ts +18 -0
- package/format.js.map +1 -1
- package/html.cjs +16 -0
- package/html.cjs.map +1 -1
- package/html.d.ts +17 -0
- package/html.js +16 -0
- package/html.js.map +1 -1
- package/id.cjs +16 -0
- package/id.cjs.map +1 -1
- package/id.d.ts +16 -0
- package/id.js +16 -0
- package/id.js.map +1 -1
- package/imports.cjs +16 -0
- package/imports.cjs.map +1 -1
- package/imports.d.ts +16 -0
- package/imports.js +16 -0
- package/imports.js.map +1 -1
- package/math.cjs.map +1 -1
- package/math.d.ts +17 -0
- package/math.js.map +1 -1
- package/media.cjs +16 -0
- package/media.cjs.map +1 -1
- package/media.d.ts +16 -0
- package/media.js +16 -0
- package/media.js.map +1 -1
- package/mobx.cjs +96 -0
- package/mobx.cjs.map +1 -1
- package/mobx.d.ts +101 -0
- package/mobx.js +96 -0
- package/mobx.js.map +1 -1
- package/ms.cjs +16 -0
- package/ms.cjs.map +1 -1
- package/ms.d.ts +16 -0
- package/ms.js +16 -0
- package/ms.js.map +1 -1
- package/number.cjs +16 -0
- package/number.cjs.map +1 -1
- package/number.d.ts +16 -0
- package/number.js +16 -0
- package/number.js.map +1 -1
- package/package.json +2 -2
- package/parser.cjs.map +1 -1
- package/parser.d.ts +17 -0
- package/parser.js.map +1 -1
- package/price.cjs.map +1 -1
- package/price.d.ts +16 -0
- package/price.js.map +1 -1
- package/random.cjs +16 -0
- package/random.cjs.map +1 -1
- package/random.d.ts +16 -0
- package/random.js +16 -0
- package/random.js.map +1 -1
- package/sound.cjs +16 -0
- package/sound.cjs.map +1 -1
- package/sound.d.ts +16 -0
- package/sound.js +16 -0
- package/sound.js.map +1 -1
- package/storage.cjs.map +1 -1
- package/storage.d.ts +16 -0
- package/storage.js.map +1 -1
- package/text.cjs +16 -0
- package/text.cjs.map +1 -1
- package/text.d.ts +16 -0
- package/text.js +16 -0
- package/text.js.map +1 -1
- package/type-guard.cjs.map +1 -1
- package/type-guard.d.ts +18 -0
- package/type-guard.js.map +1 -1
- package/types.d.ts +41 -0
- package/types.global.d.ts +41 -0
- package/vibrate.cjs +16 -0
- package/vibrate.cjs.map +1 -1
- package/vibrate.d.ts +16 -0
- package/vibrate.js +16 -0
- package/vibrate.js.map +1 -1
package/format.d.ts
CHANGED
|
@@ -144,6 +144,24 @@ declare const percent: (value: Maybe<number | string>, settings?: PercentFormatS
|
|
|
144
144
|
*/
|
|
145
145
|
declare const skipSpaces: (value: string) => string;
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* ---header-docs-section---
|
|
149
|
+
* # yummies/format
|
|
150
|
+
*
|
|
151
|
+
* ## Description
|
|
152
|
+
*
|
|
153
|
+
* Namespace-style **number, percent, and string** formatting for UI tables, inputs, and charts.
|
|
154
|
+
* Submodules cover locale-aware numbers, percent parsing/formatting, trimming helpers, and shared
|
|
155
|
+
* constants so presentation rules stay centralized and tree-shakable behind the package
|
|
156
|
+
* aggregated **format** export from this entry point.
|
|
157
|
+
*
|
|
158
|
+
* ## Usage
|
|
159
|
+
*
|
|
160
|
+
* ```ts
|
|
161
|
+
* import { format } from "yummies/format";
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
|
|
147
165
|
declare const _exports_HYPHEN: typeof HYPHEN;
|
|
148
166
|
declare const _exports_INFINITY: typeof INFINITY;
|
|
149
167
|
declare const _exports_NO_VALUE: typeof NO_VALUE;
|
package/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","names":[],"sources":["../src/format/number.ts","../src/format/percent.ts","../src/format/skip-spaces.ts","../src/format/_exports.ts"],"sourcesContent":["import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\nimport { NO_VALUE } from './constants.js';\n\nexport interface NumberFormatSettings {\n delimiter?: string;\n /**\n * digitsOnlyForFloat - Show digits after decimal point only if they are not zeros after converting to number.\n * Example: \"0.00\" -> \"0\", \"0.10\" -> \"0.1\", but \"0.003\" -> \"0.003\"\n *\n * @default true\n */\n digitsOnlyForFloat?: boolean;\n /**\n * Text which will be returned if the value is undefined, null, NaN, Infinity or empty string.\n * Example: \"–\" will be returned if the value is undefined and emptyText is \"–\".\n */\n emptyText?: string;\n /**\n * Text to append to the end of the formatted number.\n * Example: if value is 1000 and postfix is \"₽\", result will be \"1 000₽\".\n */\n postfix?: string;\n /**\n * Fixed number of digits after the decimal point (number.toFixed() method)\n * If set to false, the truncation is ignored!\n */\n digits?: number | false;\n /**\n * Remove trailing zeros from the end of the number\n * Example: 0.010000000000000000000000000000000000000000000 -> 0.01\n */\n cutZeros?: boolean;\n cropDigitsOnly?: boolean;\n}\n\n/**\n * Formats a numeric value with thousands separators, fractional digit control\n * and optional postfix text.\n *\n * Invalid, empty or unsupported values fall back to `emptyText`.\n *\n * @param rawValue Number or numeric string to format.\n * @param userSettings Formatting overrides merged with `number.defaultSettings`.\n * @returns Formatted number string or fallback text.\n *\n * @example\n * ```ts\n * number(12000); // '12 000'\n * ```\n *\n * @example\n * ```ts\n * number(12.5, { digits: 1, postfix: '%' }); // '12.5%'\n * ```\n */\nexport const number = (\n rawValue: Maybe<string | number>,\n userSettings?: Maybe<NumberFormatSettings>,\n): string => {\n const settings = {\n ...number.defaultSettings,\n ...userSettings,\n };\n\n const digits = settings.digits ?? 0;\n const cutZeros = settings?.cutZeros ?? false;\n const delimiter = settings.delimiter ?? ' ';\n const postfix = settings.postfix ?? '';\n const emptyText = settings.emptyText ?? NO_VALUE;\n const digitsOnlyForFloat = settings.digitsOnlyForFloat ?? true;\n\n let value: Maybe<number>;\n\n if (typeGuard.isString(rawValue)) {\n value = parser.number(value, { fallback: undefined });\n } else {\n value = rawValue;\n }\n\n if (typeGuard.isNumber(value)) {\n let raw: string = `${value}`;\n\n if (digits !== false) {\n if (settings.cropDigitsOnly) {\n const [integerPart, decimalPart] = `${raw}`.split('.');\n const leftPart = integerPart;\n const rightPart = (decimalPart || '')\n .slice(0, digits)\n .padEnd(digits, '0');\n\n if (rightPart) {\n raw = `${leftPart}.${rightPart}`;\n } else {\n raw = leftPart;\n }\n } else {\n raw = value.toFixed(digits);\n }\n }\n\n if (cutZeros) {\n raw = `${+raw}`;\n }\n\n const [integerPart, decimalPart] = raw.split('.', 2);\n\n let formattedIntegerPart = integerPart;\n let formattedDecimalPart = '';\n\n if (decimalPart && (!digitsOnlyForFloat || !/^0+$/.test(decimalPart))) {\n formattedDecimalPart = `.${decimalPart}`;\n }\n\n const rgx = /(\\d+)(\\d{3})/;\n\n while (rgx.test(formattedIntegerPart) && delimiter) {\n formattedIntegerPart = formattedIntegerPart.replace(\n rgx,\n `$1${delimiter}$2`,\n );\n }\n\n return formattedIntegerPart + formattedDecimalPart + postfix;\n }\n\n return emptyText;\n};\n\nnumber.defaultSettings = {} as NumberFormatSettings;\n","import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\n\nimport { NO_VALUE } from './constants.js';\n\nexport interface PercentFormatSettings\n extends Omit<parser.NumberParserSettings, 'fallback'> {\n divider?: string;\n delimiter?: string;\n symbol?: string;\n emptyText?: string;\n}\n\n/**\n * Formats a value as a percent string with configurable decimal precision,\n * decimal divider and suffix symbol.\n *\n * @param value Number or numeric string to format.\n * @param settings Parser and formatting options.\n * @returns Formatted percent string or fallback text for invalid values.\n *\n * @example\n * ```ts\n * percent(12.345); // '12.35%'\n * ```\n *\n * @example\n * ```ts\n * percent(12.345, { divider: ',', symbol: ' pct' }); // '12,35 pct'\n * ```\n */\nexport const percent = (\n value: Maybe<number | string>,\n settings?: PercentFormatSettings,\n) => {\n const numericValue = parser.number(value, {\n ...settings,\n digits: settings?.digits ?? 2,\n fallback: Number.NaN,\n });\n\n if (typeGuard.isNumber(numericValue)) {\n const divider = settings?.divider ?? '.';\n\n const formattedPercent =\n divider === '.' ? numericValue : `${numericValue}`.replace('.', divider);\n\n return `${formattedPercent}${settings?.delimiter ?? ''}${settings?.symbol ?? '%'}`;\n } else {\n return settings?.emptyText ?? NO_VALUE;\n }\n};\n","/**\n * Removes all whitespace characters from a string.\n *\n * @param value Source string.\n * @returns String without spaces, tabs and line breaks.\n *\n * @example\n * ```ts\n * skipSpaces('1 000 000'); // '1000000'\n * ```\n *\n * @example\n * ```ts\n * skipSpaces('a\\tb\\nc'); // 'abc'\n * ```\n */\nexport const skipSpaces = (value: string) => value.replaceAll(/\\s/g, '');\n","export * from './constants.js';\nexport * from './number.js';\nexport * from './percent.js';\nexport * from './skip-spaces.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyDA,IAAa,UACX,UACA,iBACW;CACX,MAAM,WAAW;EACf,GAAG,OAAO;EACV,GAAG;EACJ;CAED,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,UAAU,YAAY;CACvC,MAAM,YAAY,SAAS,aAAa;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,aAAA;CAC3B,MAAM,qBAAqB,SAAS,sBAAsB;CAE1D,IAAI;AAEJ,KAAI,UAAU,SAAS,SAAS,CAC9B,SAAQ,OAAO,OAAO,OAAO,EAAE,UAAU,KAAA,GAAW,CAAC;KAErD,SAAQ;AAGV,KAAI,UAAU,SAAS,MAAM,EAAE;EAC7B,IAAI,MAAc,GAAG;AAErB,MAAI,WAAW,MACb,KAAI,SAAS,gBAAgB;GAC3B,MAAM,CAAC,aAAa,eAAe,GAAG,MAAM,MAAM,IAAI;GACtD,MAAM,WAAW;GACjB,MAAM,aAAa,eAAe,IAC/B,MAAM,GAAG,OAAO,CAChB,OAAO,QAAQ,IAAI;AAEtB,OAAI,UACF,OAAM,GAAG,SAAS,GAAG;OAErB,OAAM;QAGR,OAAM,MAAM,QAAQ,OAAO;AAI/B,MAAI,SACF,OAAM,GAAG,CAAC;EAGZ,MAAM,CAAC,aAAa,eAAe,IAAI,MAAM,KAAK,EAAE;EAEpD,IAAI,uBAAuB;EAC3B,IAAI,uBAAuB;AAE3B,MAAI,gBAAgB,CAAC,sBAAsB,CAAC,OAAO,KAAK,YAAY,EAClE,wBAAuB,IAAI;EAG7B,MAAM,MAAM;AAEZ,SAAO,IAAI,KAAK,qBAAqB,IAAI,UACvC,wBAAuB,qBAAqB,QAC1C,KACA,KAAK,UAAU,IAChB;AAGH,SAAO,uBAAuB,uBAAuB;;AAGvD,QAAO;;AAGT,OAAO,kBAAkB,EAAE;;;;;;;;;;;;;;;;;;;;;AClG3B,IAAa,WACX,OACA,aACG;CACH,MAAM,eAAe,OAAO,OAAO,OAAO;EACxC,GAAG;EACH,QAAQ,UAAU,UAAU;EAC5B,UAAU;EACX,CAAC;AAEF,KAAI,UAAU,SAAS,aAAa,EAAE;EACpC,MAAM,UAAU,UAAU,WAAW;AAKrC,SAAO,GAFL,YAAY,MAAM,eAAe,GAAG,eAAe,QAAQ,KAAK,QAAQ,GAE7C,UAAU,aAAa,KAAK,UAAU,UAAU;OAE7E,QAAO,UAAU,aAAA;;;;;;;;;;;;;;;;;;;;AClCrB,IAAa,cAAc,UAAkB,MAAM,WAAW,OAAO,GAAG"}
|
|
1
|
+
{"version":3,"file":"format.js","names":[],"sources":["../src/format/number.ts","../src/format/percent.ts","../src/format/skip-spaces.ts","../src/format/_exports.ts"],"sourcesContent":["import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\nimport { NO_VALUE } from './constants.js';\n\nexport interface NumberFormatSettings {\n delimiter?: string;\n /**\n * digitsOnlyForFloat - Show digits after decimal point only if they are not zeros after converting to number.\n * Example: \"0.00\" -> \"0\", \"0.10\" -> \"0.1\", but \"0.003\" -> \"0.003\"\n *\n * @default true\n */\n digitsOnlyForFloat?: boolean;\n /**\n * Text which will be returned if the value is undefined, null, NaN, Infinity or empty string.\n * Example: \"–\" will be returned if the value is undefined and emptyText is \"–\".\n */\n emptyText?: string;\n /**\n * Text to append to the end of the formatted number.\n * Example: if value is 1000 and postfix is \"₽\", result will be \"1 000₽\".\n */\n postfix?: string;\n /**\n * Fixed number of digits after the decimal point (number.toFixed() method)\n * If set to false, the truncation is ignored!\n */\n digits?: number | false;\n /**\n * Remove trailing zeros from the end of the number\n * Example: 0.010000000000000000000000000000000000000000000 -> 0.01\n */\n cutZeros?: boolean;\n cropDigitsOnly?: boolean;\n}\n\n/**\n * Formats a numeric value with thousands separators, fractional digit control\n * and optional postfix text.\n *\n * Invalid, empty or unsupported values fall back to `emptyText`.\n *\n * @param rawValue Number or numeric string to format.\n * @param userSettings Formatting overrides merged with `number.defaultSettings`.\n * @returns Formatted number string or fallback text.\n *\n * @example\n * ```ts\n * number(12000); // '12 000'\n * ```\n *\n * @example\n * ```ts\n * number(12.5, { digits: 1, postfix: '%' }); // '12.5%'\n * ```\n */\nexport const number = (\n rawValue: Maybe<string | number>,\n userSettings?: Maybe<NumberFormatSettings>,\n): string => {\n const settings = {\n ...number.defaultSettings,\n ...userSettings,\n };\n\n const digits = settings.digits ?? 0;\n const cutZeros = settings?.cutZeros ?? false;\n const delimiter = settings.delimiter ?? ' ';\n const postfix = settings.postfix ?? '';\n const emptyText = settings.emptyText ?? NO_VALUE;\n const digitsOnlyForFloat = settings.digitsOnlyForFloat ?? true;\n\n let value: Maybe<number>;\n\n if (typeGuard.isString(rawValue)) {\n value = parser.number(value, { fallback: undefined });\n } else {\n value = rawValue;\n }\n\n if (typeGuard.isNumber(value)) {\n let raw: string = `${value}`;\n\n if (digits !== false) {\n if (settings.cropDigitsOnly) {\n const [integerPart, decimalPart] = `${raw}`.split('.');\n const leftPart = integerPart;\n const rightPart = (decimalPart || '')\n .slice(0, digits)\n .padEnd(digits, '0');\n\n if (rightPart) {\n raw = `${leftPart}.${rightPart}`;\n } else {\n raw = leftPart;\n }\n } else {\n raw = value.toFixed(digits);\n }\n }\n\n if (cutZeros) {\n raw = `${+raw}`;\n }\n\n const [integerPart, decimalPart] = raw.split('.', 2);\n\n let formattedIntegerPart = integerPart;\n let formattedDecimalPart = '';\n\n if (decimalPart && (!digitsOnlyForFloat || !/^0+$/.test(decimalPart))) {\n formattedDecimalPart = `.${decimalPart}`;\n }\n\n const rgx = /(\\d+)(\\d{3})/;\n\n while (rgx.test(formattedIntegerPart) && delimiter) {\n formattedIntegerPart = formattedIntegerPart.replace(\n rgx,\n `$1${delimiter}$2`,\n );\n }\n\n return formattedIntegerPart + formattedDecimalPart + postfix;\n }\n\n return emptyText;\n};\n\nnumber.defaultSettings = {} as NumberFormatSettings;\n","import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\n\nimport { NO_VALUE } from './constants.js';\n\nexport interface PercentFormatSettings\n extends Omit<parser.NumberParserSettings, 'fallback'> {\n divider?: string;\n delimiter?: string;\n symbol?: string;\n emptyText?: string;\n}\n\n/**\n * Formats a value as a percent string with configurable decimal precision,\n * decimal divider and suffix symbol.\n *\n * @param value Number or numeric string to format.\n * @param settings Parser and formatting options.\n * @returns Formatted percent string or fallback text for invalid values.\n *\n * @example\n * ```ts\n * percent(12.345); // '12.35%'\n * ```\n *\n * @example\n * ```ts\n * percent(12.345, { divider: ',', symbol: ' pct' }); // '12,35 pct'\n * ```\n */\nexport const percent = (\n value: Maybe<number | string>,\n settings?: PercentFormatSettings,\n) => {\n const numericValue = parser.number(value, {\n ...settings,\n digits: settings?.digits ?? 2,\n fallback: Number.NaN,\n });\n\n if (typeGuard.isNumber(numericValue)) {\n const divider = settings?.divider ?? '.';\n\n const formattedPercent =\n divider === '.' ? numericValue : `${numericValue}`.replace('.', divider);\n\n return `${formattedPercent}${settings?.delimiter ?? ''}${settings?.symbol ?? '%'}`;\n } else {\n return settings?.emptyText ?? NO_VALUE;\n }\n};\n","/**\n * Removes all whitespace characters from a string.\n *\n * @param value Source string.\n * @returns String without spaces, tabs and line breaks.\n *\n * @example\n * ```ts\n * skipSpaces('1 000 000'); // '1000000'\n * ```\n *\n * @example\n * ```ts\n * skipSpaces('a\\tb\\nc'); // 'abc'\n * ```\n */\nexport const skipSpaces = (value: string) => value.replaceAll(/\\s/g, '');\n","/**\n * ---header-docs-section---\n * # yummies/format\n *\n * ## Description\n *\n * Namespace-style **number, percent, and string** formatting for UI tables, inputs, and charts.\n * Submodules cover locale-aware numbers, percent parsing/formatting, trimming helpers, and shared\n * constants so presentation rules stay centralized and tree-shakable behind the package\n * aggregated **format** export from this entry point.\n *\n * ## Usage\n *\n * ```ts\n * import { format } from \"yummies/format\";\n * ```\n */\n\nexport * from './constants.js';\nexport * from './number.js';\nexport * from './percent.js';\nexport * from './skip-spaces.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyDA,IAAa,UACX,UACA,iBACW;CACX,MAAM,WAAW;EACf,GAAG,OAAO;EACV,GAAG;EACJ;CAED,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,UAAU,YAAY;CACvC,MAAM,YAAY,SAAS,aAAa;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,aAAA;CAC3B,MAAM,qBAAqB,SAAS,sBAAsB;CAE1D,IAAI;AAEJ,KAAI,UAAU,SAAS,SAAS,CAC9B,SAAQ,OAAO,OAAO,OAAO,EAAE,UAAU,KAAA,GAAW,CAAC;KAErD,SAAQ;AAGV,KAAI,UAAU,SAAS,MAAM,EAAE;EAC7B,IAAI,MAAc,GAAG;AAErB,MAAI,WAAW,MACb,KAAI,SAAS,gBAAgB;GAC3B,MAAM,CAAC,aAAa,eAAe,GAAG,MAAM,MAAM,IAAI;GACtD,MAAM,WAAW;GACjB,MAAM,aAAa,eAAe,IAC/B,MAAM,GAAG,OAAO,CAChB,OAAO,QAAQ,IAAI;AAEtB,OAAI,UACF,OAAM,GAAG,SAAS,GAAG;OAErB,OAAM;QAGR,OAAM,MAAM,QAAQ,OAAO;AAI/B,MAAI,SACF,OAAM,GAAG,CAAC;EAGZ,MAAM,CAAC,aAAa,eAAe,IAAI,MAAM,KAAK,EAAE;EAEpD,IAAI,uBAAuB;EAC3B,IAAI,uBAAuB;AAE3B,MAAI,gBAAgB,CAAC,sBAAsB,CAAC,OAAO,KAAK,YAAY,EAClE,wBAAuB,IAAI;EAG7B,MAAM,MAAM;AAEZ,SAAO,IAAI,KAAK,qBAAqB,IAAI,UACvC,wBAAuB,qBAAqB,QAC1C,KACA,KAAK,UAAU,IAChB;AAGH,SAAO,uBAAuB,uBAAuB;;AAGvD,QAAO;;AAGT,OAAO,kBAAkB,EAAE;;;;;;;;;;;;;;;;;;;;;AClG3B,IAAa,WACX,OACA,aACG;CACH,MAAM,eAAe,OAAO,OAAO,OAAO;EACxC,GAAG;EACH,QAAQ,UAAU,UAAU;EAC5B,UAAU;EACX,CAAC;AAEF,KAAI,UAAU,SAAS,aAAa,EAAE;EACpC,MAAM,UAAU,UAAU,WAAW;AAKrC,SAAO,GAFL,YAAY,MAAM,eAAe,GAAG,eAAe,QAAQ,KAAK,QAAQ,GAE7C,UAAU,aAAa,KAAK,UAAU,UAAU;OAE7E,QAAO,UAAU,aAAA;;;;;;;;;;;;;;;;;;;;AClCrB,IAAa,cAAc,UAAkB,MAAM,WAAW,OAAO,GAAG"}
|
package/html.cjs
CHANGED
|
@@ -5,6 +5,22 @@ dompurify = require_chunk.__toESM(dompurify);
|
|
|
5
5
|
let yummies_media = require("yummies/media");
|
|
6
6
|
//#region src/html.ts
|
|
7
7
|
/**
|
|
8
|
+
* ---header-docs-section---
|
|
9
|
+
* # yummies/html
|
|
10
|
+
*
|
|
11
|
+
* ## Description
|
|
12
|
+
*
|
|
13
|
+
* DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via
|
|
14
|
+
* temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with
|
|
15
|
+
* raw user input; keep CSP and server-side validation as the real security boundary.
|
|
16
|
+
*
|
|
17
|
+
* ## Usage
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { getComputedColor } from "yummies/html";
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
8
24
|
* Extracts an RGB value from any valid CSS color.
|
|
9
25
|
*
|
|
10
26
|
* Not recommended for frequent use because it triggers a reflow.
|
package/html.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.cjs","names":[],"sources":["../src/html.ts"],"sourcesContent":["import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"html.cjs","names":[],"sources":["../src/html.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/html\n *\n * ## Description\n *\n * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via\n * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with\n * raw user input; keep CSP and server-side validation as the real security boundary.\n *\n * ## Usage\n *\n * ```ts\n * import { getComputedColor } from \"yummies/html\";\n * ```\n */\n\nimport DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
|
package/html.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { Config } from 'dompurify';
|
|
2
2
|
import { Maybe } from 'yummies/types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* ---header-docs-section---
|
|
6
|
+
* # yummies/html
|
|
7
|
+
*
|
|
8
|
+
* ## Description
|
|
9
|
+
*
|
|
10
|
+
* DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via
|
|
11
|
+
* temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with
|
|
12
|
+
* raw user input; keep CSP and server-side validation as the real security boundary.
|
|
13
|
+
*
|
|
14
|
+
* ## Usage
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { getComputedColor } from "yummies/html";
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
4
21
|
/**
|
|
5
22
|
* Extracts an RGB value from any valid CSS color.
|
|
6
23
|
*
|
package/html.js
CHANGED
|
@@ -2,6 +2,22 @@ import DOMPurify from "dompurify";
|
|
|
2
2
|
import { blobToUrl } from "yummies/media";
|
|
3
3
|
//#region src/html.ts
|
|
4
4
|
/**
|
|
5
|
+
* ---header-docs-section---
|
|
6
|
+
* # yummies/html
|
|
7
|
+
*
|
|
8
|
+
* ## Description
|
|
9
|
+
*
|
|
10
|
+
* DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via
|
|
11
|
+
* temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with
|
|
12
|
+
* raw user input; keep CSP and server-side validation as the real security boundary.
|
|
13
|
+
*
|
|
14
|
+
* ## Usage
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { getComputedColor } from "yummies/html";
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
5
21
|
* Extracts an RGB value from any valid CSS color.
|
|
6
22
|
*
|
|
7
23
|
* Not recommended for frequent use because it triggers a reflow.
|
package/html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/html\n *\n * ## Description\n *\n * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via\n * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with\n * raw user input; keep CSP and server-side validation as the real security boundary.\n *\n * ## Usage\n *\n * ```ts\n * import { getComputedColor } from \"yummies/html\";\n * ```\n */\n\nimport DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
|
package/id.cjs
CHANGED
|
@@ -2,6 +2,22 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
require("./chunk-CVq3Gv4J.cjs");
|
|
3
3
|
let nanoid = require("nanoid");
|
|
4
4
|
//#region src/id.ts
|
|
5
|
+
/**
|
|
6
|
+
* ---header-docs-section---
|
|
7
|
+
* # yummies/id
|
|
8
|
+
*
|
|
9
|
+
* ## Description
|
|
10
|
+
*
|
|
11
|
+
* Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for
|
|
12
|
+
* client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are
|
|
13
|
+
* unlikely at these lengths but still assume server-side uniqueness for persisted entities.
|
|
14
|
+
*
|
|
15
|
+
* ## Usage
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { generateId } from "yummies/id";
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
5
21
|
var DIGITS = "0123456789";
|
|
6
22
|
var ALPHABET = `abcdefghijklmnopqrstuvwxyz${DIGITS}`;
|
|
7
23
|
/**
|
package/id.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.cjs","names":[],"sources":["../src/id.ts"],"sourcesContent":["import { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"id.cjs","names":[],"sources":["../src/id.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/id\n *\n * ## Description\n *\n * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for\n * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are\n * unlikely at these lengths but still assume server-side uniqueness for persisted entities.\n *\n * ## Usage\n *\n * ```ts\n * import { generateId } from \"yummies/id\";\n * ```\n */\n\nimport { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,IAAM,SAAS;AAGf,IAAM,WAAW,6BAAiB;;;;;AAMlC,IAAa,cAAA,GAAA,OAAA,gBAA4B,UAAU,EAAE;;;;;AAMrD,IAAa,mBAAA,GAAA,OAAA,gBAAiC,UAAU,EAAE;;;;;AAM1D,IAAa,qBAAA,GAAA,OAAA,gBAAmC,QAAQ,EAAE;;;;;AAM1D,IAAa,0BAAA,GAAA,OAAA,gBAAwC,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;AAqB/D,IAAa,kCAAkC,OAAe,MAAM;CAClE,IAAI,YAAY;AAChB,cAAa;AACX,UAAQ,aAAa,UAAU,CAAC,SAAS,MAAM,IAAI;;;;;;;;;;;;;;;;;;AAmBvD,IAAa,0BAA0B,gCAAgC;;;;;;AAOvE,IAAa,8CACX,IAAI,OAAO,EAAC,MAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG"}
|
package/id.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ---header-docs-section---
|
|
3
|
+
* # yummies/id
|
|
4
|
+
*
|
|
5
|
+
* ## Description
|
|
6
|
+
*
|
|
7
|
+
* Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for
|
|
8
|
+
* client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are
|
|
9
|
+
* unlikely at these lengths but still assume server-side uniqueness for persisted entities.
|
|
10
|
+
*
|
|
11
|
+
* ## Usage
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { generateId } from "yummies/id";
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
1
17
|
/**
|
|
2
18
|
* Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.
|
|
3
19
|
* Length: 6.
|
package/id.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { customAlphabet } from "nanoid";
|
|
2
2
|
//#region src/id.ts
|
|
3
|
+
/**
|
|
4
|
+
* ---header-docs-section---
|
|
5
|
+
* # yummies/id
|
|
6
|
+
*
|
|
7
|
+
* ## Description
|
|
8
|
+
*
|
|
9
|
+
* Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for
|
|
10
|
+
* client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are
|
|
11
|
+
* unlikely at these lengths but still assume server-side uniqueness for persisted entities.
|
|
12
|
+
*
|
|
13
|
+
* ## Usage
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { generateId } from "yummies/id";
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
3
19
|
var DIGITS = "0123456789";
|
|
4
20
|
var ALPHABET = `abcdefghijklmnopqrstuvwxyz${DIGITS}`;
|
|
5
21
|
/**
|
package/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","names":[],"sources":["../src/id.ts"],"sourcesContent":["import { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"id.js","names":[],"sources":["../src/id.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/id\n *\n * ## Description\n *\n * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for\n * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are\n * unlikely at these lengths but still assume server-side uniqueness for persisted entities.\n *\n * ## Usage\n *\n * ```ts\n * import { generateId } from \"yummies/id\";\n * ```\n */\n\nimport { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,IAAM,SAAS;AAGf,IAAM,WAAW,6BAAiB;;;;;AAMlC,IAAa,aAAa,eAAe,UAAU,EAAE;;;;;AAMrD,IAAa,kBAAkB,eAAe,UAAU,EAAE;;;;;AAM1D,IAAa,oBAAoB,eAAe,QAAQ,EAAE;;;;;AAM1D,IAAa,yBAAyB,eAAe,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;AAqB/D,IAAa,kCAAkC,OAAe,MAAM;CAClE,IAAI,YAAY;AAChB,cAAa;AACX,UAAQ,aAAa,UAAU,CAAC,SAAS,MAAM,IAAI;;;;;;;;;;;;;;;;;;AAmBvD,IAAa,0BAA0B,gCAAgC;;;;;;AAOvE,IAAa,8CACX,IAAI,OAAO,EAAC,MAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG"}
|
package/imports.cjs
CHANGED
|
@@ -3,6 +3,22 @@ require("./chunk-CVq3Gv4J.cjs");
|
|
|
3
3
|
let yummies_async = require("yummies/async");
|
|
4
4
|
//#region src/imports.ts
|
|
5
5
|
/**
|
|
6
|
+
* ---header-docs-section---
|
|
7
|
+
* # yummies/imports
|
|
8
|
+
*
|
|
9
|
+
* ## Description
|
|
10
|
+
*
|
|
11
|
+
* Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM
|
|
12
|
+
* shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,
|
|
13
|
+
* and resilient loading when the network or CDN flakes without freezing the whole app.
|
|
14
|
+
*
|
|
15
|
+
* ## Usage
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { fetchLazyModule } from "yummies/imports";
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
6
22
|
* Lazily loads a module with retry support.
|
|
7
23
|
*
|
|
8
24
|
* @example
|
package/imports.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imports.cjs","names":[],"sources":["../src/imports.ts"],"sourcesContent":["import { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"imports.cjs","names":[],"sources":["../src/imports.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/imports\n *\n * ## Description\n *\n * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM\n * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,\n * and resilient loading when the network or CDN flakes without freezing the whole app.\n *\n * ## Usage\n *\n * ```ts\n * import { fetchLazyModule } from \"yummies/imports\";\n * ```\n */\n\nimport { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,kBAAkB,OAC7B,aACA,WAAW,GACX,QAAQ,QACO;CACf,MAAM,gBAAgB,MAAM,KAAyB,EACnD,QAAQ,UACT,CAAC,CAAC,KAAK,YAAY;CAEpB,IAAI,YAA0B;AAE9B,YAAW,MAAM,WAAW,cAC1B,KAAI;AACF,MAAI,cAAc,KAChB,QAAA,GAAA,cAAA,OAAY,MAAM;AAEpB,SAAO,MAAM,SAAS;UACf,OAAO;AACd,cAAY;;AAGhB,OAAM;;;;;;;;;;AAaR,IAAa,oBAAoB,OAC/B,gBACe;AACf,KAAI,uBAAuB,SAAS;EAClC,MAAM,OAAQ,MAAM;AAEpB,MAAK,KAAa,QAChB,QAAQ,KAAa;MAErB,QAAO;;AAIX,QAAO"}
|
package/imports.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ---header-docs-section---
|
|
3
|
+
* # yummies/imports
|
|
4
|
+
*
|
|
5
|
+
* ## Description
|
|
6
|
+
*
|
|
7
|
+
* Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM
|
|
8
|
+
* shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,
|
|
9
|
+
* and resilient loading when the network or CDN flakes without freezing the whole app.
|
|
10
|
+
*
|
|
11
|
+
* ## Usage
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { fetchLazyModule } from "yummies/imports";
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
1
17
|
/**
|
|
2
18
|
* Lazily loads a module with retry support.
|
|
3
19
|
*
|
package/imports.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { sleep } from "yummies/async";
|
|
2
2
|
//#region src/imports.ts
|
|
3
3
|
/**
|
|
4
|
+
* ---header-docs-section---
|
|
5
|
+
* # yummies/imports
|
|
6
|
+
*
|
|
7
|
+
* ## Description
|
|
8
|
+
*
|
|
9
|
+
* Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM
|
|
10
|
+
* shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,
|
|
11
|
+
* and resilient loading when the network or CDN flakes without freezing the whole app.
|
|
12
|
+
*
|
|
13
|
+
* ## Usage
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { fetchLazyModule } from "yummies/imports";
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
4
20
|
* Lazily loads a module with retry support.
|
|
5
21
|
*
|
|
6
22
|
* @example
|
package/imports.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imports.js","names":[],"sources":["../src/imports.ts"],"sourcesContent":["import { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"imports.js","names":[],"sources":["../src/imports.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/imports\n *\n * ## Description\n *\n * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM\n * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,\n * and resilient loading when the network or CDN flakes without freezing the whole app.\n *\n * ## Usage\n *\n * ```ts\n * import { fetchLazyModule } from \"yummies/imports\";\n * ```\n */\n\nimport { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,kBAAkB,OAC7B,aACA,WAAW,GACX,QAAQ,QACO;CACf,MAAM,gBAAgB,MAAM,KAAyB,EACnD,QAAQ,UACT,CAAC,CAAC,KAAK,YAAY;CAEpB,IAAI,YAA0B;AAE9B,YAAW,MAAM,WAAW,cAC1B,KAAI;AACF,MAAI,cAAc,KAChB,OAAM,MAAM,MAAM;AAEpB,SAAO,MAAM,SAAS;UACf,OAAO;AACd,cAAY;;AAGhB,OAAM;;;;;;;;;;AAaR,IAAa,oBAAoB,OAC/B,gBACe;AACf,KAAI,uBAAuB,SAAS;EAClC,MAAM,OAAQ,MAAM;AAEpB,MAAK,KAAa,QAChB,QAAQ,KAAa;MAErB,QAAO;;AAIX,QAAO"}
|
package/math.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"math.cjs","names":[],"sources":["../src/math.ts"],"sourcesContent":["import type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"math.cjs","names":[],"sources":["../src/math.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/math\n *\n * ## Description\n *\n * Tiny math helpers: degree/radian conversion and nullable-safe percentage calculations. They wrap\n * common formulas so components do not repeat magic numbers or null checks when deriving layout or\n * charts from partially loaded data.\n *\n * ## Usage\n *\n * ```ts\n * import { degToRad, percentFrom } from \"yummies/math\";\n * ```\n */\n\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;;AA2BA,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;AAW1B,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,MAAM,KAAK;;;;;;;;;;AAW3B,IAAa,eAAe,OAAsB,SAAwB;AACxE,SAAS,SAAS,MAAM,QAAQ,KAAM,OAAO"}
|
package/math.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { Maybe } from 'yummies/types';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* ---header-docs-section---
|
|
5
|
+
* # yummies/math
|
|
6
|
+
*
|
|
7
|
+
* ## Description
|
|
8
|
+
*
|
|
9
|
+
* Tiny math helpers: degree/radian conversion and nullable-safe percentage calculations. They wrap
|
|
10
|
+
* common formulas so components do not repeat magic numbers or null checks when deriving layout or
|
|
11
|
+
* charts from partially loaded data.
|
|
12
|
+
*
|
|
13
|
+
* ## Usage
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { degToRad, percentFrom } from "yummies/math";
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
3
20
|
/**
|
|
4
21
|
* Converts degrees to radians.
|
|
5
22
|
*
|
package/math.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"math.js","names":[],"sources":["../src/math.ts"],"sourcesContent":["import type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"math.js","names":[],"sources":["../src/math.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/math\n *\n * ## Description\n *\n * Tiny math helpers: degree/radian conversion and nullable-safe percentage calculations. They wrap\n * common formulas so components do not repeat magic numbers or null checks when deriving layout or\n * charts from partially loaded data.\n *\n * ## Usage\n *\n * ```ts\n * import { degToRad, percentFrom } from \"yummies/math\";\n * ```\n */\n\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;AA2BA,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;AAW1B,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,MAAM,KAAK;;;;;;;;;;AAW3B,IAAa,eAAe,OAAsB,SAAwB;AACxE,SAAS,SAAS,MAAM,QAAQ,KAAM,OAAO"}
|
package/media.cjs
CHANGED
|
@@ -3,6 +3,22 @@ require("./chunk-CVq3Gv4J.cjs");
|
|
|
3
3
|
let yummies_math = require("yummies/math");
|
|
4
4
|
//#region src/media.ts
|
|
5
5
|
/**
|
|
6
|
+
* ---header-docs-section---
|
|
7
|
+
* # yummies/media
|
|
8
|
+
*
|
|
9
|
+
* ## Description
|
|
10
|
+
*
|
|
11
|
+
* Binary and media helpers: Base64 data URLs, object URLs, image orientation fixes, and related
|
|
12
|
+
* browser APIs wrapped in promises. Centralizes `FileReader`/`URL.createObjectURL` usage so image
|
|
13
|
+
* pipelines and uploads stay consistent and easier to test or mock.
|
|
14
|
+
*
|
|
15
|
+
* ## Usage
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { blobToBase64 } from "yummies/media";
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
6
22
|
* Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.
|
|
7
23
|
*
|
|
8
24
|
* Useful for previewing uploads, embedding small assets inline, or serializing binary for APIs that
|
package/media.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.cjs","names":[],"sources":["../src/media.ts"],"sourcesContent":["import { degToRad } from 'yummies/math';\n\n/**\n * Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.\n *\n * Useful for previewing uploads, embedding small assets inline, or serializing binary for APIs that\n * expect Base64-in-JSON. The result includes the MIME prefix, not raw Base64 alone — use\n * {@link decodeDataUrl} if you need the payload and type separately.\n *\n * @param blob - Any `Blob` or `File` (files are blobs).\n * @returns Resolves to the data URL string; rejects if reading fails.\n *\n * @example\n * ```ts\n * const dataUrl = await blobToBase64(file);\n * previewImg.src = dataUrl;\n * ```\n *\n * @example\n * ```ts\n * const fromFetch = await fetch('/api/export').then((r) => r.blob());\n * const dataUrl = await blobToBase64(fromFetch);\n * ```\n */\nexport function blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * If `urlOrBlob` is already a string, returns it unchanged. If it is a {@link Blob}, returns\n * `URL.createObjectURL(blob)` — a short-lived `blob:` URL valid in this document until\n * {@link URL.revokeObjectURL} is called.\n *\n * Pair with {@link renderImage} or `<img src>` without re-fetching binary data. **Remember to\n * `revokeObjectURL`** when the URL is no longer needed to avoid retaining blob memory.\n *\n * @param urlOrBlob - Remote/http(s) URL string, data URL, or a `Blob` / `File`.\n * @returns A string suitable for `HTMLImageElement.src` or similar.\n *\n * @example\n * ```ts\n * const src = blobToUrl(uploadedFile);\n * img.src = src;\n * // when done:\n * URL.revokeObjectURL(src);\n * ```\n *\n * @example\n * ```ts\n * blobToUrl('https://cdn.example.com/logo.png'); // passed through as-is\n * ```\n */\nexport const blobToUrl = (urlOrBlob: string | Blob) =>\n urlOrBlob instanceof Blob ? URL.createObjectURL(urlOrBlob) : urlOrBlob;\n\n/**\n * Creates a new {@link Blob} from a {@link File}, copying the bytes and keeping `file.type` as\n * the blob’s MIME type. Handy when an API accepts `Blob` but you only have `File`, or you want a\n * plain blob without the `File` name/lastModified metadata.\n *\n * @param file - Source file from an `<input type=\"file\">` or drag-and-drop.\n * @returns A `Blob` with the same content and `type` as the file.\n *\n * @example\n * ```ts\n * const blob = fileToBlob(fileFromInput);\n * await uploadEndpoint(blob);\n * ```\n *\n * @example\n * ```ts\n * const blob = fileToBlob(imageFile);\n * const url = URL.createObjectURL(blob);\n * ```\n */\nexport const fileToBlob = (file: File) => {\n return new Blob([file], { type: file.type });\n};\n\n/**\n * Draws an {@link HTMLImageElement} onto an offscreen canvas, then builds a {@link Blob} from the\n * raster (via `canvas.toDataURL` + binary decode). Dimensions use `naturalWidth` / `naturalHeight`,\n * falling back to `300×300` if those are zero (e.g. not yet decoded).\n *\n * **CORS:** if the image is cross-origin without proper CORS headers, the canvas may be\n * *tainted* and `toDataURL` can throw — same browser rules as any canvas export.\n *\n * @param imageElement - Loaded image (`complete` / decoded recommended).\n * @param mimeType - Output MIME type, e.g. `'image/png'` (default) or `'image/jpeg'`. Encoder support is browser-dependent.\n * @returns Encoded image as a `Blob` with a matching `type` when possible.\n *\n * @example\n * ```ts\n * const img = await renderImage('/photo.jpg');\n * const jpegBlob = imageToBlob(img, 'image/jpeg');\n * ```\n *\n * @example\n * ```ts\n * const pngBlob = imageToBlob(cachedImg); // default image/png\n * ```\n */\nexport const imageToBlob = (\n imageElement: HTMLImageElement,\n mimeType: string = 'image/png',\n) => {\n const canvas = document.createElement('canvas');\n\n canvas.width = imageElement.naturalWidth || 300;\n canvas.height = imageElement.naturalHeight || 300;\n\n canvas.getContext('2d')!.drawImage(imageElement, 0, 0);\n\n const dataUri = canvas.toDataURL(mimeType, 1);\n const base64data = dataUri.split(',')[1];\n const base64MimeType = dataUri.split(';')[0].slice(5);\n\n const bytes = globalThis.atob(base64data);\n const buf = new ArrayBuffer(bytes.length);\n const array = new Uint8Array(buf);\n\n for (let index = 0; index < bytes.length; index++) {\n array[index] = bytes.charCodeAt(index);\n }\n\n const blob = new Blob([array], { type: base64MimeType });\n\n return blob;\n};\n\n/**\n * Loads a resource into a new `HTMLImageElement`: `src` is set via {@link blobToUrl} (so blobs get\n * object URLs, strings are used directly). Resolves on `load` with the same element; rejects on\n * `error` (e.g. bad URL, network failure, corrupt image) **with no rejection value**.\n *\n * Does not add the node to the DOM — use the returned element for canvas, measuring, or\n * {@link imageToBlob}.\n *\n * @param urlOrBlob - Remote URL, data URL, or `Blob` / `File`.\n * @returns Promise that fulfills with the loaded `HTMLImageElement`.\n *\n * @example\n * ```ts\n * const img = await renderImage('https://example.com/pic.png');\n * document.body.appendChild(img);\n * ```\n *\n * @example\n * ```ts\n * const img = await renderImage(pickedFile);\n * const blob = imageToBlob(img, 'image/webp');\n * ```\n */\nexport const renderImage = (urlOrBlob: Blob | string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.src = blobToUrl(urlOrBlob);\n image.onload = () => resolve(image);\n image.onerror = () => reject();\n });\n\nfunction cropImageFromCanvas(context: CanvasRenderingContext2D) {\n const canvas = context.canvas;\n let w = canvas.width;\n let h = canvas.height;\n const pix: { x: number[]; y: number[] } = { x: [], y: [] };\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n let x: number;\n let y: number;\n let index: number;\n\n for (y = 0; y < h; y++) {\n for (x = 0; x < w; x++) {\n index = (y * w + x) * 4;\n if (imageData.data[index + 3] > 0) {\n pix.x.push(x);\n pix.y.push(y);\n }\n }\n }\n pix.x.sort((a, b) => a - b);\n pix.y.sort((a, b) => a - b);\n const n = pix.x.length - 1;\n\n w = 1 + pix.x[n] - pix.x[0];\n h = 1 + pix.y[n] - pix.y[0];\n const cut = context.getImageData(pix.x[0], pix.y[0], w, h);\n\n canvas.width = w;\n canvas.height = h;\n context.putImageData(cut, 0, 0);\n return canvas;\n}\n\n// TODO: ломает iphone с огромными изображениями\n/**\n * Rotates `image` around its center on a square canvas (side = max(width, height)), then **crops**\n * transparent margins by trimming to the bounding box of pixels with non-zero alpha.\n * Returns a **new** loaded `HTMLImageElement` (PNG data URL under the hood via {@link renderImage}).\n *\n * `angle` is in **degrees**; converted with {@link degToRad} from `yummies/math`.\n *\n * Very large sources can stress memory on some mobile browsers (known iPhone issues — see TODO in source).\n *\n * @param image - Source image (should be decoded; uses `width` / `height`).\n * @param angle - Rotation in degrees (e.g. `90` for quarter turn).\n * @returns Promise of a new `HTMLImageElement` showing the rotated, cropped result.\n *\n * @example\n * ```ts\n * const upright = await rotateImage(landscapeImg, 90);\n * ```\n *\n * @example\n * ```ts\n * const fixed = await rotateImage(await renderImage(file), -15);\n * ```\n */\nexport const rotateImage = (image: HTMLImageElement, angle: number) => {\n const maxSize = Math.max(image.width, image.height);\n const canvas = document.createElement('canvas');\n canvas.width = maxSize;\n canvas.height = maxSize;\n const context = canvas.getContext('2d')!;\n context.save();\n context.translate(canvas.width / 2, canvas.height / 2);\n context.rotate(degToRad(angle));\n context.drawImage(image, -image.width / 2, -image.height / 2);\n context.restore();\n cropImageFromCanvas(context);\n return renderImage(canvas.toDataURL('image/png'));\n};\n\ninterface DecodedDataUrl {\n mimeType?: string;\n data?: string;\n}\n\n/**\n * Parses a `data:` URL of the form `data:<mime>;base64,<payload>` into its MIME type and raw\n * Base64 body (without the `data:...;base64,` prefix). Non-matching strings yield `undefined`\n * fields in the result object.\n *\n * @param url - Full data URL string.\n * @returns `{ mimeType, data }` — both optional when the regex does not match.\n *\n * @example\n * ```ts\n * const { mimeType, data } = decodeDataUrl('data:image/png;base64,iVBORw0KGgo=');\n * // mimeType === 'image/png', data === 'iVBORw0KGgo='\n * ```\n *\n * @example\n * ```ts\n * decodeDataUrl('not-a-data-url'); // { mimeType: undefined, data: undefined }\n * ```\n */\nexport function decodeDataUrl(url: string): DecodedDataUrl {\n const regex = /^data:(.*);base64,\\s?(.*)$/;\n const matches = new RegExp(regex).exec(url);\n\n return {\n mimeType: matches?.[1],\n data: matches?.[2],\n };\n}\n\n/**\n * Returns `true` if `url` starts with `https://` or `http://` (case-sensitive, as in\n * `String#startsWith`). Does not validate that the rest is a well-formed URL — only the scheme\n * prefix. `blob:`, `data:`, and relative paths return `false`.\n *\n * @param url - String to test (often a user-provided or configured href).\n *\n * @example\n * ```ts\n * isHttpUrl('https://example.com/path'); // true\n * isHttpUrl('http://localhost:3000'); // true\n * ```\n *\n * @example\n * ```ts\n * isHttpUrl('//cdn.example.com'); // false — no explicit http(s) prefix\n * isHttpUrl('/assets/logo.png'); // false — relative path\n * ```\n */\nexport const isHttpUrl = (url: string): boolean => {\n return url.startsWith('https://') || url.startsWith('http://');\n};\n\n/**\n * Returns `true` when `str` is a data URL that {@link decodeDataUrl} can parse **and** the MIME\n * type starts with `image/` (e.g. `image/png`, `image/jpeg`). Requires both a non-empty Base64\n * payload and an image MIME — arbitrary `data:text/plain;base64,...` is `false`.\n *\n * @param str - Candidate string (often `img.src` or API payload).\n *\n * @example\n * ```ts\n * isBase64Image('data:image/png;base64,iVBORw0KGgo='); // true\n * ```\n *\n * @example\n * ```ts\n * isBase64Image('https://example.com/x.png'); // false\n * isBase64Image('data:text/plain;base64,SGk='); // false\n * ```\n */\nexport const isBase64Image = (str: string): boolean => {\n const { mimeType, data } = decodeDataUrl(str);\n return !!(data && mimeType?.startsWith('image/'));\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"media.cjs","names":[],"sources":["../src/media.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/media\n *\n * ## Description\n *\n * Binary and media helpers: Base64 data URLs, object URLs, image orientation fixes, and related\n * browser APIs wrapped in promises. Centralizes `FileReader`/`URL.createObjectURL` usage so image\n * pipelines and uploads stay consistent and easier to test or mock.\n *\n * ## Usage\n *\n * ```ts\n * import { blobToBase64 } from \"yummies/media\";\n * ```\n */\n\nimport { degToRad } from 'yummies/math';\n\n/**\n * Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.\n *\n * Useful for previewing uploads, embedding small assets inline, or serializing binary for APIs that\n * expect Base64-in-JSON. The result includes the MIME prefix, not raw Base64 alone — use\n * {@link decodeDataUrl} if you need the payload and type separately.\n *\n * @param blob - Any `Blob` or `File` (files are blobs).\n * @returns Resolves to the data URL string; rejects if reading fails.\n *\n * @example\n * ```ts\n * const dataUrl = await blobToBase64(file);\n * previewImg.src = dataUrl;\n * ```\n *\n * @example\n * ```ts\n * const fromFetch = await fetch('/api/export').then((r) => r.blob());\n * const dataUrl = await blobToBase64(fromFetch);\n * ```\n */\nexport function blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * If `urlOrBlob` is already a string, returns it unchanged. If it is a {@link Blob}, returns\n * `URL.createObjectURL(blob)` — a short-lived `blob:` URL valid in this document until\n * {@link URL.revokeObjectURL} is called.\n *\n * Pair with {@link renderImage} or `<img src>` without re-fetching binary data. **Remember to\n * `revokeObjectURL`** when the URL is no longer needed to avoid retaining blob memory.\n *\n * @param urlOrBlob - Remote/http(s) URL string, data URL, or a `Blob` / `File`.\n * @returns A string suitable for `HTMLImageElement.src` or similar.\n *\n * @example\n * ```ts\n * const src = blobToUrl(uploadedFile);\n * img.src = src;\n * // when done:\n * URL.revokeObjectURL(src);\n * ```\n *\n * @example\n * ```ts\n * blobToUrl('https://cdn.example.com/logo.png'); // passed through as-is\n * ```\n */\nexport const blobToUrl = (urlOrBlob: string | Blob) =>\n urlOrBlob instanceof Blob ? URL.createObjectURL(urlOrBlob) : urlOrBlob;\n\n/**\n * Creates a new {@link Blob} from a {@link File}, copying the bytes and keeping `file.type` as\n * the blob’s MIME type. Handy when an API accepts `Blob` but you only have `File`, or you want a\n * plain blob without the `File` name/lastModified metadata.\n *\n * @param file - Source file from an `<input type=\"file\">` or drag-and-drop.\n * @returns A `Blob` with the same content and `type` as the file.\n *\n * @example\n * ```ts\n * const blob = fileToBlob(fileFromInput);\n * await uploadEndpoint(blob);\n * ```\n *\n * @example\n * ```ts\n * const blob = fileToBlob(imageFile);\n * const url = URL.createObjectURL(blob);\n * ```\n */\nexport const fileToBlob = (file: File) => {\n return new Blob([file], { type: file.type });\n};\n\n/**\n * Draws an {@link HTMLImageElement} onto an offscreen canvas, then builds a {@link Blob} from the\n * raster (via `canvas.toDataURL` + binary decode). Dimensions use `naturalWidth` / `naturalHeight`,\n * falling back to `300×300` if those are zero (e.g. not yet decoded).\n *\n * **CORS:** if the image is cross-origin without proper CORS headers, the canvas may be\n * *tainted* and `toDataURL` can throw — same browser rules as any canvas export.\n *\n * @param imageElement - Loaded image (`complete` / decoded recommended).\n * @param mimeType - Output MIME type, e.g. `'image/png'` (default) or `'image/jpeg'`. Encoder support is browser-dependent.\n * @returns Encoded image as a `Blob` with a matching `type` when possible.\n *\n * @example\n * ```ts\n * const img = await renderImage('/photo.jpg');\n * const jpegBlob = imageToBlob(img, 'image/jpeg');\n * ```\n *\n * @example\n * ```ts\n * const pngBlob = imageToBlob(cachedImg); // default image/png\n * ```\n */\nexport const imageToBlob = (\n imageElement: HTMLImageElement,\n mimeType: string = 'image/png',\n) => {\n const canvas = document.createElement('canvas');\n\n canvas.width = imageElement.naturalWidth || 300;\n canvas.height = imageElement.naturalHeight || 300;\n\n canvas.getContext('2d')!.drawImage(imageElement, 0, 0);\n\n const dataUri = canvas.toDataURL(mimeType, 1);\n const base64data = dataUri.split(',')[1];\n const base64MimeType = dataUri.split(';')[0].slice(5);\n\n const bytes = globalThis.atob(base64data);\n const buf = new ArrayBuffer(bytes.length);\n const array = new Uint8Array(buf);\n\n for (let index = 0; index < bytes.length; index++) {\n array[index] = bytes.charCodeAt(index);\n }\n\n const blob = new Blob([array], { type: base64MimeType });\n\n return blob;\n};\n\n/**\n * Loads a resource into a new `HTMLImageElement`: `src` is set via {@link blobToUrl} (so blobs get\n * object URLs, strings are used directly). Resolves on `load` with the same element; rejects on\n * `error` (e.g. bad URL, network failure, corrupt image) **with no rejection value**.\n *\n * Does not add the node to the DOM — use the returned element for canvas, measuring, or\n * {@link imageToBlob}.\n *\n * @param urlOrBlob - Remote URL, data URL, or `Blob` / `File`.\n * @returns Promise that fulfills with the loaded `HTMLImageElement`.\n *\n * @example\n * ```ts\n * const img = await renderImage('https://example.com/pic.png');\n * document.body.appendChild(img);\n * ```\n *\n * @example\n * ```ts\n * const img = await renderImage(pickedFile);\n * const blob = imageToBlob(img, 'image/webp');\n * ```\n */\nexport const renderImage = (urlOrBlob: Blob | string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.src = blobToUrl(urlOrBlob);\n image.onload = () => resolve(image);\n image.onerror = () => reject();\n });\n\nfunction cropImageFromCanvas(context: CanvasRenderingContext2D) {\n const canvas = context.canvas;\n let w = canvas.width;\n let h = canvas.height;\n const pix: { x: number[]; y: number[] } = { x: [], y: [] };\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n let x: number;\n let y: number;\n let index: number;\n\n for (y = 0; y < h; y++) {\n for (x = 0; x < w; x++) {\n index = (y * w + x) * 4;\n if (imageData.data[index + 3] > 0) {\n pix.x.push(x);\n pix.y.push(y);\n }\n }\n }\n pix.x.sort((a, b) => a - b);\n pix.y.sort((a, b) => a - b);\n const n = pix.x.length - 1;\n\n w = 1 + pix.x[n] - pix.x[0];\n h = 1 + pix.y[n] - pix.y[0];\n const cut = context.getImageData(pix.x[0], pix.y[0], w, h);\n\n canvas.width = w;\n canvas.height = h;\n context.putImageData(cut, 0, 0);\n return canvas;\n}\n\n// TODO: ломает iphone с огромными изображениями\n/**\n * Rotates `image` around its center on a square canvas (side = max(width, height)), then **crops**\n * transparent margins by trimming to the bounding box of pixels with non-zero alpha.\n * Returns a **new** loaded `HTMLImageElement` (PNG data URL under the hood via {@link renderImage}).\n *\n * `angle` is in **degrees**; converted with {@link degToRad} from `yummies/math`.\n *\n * Very large sources can stress memory on some mobile browsers (known iPhone issues — see TODO in source).\n *\n * @param image - Source image (should be decoded; uses `width` / `height`).\n * @param angle - Rotation in degrees (e.g. `90` for quarter turn).\n * @returns Promise of a new `HTMLImageElement` showing the rotated, cropped result.\n *\n * @example\n * ```ts\n * const upright = await rotateImage(landscapeImg, 90);\n * ```\n *\n * @example\n * ```ts\n * const fixed = await rotateImage(await renderImage(file), -15);\n * ```\n */\nexport const rotateImage = (image: HTMLImageElement, angle: number) => {\n const maxSize = Math.max(image.width, image.height);\n const canvas = document.createElement('canvas');\n canvas.width = maxSize;\n canvas.height = maxSize;\n const context = canvas.getContext('2d')!;\n context.save();\n context.translate(canvas.width / 2, canvas.height / 2);\n context.rotate(degToRad(angle));\n context.drawImage(image, -image.width / 2, -image.height / 2);\n context.restore();\n cropImageFromCanvas(context);\n return renderImage(canvas.toDataURL('image/png'));\n};\n\ninterface DecodedDataUrl {\n mimeType?: string;\n data?: string;\n}\n\n/**\n * Parses a `data:` URL of the form `data:<mime>;base64,<payload>` into its MIME type and raw\n * Base64 body (without the `data:...;base64,` prefix). Non-matching strings yield `undefined`\n * fields in the result object.\n *\n * @param url - Full data URL string.\n * @returns `{ mimeType, data }` — both optional when the regex does not match.\n *\n * @example\n * ```ts\n * const { mimeType, data } = decodeDataUrl('data:image/png;base64,iVBORw0KGgo=');\n * // mimeType === 'image/png', data === 'iVBORw0KGgo='\n * ```\n *\n * @example\n * ```ts\n * decodeDataUrl('not-a-data-url'); // { mimeType: undefined, data: undefined }\n * ```\n */\nexport function decodeDataUrl(url: string): DecodedDataUrl {\n const regex = /^data:(.*);base64,\\s?(.*)$/;\n const matches = new RegExp(regex).exec(url);\n\n return {\n mimeType: matches?.[1],\n data: matches?.[2],\n };\n}\n\n/**\n * Returns `true` if `url` starts with `https://` or `http://` (case-sensitive, as in\n * `String#startsWith`). Does not validate that the rest is a well-formed URL — only the scheme\n * prefix. `blob:`, `data:`, and relative paths return `false`.\n *\n * @param url - String to test (often a user-provided or configured href).\n *\n * @example\n * ```ts\n * isHttpUrl('https://example.com/path'); // true\n * isHttpUrl('http://localhost:3000'); // true\n * ```\n *\n * @example\n * ```ts\n * isHttpUrl('//cdn.example.com'); // false — no explicit http(s) prefix\n * isHttpUrl('/assets/logo.png'); // false — relative path\n * ```\n */\nexport const isHttpUrl = (url: string): boolean => {\n return url.startsWith('https://') || url.startsWith('http://');\n};\n\n/**\n * Returns `true` when `str` is a data URL that {@link decodeDataUrl} can parse **and** the MIME\n * type starts with `image/` (e.g. `image/png`, `image/jpeg`). Requires both a non-empty Base64\n * payload and an image MIME — arbitrary `data:text/plain;base64,...` is `false`.\n *\n * @param str - Candidate string (often `img.src` or API payload).\n *\n * @example\n * ```ts\n * isBase64Image('data:image/png;base64,iVBORw0KGgo='); // true\n * ```\n *\n * @example\n * ```ts\n * isBase64Image('https://example.com/x.png'); // false\n * isBase64Image('data:text/plain;base64,SGk='); // false\n * ```\n */\nexport const isBase64Image = (str: string): boolean => {\n const { mimeType, data } = decodeDataUrl(str);\n return !!(data && mimeType?.startsWith('image/'));\n};\n"],"mappings":"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"}
|
package/media.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ---header-docs-section---
|
|
3
|
+
* # yummies/media
|
|
4
|
+
*
|
|
5
|
+
* ## Description
|
|
6
|
+
*
|
|
7
|
+
* Binary and media helpers: Base64 data URLs, object URLs, image orientation fixes, and related
|
|
8
|
+
* browser APIs wrapped in promises. Centralizes `FileReader`/`URL.createObjectURL` usage so image
|
|
9
|
+
* pipelines and uploads stay consistent and easier to test or mock.
|
|
10
|
+
*
|
|
11
|
+
* ## Usage
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { blobToBase64 } from "yummies/media";
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
1
17
|
/**
|
|
2
18
|
* Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.
|
|
3
19
|
*
|