import {
isTrustedHtml,
type SanitizedHtml,
toSanitizedHtml,
unwrapTrustedHtml,
} from '../security/trusted-html';
const BOOLEAN_ATTRIBUTE_MARKER: unique symbol = Symbol('bquery.booleanAttribute');
const BOOLEAN_ATTRIBUTE_NAME = /^[^\0-\x20"'/>=]+$/;
/**
* Public shape of a boolean HTML attribute created by {@link bool}.
*
* This type is returned from {@link bool} and can be interpolated into
* {@link html} / {@link safeHtml} templates to conditionally include or omit
* an attribute by name. The internal marker property used for runtime checks
* remains private and is not part of the public API.
*
* @example
* ```ts
* const disabled = bool('disabled', isDisabled);
* const button = html``;
* ```
*/
export interface BooleanAttribute {
readonly enabled: boolean;
readonly name: string;
}
interface BooleanAttributeValue extends BooleanAttribute {
readonly [BOOLEAN_ATTRIBUTE_MARKER]: true;
}
const isBooleanAttributeValue = (value: unknown): value is BooleanAttributeValue => {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as Partial;
return (
candidate[BOOLEAN_ATTRIBUTE_MARKER] === true &&
typeof candidate.enabled === 'boolean' &&
typeof candidate.name === 'string'
);
};
const stringifyTemplateValue = (value: unknown): string => {
if (isBooleanAttributeValue(value)) {
return value.enabled ? value.name : '';
}
return String(value ?? '');
};
const escapeMap: Record = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`',
};
const escapeTemplateValue = (value: unknown): string => {
if (isBooleanAttributeValue(value)) {
return value.enabled ? value.name : '';
}
return stringifyTemplateValue(value).replace(/[&<>"'`]/g, (char) => escapeMap[char]);
};
/**
* Creates a boolean-attribute marker for the {@link html} and {@link safeHtml} template tags.
*
* When the condition is truthy, the attribute name is rendered without a value.
* When the condition is falsy, an empty string is rendered and any surrounding
* template-literal whitespace is preserved.
*
* @param name - HTML attribute name to emit
* @param enabled - Whether the boolean attribute should be present
* @returns Internal marker consumed by template tags
*
* @example
* ```ts
* html``;
* // Result when isDisabled = true: ''
* ```
*/
export const bool = (name: string, enabled: unknown): BooleanAttribute => {
if (!BOOLEAN_ATTRIBUTE_NAME.test(name)) {
throw new TypeError(`Invalid boolean attribute name: ${name}`);
}
const attribute: BooleanAttributeValue = {
[BOOLEAN_ATTRIBUTE_MARKER]: true,
enabled: Boolean(enabled),
name,
};
return Object.freeze(attribute);
};
/**
* Tagged template literal for creating HTML strings.
*
* This function handles interpolation of values into HTML templates,
* converting null/undefined to empty strings.
*
* @param strings - Template literal string parts
* @param values - Interpolated values
* @returns Combined HTML string
*
* @example
* ```ts
* const name = 'World';
* const greeting = html`
Hello, ${name}!
`;
* // Result: '
Hello, World!
'
* ```
*/
export const html = (strings: TemplateStringsArray, ...values: unknown[]): string => {
return strings.reduce(
(acc, part, index) => `${acc}${part}${stringifyTemplateValue(values[index])}`,
''
);
};
/**
* Escapes HTML entities in interpolated values for XSS prevention.
* Use this when you need to safely embed user content in templates.
*
* @param strings - Template literal string parts
* @param values - Interpolated values to escape
* @returns Branded escaped HTML string safe for bQuery template composition
*
* @example
* ```ts
* const userInput = '';
* const safe = safeHtml`