ngx-dynamic-hooks
Version:
Automatically insert live Angular components into a dynamic string of content (based on their selector or any pattern of your choice) and render the result in the DOM.
1,075 lines (1,059 loc) • 225 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, reflectComponentType, Injectable, SecurityContext, Inject, Optional, isDevMode, PLATFORM_ID, createComponent, APP_INITIALIZER, SkipSelf, createEnvironmentInjector, NgZone, EventEmitter, Component, Input, Output } from '@angular/core';
import { Observable, ReplaySubject, of, combineLatest, firstValueFrom } from 'rxjs';
import { mergeMap, tap, catchError, first, map } from 'rxjs/operators';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import * as i1 from '@angular/platform-browser';
import { createApplication } from '@angular/platform-browser';
/**
* Custom injector tokens that are used for varous internal communication purposes
*/
const DYNAMICHOOKS_ALLSETTINGS = new InjectionToken('All of the settings registered in the whole app.');
const DYNAMICHOOKS_ANCESTORSETTINGS = new InjectionToken('The settings collected from all ancestor injectors');
const DYNAMICHOOKS_MODULESETTINGS = new InjectionToken('The settings for the currently loaded module.');
const contentElementAttr = '__ngx_dynamic_hooks_content';
const anchorElementTag = 'dynamic-component-anchor';
const anchorAttrHookId = '__ngx_dynamic_hooks_anchor_id';
const anchorAttrParseToken = '__ngx_dynamic_hooks_anchor_parsetoken';
const voidElementTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
var DynamicHooksInheritance;
(function (DynamicHooksInheritance) {
/**
* Merges with settings from all injectors in the app.
*/
DynamicHooksInheritance[DynamicHooksInheritance["All"] = 0] = "All";
/**
* (Default) Only merges with settings from direct ancestor injectors (such a father and grandfather injectors, but not "uncle" injectors).
*/
DynamicHooksInheritance[DynamicHooksInheritance["Linear"] = 1] = "Linear";
/**
* Does not merge at all. Injector only uses own settings.
*/
DynamicHooksInheritance[DynamicHooksInheritance["None"] = 2] = "None";
})(DynamicHooksInheritance || (DynamicHooksInheritance = {}));
/**
* Returns the default values for the ParseOptions
*/
const getParseOptionDefaults = () => {
return {
sanitize: true,
convertHTMLEntities: true,
fixParagraphTags: true,
updateOnPushOnly: false,
compareInputsByValue: false,
compareOutputsByValue: false,
compareByValueDepth: 5,
triggerDOMEvents: false,
ignoreInputAliases: false,
ignoreOutputAliases: false,
acceptInputsForAnyProperty: false,
acceptOutputsForAnyObservable: false,
logOptions: {
dev: true,
prod: false,
ssr: false
}
};
};
const regexes = {};
// General
const variableName = '[a-zA-Z_$]+[a-zA-Z0-9_$]*';
const attributeName = '[a-zA-Z$\\-_:][a-zA-Z$\\-_:0-9\\.]*';
// Attribute regex
regexes.attributeNameNoBracketsRegex = '(' + attributeName + ')';
regexes.attributeNameBracketsRegex = '\\[(' + attributeName + ')\\]';
regexes.attributeNameRoundBracketsRegex = '\\((' + attributeName + ')\\)';
regexes.attributeNameRegex = '(?:' + regexes.attributeNameNoBracketsRegex + '|' + regexes.attributeNameBracketsRegex + '|' + regexes.attributeNameRoundBracketsRegex + ')';
regexes.attributeValueDoubleQuotesRegex = '\"((?:\\\\.|[^\"])*?)\"'; // Clever bit of regex to allow escaped chars in strings: https://stackoverflow.com/a/1016356/3099523
regexes.attributeValueSingleQuotesRegex = '\'((?:\\\\.|[^\'])*?)\'';
// Context var regex examples: https://regex101.com/r/zSbY7M/4
// Supports the dot notation, the [] notation as well as function calls () for building variable paths
regexes.variablePathDotNotation = '\\.' + variableName;
regexes.variableBracketsNotation = '\\[[^\\]]*\\]'; // Relies on nested '[]'brackets being encoded
regexes.variablePathFunctionCall = '\\([^\\)]*\\)'; // Relies on nested '()'-brackets being encoded.
regexes.variablePathPartRegex = '(?:' + regexes.variablePathDotNotation + '|' + regexes.variableBracketsNotation + '|' + regexes.variablePathFunctionCall + ')';
regexes.contextVariableRegex = 'context' + regexes.variablePathPartRegex + '*';
regexes.placeholderVariablePathDotNotation = '\\@@@cxtDot@@@' + variableName;
regexes.placeholderVariableBracketsNotation = '@@@cxtOpenSquareBracket@@@[^\\]]*@@@cxtCloseSquareBracket@@@';
regexes.placeholderVariablePathFunctionCall = '@@@cxtOpenRoundBracket@@@[^\\)]*@@@cxtCloseRoundBracket@@@';
regexes.placeholderVariablePathPartRegex = '(?:' + regexes.placeholderVariablePathDotNotation + '|' + regexes.placeholderVariableBracketsNotation + '|' + regexes.placeholderVariablePathFunctionCall + ')';
regexes.placeholderContextVariableRegex = '__CXT__' + regexes.placeholderVariablePathPartRegex + '*';
/**
* Polyfill for String.prototype.matchAll() from the ES2020 spec
*
* Note: The 'string.prototype.matchall' npm package was unstable for me so providing my own version here
*
* @param text - The text to search
* @param regExp - The RegExp object to use
*/
function matchAll(text, regExp) {
// Must be global
if (!regExp.global) {
throw Error('TypeError: matchAll called with a non-global RegExp argument');
}
// Get matches
const result = [];
let match = regExp.exec(text);
while (match !== null) {
result.push(match);
match = regExp.exec(text);
}
// Reset internal index
regExp.lastIndex = 0;
return result;
}
/**
* Sort elements/nodes based on the order of their appearance in the document
*
* @param arr - The array to sort
* @param sortCallback - The callback to use to sort the elements
* @param getElement - An optional callback that returns the element to compare from each arr entry
*/
function sortElements(arr, sortCallback, getElementCallback) {
const result = [...arr];
return result.sort(function (a, b) {
if (typeof getElementCallback === 'function') {
a = getElementCallback(a);
b = getElementCallback(b);
}
return sortCallback(a, b);
});
}
/**
* Indicates if an element is either a component host element or part of a component's view/template
*
* @param element - The element to inspect
*/
function isAngularManagedElement(element) {
// Angular gives component host and view elements the following property, so can simply check for that
return element?.__ngContext__ !== undefined;
}
/**
* A text parser to load components with their bindings like in Angular templates.
*/
class TextSelectorHookParser {
constructor(config, configResolver, tagHookFinder, bindingsValueManager) {
this.configResolver = configResolver;
this.tagHookFinder = tagHookFinder;
this.bindingsValueManager = bindingsValueManager;
this.savedBindings = {};
this.config = this.configResolver.processConfig(config);
this.name = this.config.name;
}
findHooks(content, context, options) {
let hookPositions = this.config.enclosing ?
this.tagHookFinder.findEnclosingTags(content, this.config.selector, this.config.bracketStyle, options) :
this.tagHookFinder.findSingleTags(content, this.config.selector, this.config.bracketStyle, options);
if (this.config.allowSelfClosing) {
hookPositions = [
...hookPositions,
...this.tagHookFinder.findSelfClosingTags(content, this.config.selector, this.config.bracketStyle, options)
];
hookPositions.sort((a, b) => a.openingTagStartIndex - b.openingTagStartIndex);
}
return hookPositions;
}
loadComponent(hookId, hookValue, context, childNodes, options) {
return {
component: this.config.component,
hostElementTag: this.config.hostElementTag || this.config.selector, // If no hostElementTag specified, use selector (which in the case of TextSelectorHookParser is only allowed to be tag name)
injector: this.config.injector,
environmentInjector: this.config.environmentInjector
};
}
getBindings(hookId, hookValue, context, options) {
let hookBindings = this.savedBindings[hookId];
// Parse bindings once from hookValue, then reuse on subsequent runs
if (hookBindings === undefined) {
hookBindings = this.createBindings(hookValue.openingTag);
this.savedBindings[hookId] = hookBindings;
}
// (Re)evaluate if needed
this.bindingsValueManager.checkInputBindings(hookBindings.inputs, context, this.config, options);
this.bindingsValueManager.checkOutputBindings(hookBindings.outputs, this.config, options);
return {
inputs: this.getValuesFromSavedBindings(hookBindings.inputs),
outputs: this.getValuesFromSavedBindings(hookBindings.outputs)
};
}
// Bindings
// --------------------------------------------------------------------------
/**
* Returns RichBindingData for Angular-style inputs & output attrs from an openingTag
*
* @param openingTag - The openingTag to inspect
*/
createBindings(openingTag) {
const rawInputs = this.collectRawInputs(openingTag);
const inputBindings = {};
for (const [rawInputKey, rawInputValue] of Object.entries(rawInputs)) {
inputBindings[rawInputKey] = { raw: rawInputValue, parsed: false, value: null, boundContextVariables: {} };
}
const rawOutputs = this.collectRawOutputs(openingTag);
const outputBindings = {};
for (const [rawOutputKey, rawOutputValue] of Object.entries(rawOutputs)) {
outputBindings[rawOutputKey] = { raw: rawOutputValue, parsed: false, value: null, boundContextVariables: {} };
}
return {
inputs: inputBindings,
outputs: outputBindings
};
}
/**
* Collects Angular-style inputs from an openingTag
*
* @param openingTag - The openingTag to inspect
*/
collectRawInputs(openingTag) {
const rawNoBracketInputs = this.getBindingsFromOpeningTag(openingTag, 'noBracketInputs', this.config.inputsBlacklist || null, this.config.inputsWhitelist || null);
const rawBracketInputs = this.getBindingsFromOpeningTag(openingTag, 'bracketInputs', this.config.inputsBlacklist || null, this.config.inputsWhitelist || null);
// NoBracketInputs are to be interpreted as plain strings, so wrap them in quotes
for (const [noBracketInputName, noBracketInputValue] of Object.entries(rawNoBracketInputs)) {
rawNoBracketInputs[noBracketInputName] = "'" + noBracketInputValue + "'";
}
// Merge both input objects
return { ...rawNoBracketInputs, ...rawBracketInputs };
}
/**
* Collects Angular-style outputs from an openingTag
*
* @param openingTag - The openingTag to inspect
*/
collectRawOutputs(openingTag) {
return this.getBindingsFromOpeningTag(openingTag, 'outputs', this.config.outputsBlacklist || null, this.config.outputsWhitelist || null);
}
/**
* Collects Angular-style inputs or outputs from an openingTag
*
* @param type - What kind of bindings to extract
* @param openingTag - The opening tag to inspect
* @param blacklist - A list of inputs/outputs to blacklist
* @param whitelist - A list of inputs/outputs to whitelist
*/
getBindingsFromOpeningTag(openingTag, type, blacklist, whitelist) {
const bindings = {};
// Examples: https://regex101.com/r/17x3cc/16
const attributeValuesOR = '(?:' + regexes.attributeValueDoubleQuotesRegex + '|' + regexes.attributeValueSingleQuotesRegex + ')';
let attributeNameRegex;
switch (type) {
case 'noBracketInputs':
attributeNameRegex = regexes.attributeNameNoBracketsRegex;
break;
case 'bracketInputs':
attributeNameRegex = regexes.attributeNameBracketsRegex;
break;
case 'outputs':
attributeNameRegex = regexes.attributeNameRoundBracketsRegex;
break;
}
const attributeRegex = attributeNameRegex + '\=' + attributeValuesOR;
const attributePattern = new RegExp(attributeRegex, 'gim');
const attributeMatches = matchAll(openingTag, attributePattern);
// Collect raw bindings
for (const match of attributeMatches) {
// Could be either of the attribute value capturing groups
let rawBindingValue = match[2] || match[3];
// If value is empty (someInput=""), it will return undefined for it. When using noBracketInputs, return empty string instead.
if (rawBindingValue === undefined && type === 'noBracketInputs') {
rawBindingValue = '';
}
bindings[match[1]] = rawBindingValue;
}
// Filter bindings
const filteredBindings = {};
for (const [bindingName, bindingValue] of Object.entries(bindings)) {
if (blacklist && blacklist.includes(bindingName)) {
continue;
}
if (whitelist && !whitelist.includes(bindingName)) {
continue;
}
filteredBindings[bindingName] = bindingValue;
}
return filteredBindings;
}
/**
* Transforms a RichBindingData object into a normal bindings object
*
* @param richBindingsObject - The object containing the RichBindingData
*/
getValuesFromSavedBindings(richBindingsObject) {
const result = {};
for (const [key, value] of Object.entries(richBindingsObject)) {
result[key] = value.value;
}
return result;
}
}
/**
* An element parser to load components with their bindings like in Angular templates.
*/
class ElementSelectorHookParser {
constructor(config, configResolver, platformService, bindingsValueManager) {
this.configResolver = configResolver;
this.platformService = platformService;
this.bindingsValueManager = bindingsValueManager;
this.savedBindings = {};
this.config = this.configResolver.processConfig(config);
this.name = this.config.name;
}
findHookElements(contentElement, context, options) {
return Array.from(this.platformService.querySelectorAll(contentElement, this.config.selector));
}
loadComponent(hookId, hookValue, context, childNodes, options) {
// Always scrub potential []-input- and ()-output-attrs from anchor elements
this.scrubAngularBindingAttrs(hookValue.element);
return {
component: this.config.component,
hostElementTag: this.config.hostElementTag,
injector: this.config.injector,
environmentInjector: this.config.environmentInjector
};
}
getBindings(hookId, hookValue, context, options) {
let hookBindings = this.savedBindings[hookId];
// Parse bindings once from hookValue, then reuse on subsequent runs (raw values will never change as hookValue.element is a snapshot)
if (hookBindings === undefined) {
hookBindings = this.createBindings(hookValue.elementSnapshot);
this.savedBindings[hookId] = hookBindings;
}
// (Re)evaluate if needed
this.bindingsValueManager.checkInputBindings(hookBindings.inputs, context, this.config, options);
this.bindingsValueManager.checkOutputBindings(hookBindings.outputs, this.config, options);
return {
inputs: this.getValuesFromSavedBindings(hookBindings.inputs),
outputs: this.getValuesFromSavedBindings(hookBindings.outputs)
};
}
// Bindings
// --------------------------------------------------------------------------
/**
* Always removes angular-typical template attrs like []-input and ()-outputs from anchors
*
* @param anchorElement - The element to strub
*/
scrubAngularBindingAttrs(anchorElement) {
const attrsToScrub = Array.from(anchorElement.attributes)
.map((attrObj) => attrObj.name)
.filter((attr) => (attr.startsWith('[') && attr.endsWith(']')) ||
(attr.startsWith('(') && attr.endsWith(')')));
for (const attr of attrsToScrub) {
this.platformService.removeAttribute(anchorElement, attr);
}
}
/**
* Returns RichBindingData for Angular-style inputs & output attrs from an element
*
* @param element - The element to inspect
*/
createBindings(element) {
const rawInputs = this.collectRawBindings(element, 'inputs', this.config.inputsBlacklist || null, this.config.inputsWhitelist || null);
const inputBindings = {};
for (const [rawInputKey, rawInputValue] of Object.entries(rawInputs)) {
inputBindings[rawInputKey] = { raw: rawInputValue, parsed: false, value: null, boundContextVariables: {} };
}
const rawOutputs = this.collectRawBindings(element, 'outputs', this.config.outputsBlacklist || null, this.config.outputsWhitelist || null);
const outputBindings = {};
for (const [rawOutputKey, rawOutputValue] of Object.entries(rawOutputs)) {
outputBindings[rawOutputKey] = { raw: rawOutputValue, parsed: false, value: null, boundContextVariables: {} };
}
return {
inputs: inputBindings,
outputs: outputBindings
};
}
/**
* Returns Angular-style inputs or output attrs from an element
*
* @param element - The element to inspect
* @param type - Whether to return the inputs or outputs
* @param blacklist - A list of inputs/outputs to blacklist
* @param whitelist - A list of inputs/outputs to whitelist
*/
collectRawBindings(element, type, blacklist, whitelist) {
const bindings = {};
// Collect raw bindings
const attrNames = this.platformService.getAttributeNames(element);
for (let attrName of attrNames) {
if (type === 'inputs' && (!attrName.startsWith('(') || !attrName.endsWith(')')) ||
type === 'outputs' && (attrName.startsWith('(') && attrName.endsWith(')'))) {
let binding = this.platformService.getAttribute(element, attrName);
// If input has []-brackets: Transform empty attr to undefined
if (type === 'inputs' && attrName.startsWith('[') && attrName.endsWith(']') && binding === '') {
binding = undefined;
}
// If input has no []-brackets: Should be interpreted as plain strings, so wrap in quotes
if (type === 'inputs' && (!attrName.startsWith('[') || !attrName.endsWith(']'))) {
binding = `'${binding}'`;
}
// Trim [] and () brackets from attr name
attrName = attrName.replace(/^\[|^\(|\]$|\)$/g, '');
bindings[attrName] = binding;
}
}
// Filter bindings
const filteredBindings = {};
for (const [bindingName, bindingValue] of Object.entries(bindings)) {
if (blacklist && blacklist.includes(bindingName)) {
continue;
}
if (whitelist && !whitelist.includes(bindingName)) {
continue;
}
filteredBindings[bindingName] = bindingValue;
}
return filteredBindings;
}
/**
* Transforms a RichBindingData object into a normal bindings object
*
* @param richBindingsObject - The object containing the RichBindingData
*/
getValuesFromSavedBindings(richBindingsObject) {
const result = {};
for (const [key, value] of Object.entries(richBindingsObject)) {
result[key] = value.value;
}
return result;
}
}
/**
* The default values for the SelectorHookParserConfig
*/
const selectorHookParserConfigDefaults = {
component: undefined,
name: undefined,
parseWithRegex: false,
selector: undefined,
hostElementTag: undefined,
injector: undefined,
allowSelfClosing: true,
enclosing: true,
bracketStyle: { opening: '<', closing: '>' },
parseInputs: true,
unescapeStrings: true,
inputsBlacklist: undefined,
inputsWhitelist: undefined,
outputsBlacklist: undefined,
outputsWhitelist: undefined,
allowContextInBindings: true,
allowContextFunctionCalls: true
};
/**
* A helper class for resolving a SelectorHookParserConfig
*/
class SelectorHookParserConfigResolver {
constructor() {
}
/**
* Overwrites the default parser config with a (partial) SelectorHookParserConfig object and returns the result
*
* @param userParserConfig - The (partial) SelectorHookParserConfig object
*/
processConfig(userParserConfig) {
const parserConfig = JSON.parse(JSON.stringify(selectorHookParserConfigDefaults));
// component
if (!userParserConfig || !userParserConfig.hasOwnProperty('component')) {
throw Error('Missing the required "component" property for the SelectorHookParserConfig. Must be either the component class or a LazyLoadComponentConfig.');
}
parserConfig.component = userParserConfig.component;
// If is class
if (userParserConfig.component.hasOwnProperty('prototype')) {
const compMeta = reflectComponentType(userParserConfig.component);
parserConfig.selector = compMeta.selector;
// If is LazyLoadingComponentConfig
}
else if (userParserConfig.component.hasOwnProperty('importPromise') && userParserConfig.component.hasOwnProperty('importName')) {
if (!userParserConfig.hasOwnProperty('selector')) {
throw Error(`When using lazy-loaded dynamic components, you have to specify the "selector" property in the parser config, as the real selector can't be known before the component is loaded.`);
}
// If is neither
}
else {
throw Error('The "component" property in the SelectorHookParserConfig must either contain the component class or a LazyLoadComponentConfig.');
}
// name
if (userParserConfig.hasOwnProperty('name')) {
if (typeof userParserConfig.name !== 'string') {
throw Error('The submitted "name" property in the SelectorHookParserConfig must be of type string, was ' + typeof userParserConfig.name);
}
parserConfig.name = userParserConfig.name;
}
// selector (defaults to component selector)
if (userParserConfig.hasOwnProperty('selector')) {
if (typeof userParserConfig.selector !== 'string') {
throw Error('The submitted "selector" property in the SelectorHookParserConfig must be of type string, was ' + typeof userParserConfig.selector);
}
parserConfig.selector = userParserConfig.selector;
}
// hostElementTag
if (userParserConfig.hasOwnProperty('hostElementTag')) {
if (typeof userParserConfig.hostElementTag !== 'string') {
throw Error('The submitted "hostElementTag" property in the SelectorHookParserConfig must be of type string, was ' + typeof userParserConfig.hostElementTag);
}
parserConfig.hostElementTag = userParserConfig.hostElementTag;
}
// parseWithRegex
if (userParserConfig.hasOwnProperty('parseWithRegex')) {
if (typeof userParserConfig.parseWithRegex !== 'boolean') {
throw Error('The submitted "parseWithRegex" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.parseWithRegex);
}
parserConfig.parseWithRegex = userParserConfig.parseWithRegex;
}
// allowSelfClosing
if (userParserConfig.hasOwnProperty('allowSelfClosing')) {
if (typeof userParserConfig.allowSelfClosing !== 'boolean') {
throw Error('The submitted "allowSelfClosing" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.allowSelfClosing);
}
parserConfig.allowSelfClosing = userParserConfig.allowSelfClosing;
}
// enclosing
if (userParserConfig.hasOwnProperty('enclosing')) {
if (typeof userParserConfig.enclosing !== 'boolean') {
throw Error('The submitted "enclosing" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.enclosing);
}
parserConfig.enclosing = userParserConfig.enclosing;
}
// bracketStyle
if (userParserConfig.hasOwnProperty('bracketStyle')) {
if (typeof userParserConfig.bracketStyle !== 'object' || typeof userParserConfig.bracketStyle.opening !== 'string' || typeof userParserConfig.bracketStyle.closing !== 'string') {
throw Error('The submitted "bracketStyle" property in the SelectorHookParserConfig must have the form {opening: string, closing: string}');
}
parserConfig.bracketStyle = userParserConfig.bracketStyle;
}
// injector (defaults to undefined)
if (userParserConfig.hasOwnProperty('injector')) {
parserConfig.injector = userParserConfig.injector;
}
// environmentInjector (defaults to undefined)
if (userParserConfig.hasOwnProperty('environmentInjector')) {
parserConfig.environmentInjector = userParserConfig.environmentInjector;
}
// unescapeStrings
if (userParserConfig.hasOwnProperty('unescapeStrings')) {
if (typeof userParserConfig.unescapeStrings !== 'boolean') {
throw Error('The submitted "unescapeStrings" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.unescapeStrings);
}
parserConfig.unescapeStrings = userParserConfig.unescapeStrings;
}
// parseInputs
if (userParserConfig.hasOwnProperty('parseInputs')) {
if (typeof userParserConfig.parseInputs !== 'boolean') {
throw Error('The submitted "parseInputs" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.parseInputs);
}
parserConfig.parseInputs = userParserConfig.parseInputs;
}
// inputsBlacklist
if (userParserConfig.hasOwnProperty('inputsBlacklist')) {
if (!Array.isArray(userParserConfig.inputsBlacklist)) {
throw Error('The submitted "inputsBlacklist" property in the SelectorHookParserConfig must be an array of strings.');
}
for (const entry of userParserConfig.inputsBlacklist) {
if (typeof entry !== 'string') {
throw Error('All entries of the submitted "inputsBlacklist" property in the SelectorHookParserConfig must be of type string, ' + typeof entry + ' found.');
}
}
parserConfig.inputsBlacklist = userParserConfig.inputsBlacklist;
}
// inputsWhitelist
if (userParserConfig.hasOwnProperty('inputsWhitelist')) {
if (!Array.isArray(userParserConfig.inputsWhitelist)) {
throw Error('The submitted "inputsWhitelist" property in the SelectorHookParserConfig must be an array of strings.');
}
for (const entry of userParserConfig.inputsWhitelist) {
if (typeof entry !== 'string') {
throw Error('All entries of the submitted "inputsWhitelist" property in the SelectorHookParserConfig must be of type string, ' + typeof entry + ' found.');
}
}
parserConfig.inputsWhitelist = userParserConfig.inputsWhitelist;
}
// outputsBlacklist
if (userParserConfig.hasOwnProperty('outputsBlacklist')) {
if (!Array.isArray(userParserConfig.outputsBlacklist)) {
throw Error('The submitted "outputsBlacklist" property in the SelectorHookParserConfig must be an array of strings.');
}
for (const entry of userParserConfig.outputsBlacklist) {
if (typeof entry !== 'string') {
throw Error('All entries of the submitted "outputsBlacklist" property in the SelectorHookParserConfig must be of type string, ' + typeof entry + ' found.');
}
}
parserConfig.outputsBlacklist = userParserConfig.outputsBlacklist;
}
// outputsWhitelist
if (userParserConfig.hasOwnProperty('outputsWhitelist')) {
if (!Array.isArray(userParserConfig.outputsWhitelist)) {
throw Error('The submitted "outputsWhitelist" property in the SelectorHookParserConfig must be an array of strings.');
}
for (const entry of userParserConfig.outputsWhitelist) {
if (typeof entry !== 'string') {
throw Error('All entries of the submitted "outputsWhitelist" property in the SelectorHookParserConfig must be of type string, ' + typeof entry + ' found.');
}
}
parserConfig.outputsWhitelist = userParserConfig.outputsWhitelist;
}
// allowContextInBindings
if (userParserConfig.hasOwnProperty('allowContextInBindings')) {
if (typeof userParserConfig.allowContextInBindings !== 'boolean') {
throw Error('The submitted "allowContextInBindings" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.allowContextInBindings);
}
parserConfig.allowContextInBindings = userParserConfig.allowContextInBindings;
}
// allowContextFunctionCalls
if (userParserConfig.hasOwnProperty('allowContextFunctionCalls')) {
if (typeof userParserConfig.allowContextFunctionCalls !== 'boolean') {
throw Error('The submitted "allowContextFunctionCalls" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.allowContextFunctionCalls);
}
parserConfig.allowContextFunctionCalls = userParserConfig.allowContextFunctionCalls;
}
return parserConfig;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelectorHookParserConfigResolver, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelectorHookParserConfigResolver, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelectorHookParserConfigResolver, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
const PLATFORM_SERVICE = new InjectionToken('An injection token to retrieve an optionally user-provided PlatformService');
/**
* General implementation of PlatformService suited for both the standard browser and server environments
*/
class DefaultPlatformService {
constructor(document, rendererFactory, sanitizer) {
this.document = document;
this.rendererFactory = rendererFactory;
this.sanitizer = sanitizer;
this.renderer = this.rendererFactory.createRenderer(null, null);
}
getNgVersion() {
if (typeof this.document !== "undefined") {
const versionElement = this.querySelectorAll(this.document, '[ng-version]')?.[0];
const versionAttr = versionElement?.getAttribute('ng-version');
if (versionAttr) {
return parseInt(versionAttr, 10);
}
}
return null;
}
sanitize(content) {
return this.sanitizer.sanitize(SecurityContext.HTML, content) || '';
}
createElement(tagName) {
return this.renderer.createElement(tagName);
}
sortElements(a, b) {
if (a === b)
return 0;
if (!a.compareDocumentPosition) {
// support for IE8 and below
return a.sourceIndex - b.sourceIndex;
}
if (a.compareDocumentPosition(b) & 2) {
// b comes before a
return 1;
}
return -1;
}
cloneElement(element) {
return element.cloneNode(true);
}
getTagName(element) {
return element.tagName;
}
getOpeningTag(element) {
// Approach by: https://stackoverflow.com/a/55859966/3099523
const innerLength = element.innerHTML.length;
const outerLength = element.outerHTML.length;
// Check for self-closing elements
const openingTagLength = element.outerHTML[outerLength - 2] === '/' ?
outerLength :
outerLength - innerLength - element.tagName.length - 3;
return element.outerHTML.slice(0, openingTagLength);
}
getClosingTag(element) {
return element.outerHTML.slice(element.outerHTML.length - element.tagName.length - 3);
}
getAttributeNames(element) {
return typeof element.getAttributeNames === 'function' ? element.getAttributeNames() : [];
}
getAttribute(element, attributeName) {
return typeof element.getAttribute === 'function' ? element.getAttribute(attributeName) : null;
}
setAttribute(element, attributeName, value) {
this.renderer.setAttribute(element, attributeName, value);
}
removeAttribute(element, attributeName) {
this.renderer.removeAttribute(element, attributeName);
}
getParentNode(element) {
try {
return this.renderer.parentNode(element);
}
catch (e) {
return null;
}
}
querySelectorAll(parentElement, selector) {
return Array.from(parentElement.querySelectorAll(selector));
}
getChildNodes(node) {
return Array.prototype.slice.call(node.childNodes);
}
appendChild(parentElement, childElement) {
this.renderer.appendChild(parentElement, childElement);
}
insertBefore(parentElement, childElement, referenceElement) {
this.renderer.insertBefore(parentElement, childElement, referenceElement);
}
clearChildNodes(element) {
if (element) {
while (element.firstChild) {
this.removeChild(element, element.firstChild);
}
}
}
removeChild(parentElement, childElement) {
parentElement.removeChild(childElement);
}
getInnerContent(element) {
return element.innerHTML;
}
setInnerContent(element, content) {
if (element) {
element.innerHTML = content;
}
}
isTextNode(element) {
return element.nodeType === Node.TEXT_NODE;
}
createTextNode(content) {
return document.createTextNode(content);
}
getTextContent(element) {
return element.textContent;
}
dispatchEvent(element, name, payload) {
element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DefaultPlatformService, deps: [{ token: DOCUMENT }, { token: i0.RendererFactory2 }, { token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DefaultPlatformService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DefaultPlatformService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: Document, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: i0.RendererFactory2 }, { type: i1.DomSanitizer }] });
/**
* Wrapper class that either calls user-provided PlatformService methods or falls back to default implementations
*/
class AutoPlatformService {
constructor(userPlatformService, defaultPlatformService) {
this.userPlatformService = userPlatformService;
this.defaultPlatformService = defaultPlatformService;
}
getFor(methodName) {
if (this.userPlatformService && typeof this.userPlatformService[methodName] === 'function') {
return this.userPlatformService;
}
else {
return this.defaultPlatformService;
}
}
getNgVersion() {
return this.getFor('getNgVersion').getNgVersion();
}
sanitize(content) {
return this.getFor('sanitize').sanitize(content);
}
createElement(tagName) {
return this.getFor('createElement').createElement(tagName);
}
sortElements(a, b) {
return this.getFor('sortElements').sortElements(a, b);
}
cloneElement(element) {
return this.getFor('cloneElement').cloneElement(element);
}
getTagName(element) {
return this.getFor('getTagName').getTagName(element);
}
getOpeningTag(element) {
return this.getFor('getOpeningTag').getOpeningTag(element);
}
getClosingTag(element) {
return this.getFor('getClosingTag').getClosingTag(element);
}
getAttributeNames(element) {
return this.getFor('getAttributeNames').getAttributeNames(element);
}
getAttribute(element, attributeName) {
return this.getFor('getAttribute').getAttribute(element, attributeName);
}
setAttribute(element, attributeName, value) {
return this.getFor('setAttribute').setAttribute(element, attributeName, value);
}
removeAttribute(element, attributeName) {
return this.getFor('removeAttribute').removeAttribute(element, attributeName);
}
getParentNode(element) {
return this.getFor('getParentNode').getParentNode(element);
}
querySelectorAll(parentElement, selector) {
return this.getFor('querySelectorAll').querySelectorAll(parentElement, selector);
}
getChildNodes(node) {
return this.getFor('getChildNodes').getChildNodes(node);
}
appendChild(parentElement, childElement) {
return this.getFor('appendChild').appendChild(parentElement, childElement);
}
insertBefore(parentElement, childElement, referenceElement) {
return this.getFor('insertBefore').insertBefore(parentElement, childElement, referenceElement);
}
clearChildNodes(element) {
return this.getFor('clearChildNodes').clearChildNodes(element);
}
removeChild(parentElement, childElement) {
return this.getFor('removeChild').removeChild(parentElement, childElement);
}
getInnerContent(element) {
return this.getFor('getInnerContent').getInnerContent(element);
}
setInnerContent(element, content) {
return this.getFor('setInnerContent').setInnerContent(element, content);
}
isTextNode(element) {
return this.getFor('isTextNode').isTextNode(element);
}
createTextNode(content) {
return this.getFor('createTextNode').createTextNode(content);
}
getTextContent(element) {
return this.getFor('getTextContent').getTextContent(element);
}
dispatchEvent(element, name, payload) {
return this.getFor('dispatchEvent').dispatchEvent(element, name, payload);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AutoPlatformService, deps: [{ token: PLATFORM_SERVICE, optional: true }, { token: DefaultPlatformService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AutoPlatformService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AutoPlatformService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [PLATFORM_SERVICE]
}] }, { type: DefaultPlatformService }] });
/**
* A utility service to print logs and warnings
*/
class Logger {
constructor(platformId) {
this.platformId = platformId;
}
log(content, options) {
this.handleLog(content, options, 'log');
}
warn(content, options) {
this.handleLog(content, options, 'warn');
}
error(content, options) {
this.handleLog(content, options, 'error');
}
/**
* Logs an array of content according to the submitted options
*
* @param content - The content to log
* @param options - The current ParseOptions
* @param method - The console method to use
*/
handleLog(content, options, method) {
if (options.logOptions?.dev && this.isDevMode() && isPlatformBrowser(this.platformId) ||
options.logOptions?.prod && !this.isDevMode() && isPlatformBrowser(this.platformId) ||
options.logOptions?.ssr && !isPlatformBrowser(this.platformId)) {
console[method](...content);
}
}
/**
* Use local method that is easier to mock in tests
*/
isDevMode() {
return isDevMode();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: Logger, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: Logger, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: Logger, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }] });
/**
* A utility service to easily parse hooks from text content
*/
class HookFinder {
constructor(logger) {
this.logger = logger;
}
/**
* Finds all text hooks in a piece of content, e.g. <hook>...</hook>, and returns their positions
*
* @param content - The text to parse
* @param openingTagRegex - The regex for the opening tag
* @param closingTagRegex - The regex for the closing tag
* @param includeNested - Whether to include nested hooks in the result
* @param options - The current ParseOptions
*/
find(content, openingTagRegex, closingTagRegex, includeNested, options = getParseOptionDefaults()) {
if (!closingTagRegex) {
return this.findSingletagHooks(content, openingTagRegex);
}
else {
return this.findEnclosingHooks(content, openingTagRegex, closingTagRegex, includeNested, options);
}
}
/**
* Finds all text hooks that are non-enclosing in a piece of text, e.g. <hook>
*
* @param content - The text to search
* @param hookRegex - The regex to use for the hook
*/
findSingletagHooks(content, hookRegex) {
const result = [];
// Find all hooks
const openingTagMatches = matchAll(content, hookRegex);
for (const match of openingTagMatches) {
result.push({
openingTagStartIndex: match.index,
openingTagEndIndex: match.index + match[0].length,
closingTagStartIndex: null,
closingTagEndIndex: null,
});
}
return result;
}
/**
* Finds all text hooks that are enclosing in a piece of text, e.g. <hook>...</hook>
*
* Correctly finding enclosing hooks requires a programmatic parser rather then just regex alone, as regex cannot handle
* patterns that are potentially nested within themselves.
*
* - If the content between the opening and closing is lazy (.*?), it would take the first closing tag after the opening tag,
* regardless if it belongs to the opening tag or actually a nested hook. This would falsely match the first and third tag
* in this example: '<hook><hook></hook></hook>'
*
* - If the content between the opening and closing is greedy (.*), it would only end on the last closing tag in the string,
* ignoring any previous closing tags. This would falsely match the first and fourth tag in this example:
* '<hook></hook><hook></hook>'
*
* There is no regex that works for both scenarios. This method therefore manually counts and compares the opening tags with the closing tags.
*
* @param content - The text to parse
* @param openingTagRegex - The regex for the opening tag
* @param closingTagRegex - The regex for the closing tag
* @param includeNested - Whether to include nested hooks in the result
* @param options - The current parseOptions
*/
findEnclosingHooks(content, openingTagRegex, closingTagRegex, includeNested, options = getParseOptionDefaults()) {
const allTags = [];
const result = [];
// Find all opening tags
const openingTagMatches = matchAll(content, openingTagRegex);
for (const match of openingTagMatches) {
allTags.push({
isOpening: true,
value: match[0],
startIndex: match.index,
endIndex: match.index + match[0].length
});
}
// Find all closing tags
const closingTagMatches = matchAll(content, closingTagRegex);
for (const match of closingTagMatches) {
allTags.push({
isOpening: false,
value: match[0],
startIndex: match.index,
endIndex: match.index + match[0].length
});
}
// Sort by startIndex
allTags.sort((a, b) => a.startIndex - b.startIndex);
// Create HookPositions by figuring out which opening tag belongs to which closing tag
const openedTags = [];
allTagsLoop: for (const [index, tag] of allTags.entries()) {
// Any subsequent tag is only allowed to start after previous tag has ended
if (index > 0 && tag.startIndex < allTags[index - 1].endIndex) {
this.logger.warn(['Syntax error - New tag "' + tag.value + '" started at position ' + tag.startIndex + ' before previous tag "' + allTags[index - 1].value + '" ended at position ' + allTags[index - 1].endIndex + '. Ignoring.'], options);
continue;
}
// Opening or closing tag?
if (tag.isOpening) {
openedTags.push(tag);
}
else {
// Syntax error: Closing tag without preceding opening tag. Syntax error.
if (openedTags.length === 0) {
this.logger.warn(['Syntax error - Closing tag without preceding opening tag found: "' + tag.value + '". Ignoring.'], options);
continue;
}
// If nested hooks not allowed and more than one tag is open, discard both this closing tag and the latest opening tag
if (includeNested === false && openedTags.length > 1) {
openedTags.pop();
continue;
}
// Valid hook! Add to result array
const openingTag = openedTags[openedTags.length - 1];
result.push({
openingTagStartIndex: openingTag.startIndex,
openingTagEndIndex: openingTag.startIndex + openingTag.value.length,
closingTagStartIndex: tag.startIndex,
closingTagEndIndex: tag.startIndex + tag.value.length
});
openedTags.pop();
}
}
if (openedTags.length > 0) {
this.logger.warn(['Syntax error - Opening tags without corresponding closing tags found.'], options);
}
return result;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: HookFinder, deps: [{ token: Logger }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: HookFinder, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: HookFinder, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: Logger }] });
/**
* A utility service for the TextSelectorHookParser that finds Angular component selectors in the content
*/
class TagHookFinder {
constructor(hookFinder) {
this.hookFinder = hookFinder;
}
/**
* Finds singletag Angular component selectors
*
* @param content - The content to parse
* @param selector - The Angular selector to find
* @param bracketStyle - What bracket style to use
* @param options - The current ParseOptions
*/
findSingleTags(content, selector, br