diff options
Diffstat (limited to 'src/html.js')
-rw-r--r-- | src/html.js | 174 |
1 files changed, 148 insertions, 26 deletions
diff --git a/src/html.js b/src/html.js index 0fe424df..dd1d1960 100644 --- a/src/html.js +++ b/src/html.js @@ -53,6 +53,17 @@ export const attributeSpec = { }, }; +let disabledSlotValidation = false; +let disabledTagTracing = false; + +export function disableSlotValidation() { + disabledSlotValidation = true; +} + +export function disableTagTracing() { + disabledTagTracing = true; +} + // Pass to tag() as an attributes key to make tag() return a 8lank tag if the // provided content is empty. Useful for when you'll only 8e showing an element // according to the presence of content that would 8elong there. @@ -223,7 +234,11 @@ export function isBlank(content) { // could include content. These need to be checked too. // Check each of the templates one at a time. for (const template of result) { - const content = template.content; + // Resolve the content all the way down to a tag - + // if it's a template that returns another template, + // that won't do, because we need to detect if its + // final content is a tag marked onlyIfSiblings. + const content = normalize(template); if (content instanceof Tag && content.onlyIfSiblings) { continue; @@ -352,8 +367,10 @@ export class Tag { this.attributes = attributes; this.content = content; - this.#traceError = new Error(); - } + if (!disabledTagTracing) { + this.#traceError = new Error(); + } +} clone() { return Reflect.construct(this.constructor, [ @@ -512,6 +529,10 @@ export class Tag { } } + #getAttributeRaw(attribute) { + return this.attributes.get(attribute); + } + set onlyIfContent(value) { this.#setAttributeFlag(onlyIfContent, value); } @@ -579,7 +600,7 @@ export class Tag { try { this.content = this.content; - } catch (error) { + } catch { this.#setAttributeFlag(imaginarySibling, false); } } @@ -662,7 +683,7 @@ export class Tag { const chunkwrapSplitter = (this.chunkwrap - ? this.#getAttributeString('split') + ? this.#getAttributeRaw('split') : null); let seenChunkwrapSplitter = @@ -702,17 +723,19 @@ export class Tag { `of ${inspect(this, {compact: true})}`, {cause: caughtError}); - error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; - error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError; + if (this.#traceError && !disabledTagTracing) { + error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)] = true; + error[Symbol.for(`hsmusic.aggregate.traceFrom`)] = this.#traceError; - error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ - /content-function\.js/, - /util\/html\.js/, - ]; + error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)] = [ + /content-function\.js/, + /util\/html\.js/, + ]; - error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ - /content\/dependencies\/(.*\.js:.*(?=\)))/, - ]; + error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)] = [ + /content\/dependencies\/(.*\.js:.*(?=\)))/, + ]; + } throw error; } @@ -727,7 +750,7 @@ export class Tag { const chunkwrapChunks = (typeof nonTemplateItem === 'string' && chunkwrapSplitter - ? itemContent.split(chunkwrapSplitter) + ? Array.from(getChunkwrapChunks(itemContent, chunkwrapSplitter)) : null); const itemIncludesChunkwrapSplit = @@ -773,7 +796,7 @@ export class Tag { appendItemContent: { if (itemIncludesChunkwrapSplit) { - for (const [index, chunk] of chunkwrapChunks.entries()) { + for (const [index, {chunk, following}] of chunkwrapChunks.entries()) { if (index === 0) { // The first chunk isn't actually a chunk all on its own, it's // text that should be appended to the previous chunk. We will @@ -781,12 +804,27 @@ export class Tag { // the next chunk. content += chunk; } else { - const whitespace = chunk.match(/^\s+/) ?? ''; - content += chunkwrapSplitter; + const followingWhitespace = following.match(/\s+$/) ?? ''; + const chunkWhitespace = chunk.match(/^\s+/) ?? ''; + + if (followingWhitespace) { + content += following.slice(0, -followingWhitespace.length); + } else { + content += following; + } + content += '</span>'; - content += whitespace; + + content += followingWhitespace; + content += chunkWhitespace; + content += '<span class="chunkwrap">'; - content += chunk.slice(whitespace.length); + + if (chunkWhitespace) { + content += chunk.slice(chunkWhitespace.length); + } else { + content += chunk; + } } } @@ -1009,6 +1047,49 @@ export class Tag { } } +export function* getChunkwrapChunks(content, splitter) { + const splitString = + (typeof splitter === 'string' + ? splitter + : null); + + if (splitString) { + let following = ''; + for (const chunk of content.split(splitString)) { + yield {chunk, following}; + following = splitString; + } + + return; + } + + const splitRegExp = + (splitter instanceof RegExp + ? new RegExp( + splitter.source, + (splitter.flags.includes('g') + ? splitter.flags + : splitter.flags + 'g')) + : null); + + if (splitRegExp) { + let following = ''; + let prevIndex = 0; + for (const match of content.matchAll(splitRegExp)) { + const chunk = content.slice(prevIndex, match.index); + yield {chunk, following}; + + following = match[0]; + prevIndex = match.index + match[0].length; + } + + const chunk = content.slice(prevIndex); + yield {chunk, following}; + + return; + } +} + export function attributes(attributes) { return new Attributes(attributes); } @@ -1069,6 +1150,34 @@ export class Attributes { } add(...args) { + // Very common case: add({class: 'foo', id: 'bar'})¡ + // The argument is a plain object (no Template, no Attributes, + // no blessAttributes symbol). We can skip the expensive + // isAttributesAdditionSinglet() validation and flatten/array handling. + if ( + args.length === 1 && + args[0] && + typeof args[0] === 'object' && + !Array.isArray(args[0]) && + !(args[0] instanceof Attributes) && + !(args[0] instanceof Template) && + !Object.hasOwn(args[0], blessAttributes) + ) { + const obj = args[0]; + + // Preserve existing merge semantics by funnelling each key through + // the internal #addOneAttribute helper (handles class/style union, + // unique merging, etc.) but avoid *per-object* validation overhead. + for (const key of Reflect.ownKeys(obj)) { + this.#addOneAttribute(key, obj[key]); + } + + // Match the original return style (list of results) so callers that + // inspect the return continue to work. + return obj; + } + + // Fall back to the original slow-but-thorough implementation switch (args.length) { case 1: isAttributesAdditionSinglet(args[0]); @@ -1080,10 +1189,11 @@ export class Attributes { default: throw new Error( - `Expected array or object, or attribute and value`); + 'Expected array or object, or attribute and value'); } } + with(...args) { const clone = this.clone(); clone.add(...args); @@ -1254,6 +1364,9 @@ export class Attributes { return value.some(Boolean); } else if (value === null) { return false; + } else if (value instanceof RegExp) { + // Oooooooo. + return true; } else { // Other objects are an error. break; @@ -1285,13 +1398,16 @@ export class Attributes { case 'number': return value.toString(); - // If it's a kept object, it's an array. case 'object': { - const joiner = - (descriptor?.arraylike && descriptor?.join) - ?? ' '; + if (Array.isArray(value)) { + const joiner = + (descriptor?.arraylike && descriptor?.join) + ?? ' '; - return value.filter(Boolean).join(joiner); + return value.filter(Boolean).join(joiner); + } else { + return value; + } } default: @@ -1671,6 +1787,10 @@ export class Template { } static validateSlotValueAgainstDescription(value, description) { + if (disabledSlotValidation) { + return true; + } + if (value === undefined) { throw new TypeError(`Specify value as null or don't specify at all`); } @@ -1963,6 +2083,8 @@ export const isAttributeValue = anyOf( isString, isNumber, isBoolean, isArray, isTag, isTemplate, + // Evil. Ooooo + validateInstanceOf(RegExp), validateArrayItems(item => isAttributeValue(item))); export const isAttributesAdditionPair = pair => { |