diff --git a/AGENTS.md b/AGENTS.md index 99aab8ac..f0e750ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,26 @@ For convenience, include this exact sentence in the PR description when you want ## Detailed Component Documentation +### Field Masking + +SmarkForm's declarative masking API allows integrating external masking libraries via `SmarkForm.registerMask(name, factory)` and ` +``` + +The `script[type="smark-mask"]` tag is inert — browsers don't execute it. +SmarkForm scans for these during initialization, evaluates the content, and +registers each named mask function internally. + +This works naturally inside **mixin templates** — a mixin that uses a masked +field can carry its own `\n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n , node\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n , node\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n , node\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n , node\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n , node\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n , node\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n , node\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n , node\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested +
+ + +
+