diff options
Diffstat (limited to 'src/util/html.js')
-rw-r--r-- | src/util/html.js | 100 |
1 files changed, 80 insertions, 20 deletions
diff --git a/src/util/html.js b/src/util/html.js index b8dea51..9e07f9b 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -73,6 +73,15 @@ export const joinChildren = Symbol(); // or when there are multiple children. export const noEdgeWhitespace = Symbol(); +// Pass as a value on an object-shaped set of attributes to indicate that it's +// always, absolutely, no matter what, a valid attribute addition. It will be +// completely exempt from validation, which may provide a significant speed +// boost IF THIS OPERATION IS REPEATED MANY TENS OF THOUSANDS OF TIMES. +// Basically, don't use this unless you're 1) providing a constant set of +// attributes, and 2) writing a very basic building block which loads of other +// content will build off of! +export const blessAttributes = Symbol(); + // Don't pass this directly, use html.metatag('blockwrap') instead. // Causes *following* content (past the metatag) to be placed inside a span // which is styled 'inline-block', which ensures that the words inside the @@ -373,13 +382,12 @@ export class Tag { throw new Error(`Tag <${this.tagName}> is self-closing but got content`); } - let contentArray; - - if (Array.isArray(value)) { - contentArray = value; - } else { - contentArray = [value]; - } + const contentArray = + (Array.isArray(value) + ? value.flat(Infinity).filter(Boolean) + : value + ? [value] + : []); if (this.chunkwrap) { if (contentArray.some(content => content?.blockwrap)) { @@ -387,10 +395,7 @@ export class Tag { } } - this.#content = contentArray - .flat(Infinity) - .filter(Boolean); - + this.#content = contentArray; this.#content.toString = () => this.#stringifyContent(); } @@ -697,7 +702,7 @@ export class Tag { if (index === 0) { content += chunk; } else { - const whitespace = chunk.match(/^\s+/); + const whitespace = chunk.match(/^\s+/) ?? ''; content += chunkwrapSplitter; content += '</span>'; content += whitespace; @@ -994,6 +999,12 @@ export class Attributes { } } + with(...args) { + const clone = this.clone(); + clone.add(...args); + return clone; + } + #addMultipleAttributes(attributes) { const flatInputAttributes = [attributes].flat(Infinity).filter(Boolean); @@ -1007,6 +1018,8 @@ export class Attributes { const setResults = {}; for (const key of Reflect.ownKeys(set)) { + if (key === blessAttributes) continue; + const value = set[key]; setResults[key] = this.#addOneAttribute(key, value); } @@ -1088,8 +1101,17 @@ export class Attributes { return this.#attributes[attribute]; } - has(attribute) { - return attribute in this.#attributes; + has(attribute, pattern) { + if (typeof pattern === 'undefined') { + return attribute in this.#attributes; + } else if (this.has(attribute)) { + const value = this.get(attribute); + if (Array.isArray(value)) { + return value.includes(pattern); + } else { + return value === pattern; + } + } } remove(attribute) { @@ -1325,6 +1347,22 @@ export function smush(smushee) { return smush(Tag.normalize(smushee)); } +// Much gentler version of smush - this only flattens nested html.tags(), and +// guarantees the result is itself an html.tags(). It doesn't manipulate text +// content, and it doesn't resolve templates. +export function smooth(smoothie) { + // Helper function to avoid intermediate html.tags() calls. + function helper(tag) { + if (tag instanceof Tag && tag.contentOnly) { + return tag.content.flatMap(helper); + } else { + return tag; + } + } + + return tags(helper(smoothie)); +} + export function template(description) { return new Template(description); } @@ -1546,14 +1584,16 @@ export class Template { return true; } - if ('validate' in description) { + if (Object.hasOwn(description, 'validate')) { description.validate({ ...commonValidators, ...validators, })(value); + + return true; } - if ('type' in description) { + if (Object.hasOwn(description, 'type')) { switch (description.type) { case 'html': { return isHTML(value); @@ -1564,14 +1604,14 @@ export class Template { } case 'string': { + if (typeof value === 'string') + return true; + // Tags and templates are valid in string arguments - they'll be // stringified when exposed to the description's .content() function. if (value instanceof Tag || value instanceof Template) return true; - if (typeof value !== 'string') - throw new TypeError(`Slot expects string, got ${typeof value}`); - return true; } @@ -1815,9 +1855,29 @@ export const isAttributesAdditionPair = pair => { return true; }; -export const isAttributesAdditionSinglet = +const isAttributesAdditionSingletHelper = anyOf( validateInstanceOf(Template), validateInstanceOf(Attributes), validateAllPropertyValues(isAttributeValue), looseArrayOf(value => isAttributesAdditionSinglet(value))); + +export const isAttributesAdditionSinglet = (value) => { + if (typeof value === 'object' && value !== null) { + if (Object.hasOwn(value, blessAttributes)) { + return true; + } + + if ( + Array.isArray(value) && + value.length === 1 && + typeof value[0] === 'object' && + value[0] !== null && + Object.hasOwn(value[0], blessAttributes) + ) { + return true; + } + } + + return isAttributesAdditionSingletHelper(value); +}; |