import { BindingExpression, bindingMode } from 'aurelia-binding'; import { DOM, FEATURE } from 'aurelia-pal'; import { BindableProperty } from './bindable-property'; import { BindingLanguage } from './binding-language'; import { HtmlBehaviorResource } from './html-behavior'; import { BehaviorInstruction, TargetInstruction, ViewCompileInstruction } from './instructions'; import { ShadowDOM } from './shadow-dom'; import { ViewFactory } from './view-factory'; import { ViewResources } from './view-resources'; let nextInjectorId = 0; function getNextInjectorId() { return ++nextInjectorId; } let lastAUTargetID = 0; function getNextAUTargetID() { return (++lastAUTargetID).toString(); } function makeIntoInstructionTarget(element) { let value = element.getAttribute('class'); let auTargetID = getNextAUTargetID(); element.setAttribute('class', (value ? value + ' au-target' : 'au-target')); element.setAttribute('au-target-id', auTargetID); return auTargetID; } function makeShadowSlot(compiler, resources, node, instructions, parentInjectorId) { let auShadowSlot = DOM.createElement('au-shadow-slot'); DOM.replaceNode(auShadowSlot, node); let auTargetID = makeIntoInstructionTarget(auShadowSlot); let instruction = TargetInstruction.shadowSlot(parentInjectorId); instruction.slotName = node.getAttribute('name') || ShadowDOM.defaultSlotKey; instruction.slotDestination = node.getAttribute('slot'); if (node.innerHTML.trim()) { let fragment = DOM.createDocumentFragment(); let child; while (child = node.firstChild) { fragment.appendChild(child); } instruction.slotFallbackFactory = compiler.compile(fragment, resources); } instructions[auTargetID] = instruction; return auShadowSlot; } const defaultLetHandler = BindingLanguage.prototype.createLetExpressions; /** * Compiles html templates, dom fragments and strings into ViewFactory instances, capable of instantiating Views. */ export class ViewCompiler { /** @internal */ static inject() { return [BindingLanguage, ViewResources]; } /** @internal */ bindingLanguage: BindingLanguage; /** @internal */ private resources: ViewResources; /** * Creates an instance of ViewCompiler. * @param bindingLanguage The default data binding language and syntax used during view compilation. * @param resources The global resources used during compilation when none are provided for compilation. */ constructor(bindingLanguage: BindingLanguage, resources: ViewResources) { this.bindingLanguage = bindingLanguage; this.resources = resources; } /** * Compiles an html template, dom fragment or string into ViewFactory instances, capable of instantiating Views. * @param source The template, fragment or string to compile. * @param resources The view resources used during compilation. * @param compileInstruction A set of instructions that customize how compilation occurs. * @return The compiled ViewFactory. */ compile(source: Element|DocumentFragment|string, resources?: ViewResources, compileInstruction?: ViewCompileInstruction): ViewFactory { resources = resources || this.resources; compileInstruction = compileInstruction || ViewCompileInstruction.normal; source = typeof source === 'string' ? DOM.createTemplateFromMarkup(source) : source; let content; let part; let cacheSize; if ((source as HTMLTemplateElement).content) { part = (source as Element).getAttribute('part'); cacheSize = (source as Element).getAttribute('view-cache'); content = DOM.adoptNode((source as HTMLTemplateElement).content); } else { content = source; } compileInstruction.targetShadowDOM = compileInstruction.targetShadowDOM && FEATURE.shadowDOM; resources._invokeHook('beforeCompile', content, resources, compileInstruction); let instructions = {}; this._compileNode(content, resources, instructions, source, 'root', !compileInstruction.targetShadowDOM); let firstChild = content.firstChild; if (firstChild && firstChild.nodeType === 1) { let targetId = firstChild.getAttribute('au-target-id'); if (targetId) { let ins = instructions[targetId]; if (ins.shadowSlot || ins.lifting || (ins.elementInstruction && !ins.elementInstruction.anchorIsContainer)) { content.insertBefore(DOM.createComment('view'), firstChild); } } } let factory = new ViewFactory(content, instructions, resources); factory.surrogateInstruction = compileInstruction.compileSurrogate ? this._compileSurrogate(source, resources) : null; factory.part = part; if (cacheSize) { factory.setCacheSize(cacheSize); } resources._invokeHook('afterCompile', factory); return factory; } /** @internal */ _compileNode(node, resources, instructions, parentNode, parentInjectorId, targetLightDOM) { switch (node.nodeType) { case 1: //element node return this._compileElement(node, resources, instructions, parentNode, parentInjectorId, targetLightDOM); case 3: //text node //use wholeText to retrieve the textContent of all adjacent text nodes. let expression = resources.getBindingLanguage(this.bindingLanguage).inspectTextContent(resources, node.wholeText); if (expression) { let marker = DOM.createElement('au-marker'); let auTargetID = makeIntoInstructionTarget(marker); (node.parentNode || parentNode).insertBefore(marker, node); node.textContent = ' '; instructions[auTargetID] = TargetInstruction.contentExpression(expression); //remove adjacent text nodes. while (node.nextSibling && node.nextSibling.nodeType === 3) { (node.parentNode || parentNode).removeChild(node.nextSibling); } } else { //skip parsing adjacent text nodes. while (node.nextSibling && node.nextSibling.nodeType === 3) { node = node.nextSibling; } } return node.nextSibling; case 11: //document fragment node let currentChild = node.firstChild; while (currentChild) { currentChild = this._compileNode(currentChild, resources, instructions, node, parentInjectorId, targetLightDOM); } break; default: break; } return node.nextSibling; } /** @internal */ _compileSurrogate(node, resources: ViewResources) { let tagName = node.tagName.toLowerCase(); let attributes = node.attributes; let bindingLanguage = resources.getBindingLanguage(this.bindingLanguage); let knownAttribute; let property: BindableProperty; let instruction: BehaviorInstruction; let i; let ii; let attr; let attrName; let attrValue; let info: AttributeInfo; let type: HtmlBehaviorResource; let expressions: (string | BindingExpression | BehaviorInstruction)[] = []; let expression: BindingExpression & { attrToRemove?: string }; let behaviorInstructions: BehaviorInstruction[] = []; let values = {}; let hasValues = false; let providers = []; for (i = 0, ii = attributes.length; i < ii; ++i) { attr = attributes[i]; attrName = attr.name; attrValue = attr.value; info = bindingLanguage.inspectAttribute(resources, tagName, attrName, attrValue) as AttributeInfo; type = resources.getAttribute(info.attrName); if (type) { //do we have an attached behavior? knownAttribute = resources.mapAttribute(info.attrName); //map the local name to real name if (knownAttribute) { property = type.attributes[knownAttribute]; if (property) { //if there's a defined property info.defaultBindingMode = property.defaultBindingMode; //set the default binding mode if (!info.command && !info.expression) { // if there is no command or detected expression info.command = property.hasOptions ? 'options' : null; //and it is an optons property, set the options command } // if the attribute itself is bound to a default attribute value then we have to // associate the attribute value with the name of the default bindable property // (otherwise it will remain associated with "value") if (info.command && (info.command !== 'options') && type.primaryProperty) { const primaryProperty = type.primaryProperty; attrName = info.attrName = primaryProperty.attribute; // note that the defaultBindingMode always overrides the attribute bindingMode which is only used for "single-value" custom attributes // when using the syntax `
` info.defaultBindingMode = primaryProperty.defaultBindingMode; } } } } instruction = bindingLanguage.createAttributeInstruction(resources, node, info, undefined, type); if (instruction) { //HAS BINDINGS if (instruction.alteredAttr) { type = resources.getAttribute(instruction.attrName); } if (instruction.discrete) { //ref binding or listener binding expressions.push(instruction); } else { //attribute bindings if (type) { //templator or attached behavior found instruction.type = type; this._configureProperties(instruction, resources); if (type.liftsContent) { //template controller throw new Error('You cannot place a template controller on a surrogate element.'); } else { //attached behavior behaviorInstructions.push(instruction); } } else { //standard attribute binding expressions.push(instruction.attributes[instruction.attrName]); } } } else { //NO BINDINGS if (type) { //templator or attached behavior found instruction = BehaviorInstruction.attribute(attrName, type); instruction.attributes[resources.mapAttribute(attrName)] = attrValue; if (type.liftsContent) { //template controller throw new Error('You cannot place a template controller on a surrogate element.'); } else { //attached behavior behaviorInstructions.push(instruction); } } else if (attrName !== 'id' && attrName !== 'part' && attrName !== 'replace-part') { hasValues = true; values[attrName] = attrValue; } } } if (expressions.length || behaviorInstructions.length || hasValues) { for (i = 0, ii = behaviorInstructions.length; i < ii; ++i) { instruction = behaviorInstructions[i]; instruction.type.compile(this, resources, node, instruction); providers.push(instruction.type.target); } for (i = 0, ii = expressions.length; i < ii; ++i) { expression = expressions[i] as BindingExpression; if (expression.attrToRemove !== undefined) { node.removeAttribute(expression.attrToRemove); } } return TargetInstruction.surrogate(providers, behaviorInstructions, expressions, values); } return null; } /** @internal */ _compileElement(node: Element, resources: ViewResources, instructions: any, parentNode: Node, parentInjectorId: number, targetLightDOM: boolean) { let tagName = node.tagName.toLowerCase(); let attributes = node.attributes; let expressions: (string | BindingExpression | BehaviorInstruction)[] = []; let expression: BindingExpression & { attrToRemove?: string }; let behaviorInstructions: BehaviorInstruction[] = []; let providers = []; let bindingLanguage = resources.getBindingLanguage(this.bindingLanguage); let liftingInstruction: BehaviorInstruction; let viewFactory: ViewFactory; let type: HtmlBehaviorResource; let elementInstruction: BehaviorInstruction; let elementProperty: BindableProperty; let i: number; let ii: number; let attr: Attr; let attrName: string; let attrValue; let originalAttrName; let instruction: BehaviorInstruction; let info: AttributeInfo; let property: BindableProperty; let knownAttribute; let auTargetID: string; let injectorId; if (tagName === 'slot') { if (targetLightDOM) { node = makeShadowSlot(this, resources, node, instructions, parentInjectorId); } return node.nextSibling; } else if (tagName === 'template') { if (!('content' in node)) { throw new Error('You cannot place a template element within ' + node.namespaceURI + ' namespace'); } viewFactory = this.compile(node, resources); viewFactory.part = node.getAttribute('part'); } else { type = resources.getElement(node.getAttribute('as-element') || tagName); // Only attempt to process a