import { is_void } from '../../../../shared/utils/names'; import { get_attribute_expression, get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value'; import { boolean_attributes } from '../../../../shared/boolean_attributes'; import Renderer, { RenderOptions } from '../Renderer'; import Element from '../../nodes/Element'; import { p, x } from 'code-red'; import Expression from '../../nodes/shared/Expression'; import remove_whitespace_children from './utils/remove_whitespace_children'; import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; import { namespaces } from '../../../utils/namespaces'; import { start_newline } from '../../../utils/patterns'; import { Node, Expression as ESExpression } from 'estree'; export default function (node: Element, renderer: Renderer, options: RenderOptions) { const children = remove_whitespace_children(node.children, node.next); // awkward special case let node_contents; const contenteditable = ( node.name !== 'textarea' && node.name !== 'input' && node.attributes.some((attribute) => attribute.name === 'contenteditable') ); if (node.is_dynamic_element) { renderer.push(); } renderer.add_string('<'); add_tag_name(); const class_expression_list = node.classes.map(class_directive => { const { expression, name } = class_directive; const snippet = expression ? expression.node : x`#ctx.${name}`; // TODO is this right? return x`${snippet} ? "${name}" : ""`; }); if (node.needs_manual_style_scoping) { class_expression_list.push(x`"${node.component.stylesheet.id}"`); } const class_expression = class_expression_list.length > 0 && class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`); const style_expression_list = node.styles.map(style_directive => { const { name, expression: { node: expression } } = style_directive; return p`"${name}": ${expression}`; }); const style_expression = style_expression_list.length > 0 && x`{ ${style_expression_list} }`; if (node.attributes.some(attr => attr.is_spread)) { // TODO dry this out const args = []; node.attributes.forEach(attribute => { if (attribute.is_spread) { args.push(x`@escape_object(${attribute.expression.node})`); } else { const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name); const name = attribute.name.toLowerCase(); if (name === 'value' && node.name.toLowerCase() === 'textarea') { node_contents = get_attribute_value(attribute); } else if (attribute.is_true) { args.push(x`{ ${attr_name}: true }`); } else if ( boolean_attributes.has(name) && attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text' ) { // a boolean attribute with one non-Text chunk args.push(x`{ ${attr_name}: ${(attribute.chunks[0] as Expression).node} || null }`); } else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') { const snippet = (attribute.chunks[0] as Expression).node; args.push(x`{ ${attr_name}: @escape_attribute_value(${snippet}) }`); } else { args.push(x`{ ${attr_name}: ${get_attribute_value(attribute)} }`); } } }); renderer.add_expression(x`@spread([${args}], { classes: ${class_expression}, styles: ${style_expression} })`); } else { let add_class_attribute = !!class_expression; let add_style_attribute = !!style_expression; node.attributes.forEach(attribute => { const name = attribute.name.toLowerCase(); const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name); if (name === 'value' && node.name.toLowerCase() === 'textarea') { node_contents = get_attribute_value(attribute); } else if (attribute.is_true) { renderer.add_string(` ${attr_name}`); } else if ( boolean_attributes.has(name) && attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text' ) { // a boolean attribute with one non-Text chunk renderer.add_string(' '); renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attr_name}" : ""`); } else if (name === 'class' && class_expression) { add_class_attribute = false; renderer.add_string(` ${attr_name}="`); renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`); renderer.add_string('"'); } else if (name === 'style' && style_expression) { add_style_attribute = false; renderer.add_expression(x`@add_styles(@merge_ssr_styles(${get_attribute_value(attribute)}, ${style_expression}))`); } else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') { const snippet = (attribute.chunks[0] as Expression).node; renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); } else { renderer.add_string(` ${attr_name}="`); renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute)); renderer.add_string('"'); } }); if (add_class_attribute) { renderer.add_expression(x`@add_classes((${class_expression}).trim())`); } if (add_style_attribute) { renderer.add_expression(x`@add_styles(${style_expression})`); } } node.bindings.forEach(binding => { const { name, expression } = binding; if (binding.is_readonly) { return; } if (name === 'group') { const value_attribute = node.attributes.find(({ name }) => name === 'value'); if (value_attribute) { const value = get_attribute_expression(value_attribute); const type = node.get_static_attribute_value('type'); const bound = expression.node; const condition = type === 'checkbox' ? x`~${bound}.indexOf(${value})` : x`${value} === ${bound}`; renderer.add_expression(x`${condition} ? @add_attribute("checked", true, 1) : ""`); } } else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) { node_contents = expression.node; // TODO where was this used? // value = name === 'textContent' ? x`@escape($$value)` : x`$$value`; } else if (binding.name === 'value' && node.name === 'textarea') { const snippet = expression.node; node_contents = x`${snippet} || ""`; } else if (binding.name === 'value' && node.name === 'select') { // NOTE: do not add "value" attribute on } else { const snippet = expression.node; renderer.add_expression(x`@add_attribute("${name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); } }); renderer.add_string('>'); if (node_contents !== undefined) { if (contenteditable) { renderer.push(); renderer.render(children, options); const result = renderer.pop(); renderer.add_expression(x`($$value => $$value === void 0 ? ${result} : $$value)(${node_contents})`); } else { if (node.name === 'textarea') { // Two or more leading newlines are required to restore the leading newline immediately after ``. // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions const value_attribute = node.attributes.find(({ name }) => name === 'value'); if (value_attribute) { const first = value_attribute.chunks[0]; if (first && first.type === 'Text' && start_newline.test(first.data)) { renderer.add_string('\n'); } } } renderer.add_expression(node_contents); } add_close_tag(); } else { if (node.name === 'pre') { // Two or more leading newlines are required to restore the leading newline immediately after ``. // see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions const first = children[0]; if (first && first.type === 'Text' && start_newline.test(first.data)) { renderer.add_string('\n'); } } if (node.is_dynamic_element) renderer.push(); renderer.render(children, options); if (node.is_dynamic_element) { const children = renderer.pop(); renderer.add_expression(x`@is_void(#tag) ? '' : ${children}`); } add_close_tag(); } if (node.is_dynamic_element) { let content: Node = renderer.pop(); if (options.dev && node.children.length > 0) content = x`(() => { @validate_void_dynamic_element(#tag); return ${content}; })()`; renderer.add_expression(x`((#tag) => { ${options.dev && x`@validate_dynamic_element(#tag)`} return #tag ? ${content} : ''; })(${node.tag_expr.node})`); } function add_close_tag() { if (node.tag_expr.node.type === 'Literal') { if (!is_void(node.tag_expr.node.value as string)) { renderer.add_string(''); add_tag_name(); renderer.add_string('>'); } return; } renderer.add_expression(x`@is_void(#tag) ? '' : \`\${#tag}>\``); } function add_tag_name() { if (node.tag_expr.node.type === 'Literal') { renderer.add_string(node.tag_expr.node.value as string); } else { renderer.add_expression(node.tag_expr.node as ESExpression); } } }
`. // see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions const first = children[0]; if (first && first.type === 'Text' && start_newline.test(first.data)) { renderer.add_string('\n'); } } if (node.is_dynamic_element) renderer.push(); renderer.render(children, options); if (node.is_dynamic_element) { const children = renderer.pop(); renderer.add_expression(x`@is_void(#tag) ? '' : ${children}`); } add_close_tag(); } if (node.is_dynamic_element) { let content: Node = renderer.pop(); if (options.dev && node.children.length > 0) content = x`(() => { @validate_void_dynamic_element(#tag); return ${content}; })()`; renderer.add_expression(x`((#tag) => { ${options.dev && x`@validate_dynamic_element(#tag)`} return #tag ? ${content} : ''; })(${node.tag_expr.node})`); } function add_close_tag() { if (node.tag_expr.node.type === 'Literal') { if (!is_void(node.tag_expr.node.value as string)) { renderer.add_string(''); add_tag_name(); renderer.add_string('>'); } return; } renderer.add_expression(x`@is_void(#tag) ? '' : \`\${#tag}>\``); } function add_tag_name() { if (node.tag_expr.node.type === 'Literal') { renderer.add_string(node.tag_expr.node.value as string); } else { renderer.add_expression(node.tag_expr.node as ESExpression); } } }