`-tag starting before the hook and the corresponding `
`-tag ending inside, and vice versa). This will result in faulty HTML when rendered in a browser. This setting removes these ripped-apart tags. 17 | */ 18 | fixParagraphTags?: boolean; 19 | 20 | /** 21 | * Whether to update the bindings of dynamic components only when the context object passed to the `DynamicHooksComponent` changes by reference. 22 | */ 23 | updateOnPushOnly?: boolean; 24 | 25 | /** 26 | * Whether to deeply-compare inputs for dynamic components by their value instead of by their reference on updates. 27 | */ 28 | compareInputsByValue?: boolean; 29 | 30 | /** 31 | * Whether to deeply-compare outputs for dynamic components by their value instead of by their reference on updates. 32 | */ 33 | compareOutputsByValue?: boolean; 34 | 35 | /** 36 | * When comparing by value, how many levels deep to compare them (may impact performance). 37 | */ 38 | compareByValueDepth?: number; 39 | 40 | /** 41 | * Whether to emit CustomEvents from the component host elements when an output emits. The event name will be the output name. Defaults to true in standalone mode, otherwise false. 42 | */ 43 | triggerDOMEvents?: boolean; 44 | 45 | /** 46 | * Whether to ignore input aliases like `@Input('someAlias')` in dynamic components and use the actual property names instead. 47 | */ 48 | ignoreInputAliases?: boolean; 49 | 50 | /** 51 | * Whether to ignore output aliases like `@Output('someAlias')` in dynamic components and use the actual property names instead. 52 | */ 53 | ignoreOutputAliases?: boolean; 54 | 55 | /** 56 | * Whether to disregard `@Input()`-decorators completely and allow passing in values to any property in dynamic components. 57 | */ 58 | acceptInputsForAnyProperty?: boolean; 59 | 60 | /** 61 | * Whether to disregard `@Output()`-decorators completely and allow subscribing to any `Observable` in dynamic components. 62 | */ 63 | acceptOutputsForAnyObservable?: boolean; 64 | 65 | /** 66 | * Accepts a `LogOptions` object to customize when to log text, warnings and errors. 67 | */ 68 | logOptions?: LogOptions; 69 | } 70 | 71 | export interface LogOptions { 72 | 73 | /** 74 | * Whether to enable logging when in dev mode 75 | */ 76 | dev?: boolean; 77 | 78 | /** 79 | * Whether to enable logging when in prod mode 80 | */ 81 | prod?: boolean; 82 | 83 | /** 84 | * Whether to enable logging during Server-Side-Rendering 85 | */ 86 | ssr?: boolean; 87 | } 88 | 89 | /** 90 | * Returns the default values for the ParseOptions 91 | */ 92 | export const getParseOptionDefaults: () => ParseOptions = () => { 93 | return { 94 | sanitize: true, 95 | convertHTMLEntities: true, 96 | fixParagraphTags: true, 97 | updateOnPushOnly: false, 98 | compareInputsByValue: false, 99 | compareOutputsByValue: false, 100 | compareByValueDepth: 5, 101 | triggerDOMEvents: false, 102 | ignoreInputAliases: false, 103 | ignoreOutputAliases: false, 104 | acceptInputsForAnyProperty: false, 105 | acceptOutputsForAnyObservable: false, 106 | logOptions: { 107 | dev: true, 108 | prod: false, 109 | ssr: false 110 | } 111 | }; 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/lib/services/settings/parserEntry.ts: -------------------------------------------------------------------------------- 1 | import { HookParser } from '../../interfacesPublic'; 2 | import { SelectorHookParserConfig } from '../../parsers/selector/selectorHookParserConfig'; 3 | 4 | /** 5 | * An configuration entry for a HookParser. This can either be: 6 | * 7 | * 1. The component class itself. 8 | * 2. A SelectorHookParserConfig object literal. 9 | * 3. A custom HookParser instance. 10 | * 4. A custom HookParser class. If this class is available as a provider/service, it will be injected. 11 | */ 12 | export type HookParserEntry = (new(...args: any[]) => any) | SelectorHookParserConfig | HookParser; 13 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/lib/services/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { HookParserEntry } from './parserEntry'; 2 | import { HookParser } from '../../interfacesPublic'; 3 | import { ParseOptions } from './options'; 4 | 5 | export enum DynamicHooksInheritance { 6 | /** 7 | * Merges with settings from all injectors in the app. 8 | */ 9 | All, 10 | 11 | /** 12 | * (Default) Only merges with settings from direct ancestor injectors (such a father and grandfather injectors, but not "uncle" injectors). 13 | */ 14 | Linear, 15 | 16 | /** 17 | * Does not merge at all. Injector only uses own settings. 18 | */ 19 | None 20 | } 21 | 22 | /** 23 | * The interface for users to define the global options 24 | */ 25 | export interface DynamicHooksSettings { 26 | 27 | /** 28 | * A list of parsers to use globally 29 | */ 30 | parsers?: HookParserEntry[]; 31 | 32 | /** 33 | * Options to use globally 34 | */ 35 | options?: ParseOptions; 36 | 37 | /** 38 | * Used for providing child settings in child injector contexts 39 | */ 40 | inheritance?: DynamicHooksInheritance; 41 | } -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/lib/services/settings/settingsResolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Injector, Optional } from '@angular/core'; 2 | import { DynamicHooksSettings, DynamicHooksInheritance } from './settings'; 3 | import { ParserEntryResolver } from './parserEntryResolver'; 4 | import { HookParserEntry } from './parserEntry'; 5 | import { HookParser } from '../../interfacesPublic'; 6 | import { ParseOptions, getParseOptionDefaults } from './options'; 7 | 8 | /** 9 | * A helper class for resolving a combined settings object from all provided ones 10 | */ 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class SettingsResolver { 15 | 16 | constructor( 17 | private parserEntryResolver: ParserEntryResolver 18 | ) { 19 | } 20 | 21 | /** 22 | * Takes all provided settings objects and combines them into a final settings object 23 | * 24 | * @param injector - The current injector 25 | * @param content - The content 26 | * @param allSettings - All settings provided anywhere 27 | * @param ancestorSettings - All ancestor settings 28 | * @param moduleSettings - The current module settings 29 | * @param localParsers - A list of local parsers 30 | * @param localOptions - A local options object 31 | * @param globalParsersBlacklist - A list of global parsers to blacklist 32 | * @param globalParsersWhitelist - A list of global parsers to whitelist 33 | */ 34 | public resolve( 35 | injector: Injector, 36 | content: any, 37 | allSettings: DynamicHooksSettings[]|null, 38 | ancestorSettings: DynamicHooksSettings[]|null, 39 | moduleSettings: DynamicHooksSettings|null, 40 | localParsers: HookParserEntry[]|null = null, 41 | localOptions: ParseOptions|null = null, 42 | globalParsersBlacklist: string[]|null = null, 43 | globalParsersWhitelist: string[]|null = null, 44 | ): { 45 | parsers: HookParser[]; 46 | options: ParseOptions; 47 | } { 48 | let resolvedSettings: DynamicHooksSettings = {}; 49 | allSettings = allSettings || []; 50 | ancestorSettings = ancestorSettings || []; 51 | moduleSettings = moduleSettings || {}; 52 | const defaultSettings: DynamicHooksSettings = { options: getParseOptionDefaults() }; 53 | 54 | // Merge settings according to inheritance 55 | if (!moduleSettings.hasOwnProperty('inheritance') || moduleSettings.inheritance === DynamicHooksInheritance.Linear) { 56 | resolvedSettings = this.mergeSettings([ 57 | defaultSettings, 58 | ...ancestorSettings, 59 | {parsers: localParsers || undefined, options: localOptions || undefined} 60 | ]); 61 | 62 | } else if (moduleSettings.inheritance === DynamicHooksInheritance.All) { 63 | // Additionally merge ancestorSettings after allSettings to give settings closer to the current injector priority 64 | resolvedSettings = this.mergeSettings([ 65 | defaultSettings, 66 | ...allSettings, 67 | ...ancestorSettings, 68 | {options: localOptions || undefined} 69 | ]); 70 | 71 | } else { 72 | resolvedSettings = this.mergeSettings([ 73 | defaultSettings, 74 | moduleSettings || {}, 75 | {options: localOptions || undefined} 76 | ]) 77 | } 78 | 79 | const finalOptions = resolvedSettings.options!; 80 | 81 | // Disabled sanitization if content is not string 82 | if (content && typeof content !== 'string') { 83 | finalOptions.sanitize = false; 84 | } 85 | 86 | // Process parsers entries. Local parsers fully replace global ones. 87 | let finalParsers: HookParser[] = []; 88 | if (localParsers) { 89 | finalParsers = this.parserEntryResolver.resolve(localParsers, injector, null, null, finalOptions); 90 | } else if (resolvedSettings.parsers) { 91 | finalParsers = this.parserEntryResolver.resolve(resolvedSettings.parsers, injector, globalParsersBlacklist, globalParsersWhitelist, finalOptions); 92 | } 93 | 94 | return { 95 | parsers: finalParsers, 96 | options: finalOptions 97 | }; 98 | } 99 | 100 | /** 101 | * Merges multiple settings objects, overwriting previous ones with later ones in the provided array 102 | * 103 | * @param settingsArray - The settings objects to merge 104 | */ 105 | private mergeSettings(settingsArray: DynamicHooksSettings[]): DynamicHooksSettings { 106 | const mergedSettings: DynamicHooksSettings = {}; 107 | 108 | for (const settings of settingsArray) { 109 | // Unique parsers are simply all collected, not overwritten 110 | if (settings.parsers !== undefined) { 111 | if (mergedSettings.parsers === undefined) { 112 | mergedSettings.parsers = []; 113 | } 114 | for (const parserEntry of settings.parsers) { 115 | if (!mergedSettings.parsers.includes(parserEntry)) { 116 | mergedSettings.parsers.push(parserEntry); 117 | } 118 | } 119 | } 120 | // Options are individually overwritten 121 | if (settings.options !== undefined) { 122 | if (mergedSettings.options === undefined) { 123 | mergedSettings.options = {}; 124 | } 125 | 126 | mergedSettings.options = this.recursiveAssign(mergedSettings.options, settings.options); 127 | } 128 | } 129 | 130 | return mergedSettings; 131 | } 132 | 133 | /** 134 | * Recursively merges two objects 135 | * 136 | * @param a - The target object to merge into 137 | * @param b - The other object being merged 138 | */ 139 | private recursiveAssign (a: any, b: any) { 140 | if (Object(b) !== b) return b; 141 | if (Object(a) !== a) a = {}; 142 | for (const key in b) { 143 | a[key] = this.recursiveAssign(a[key], b[key]); 144 | } 145 | return a; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/lib/services/utils/contentSanitizer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HookIndex } from '../../interfacesPublic'; 3 | import { AutoPlatformService } from '../platform/autoPlatformService'; 4 | import { anchorAttrHookId, anchorAttrParseToken } from '../../constants/core'; 5 | import { matchAll } from './utils'; 6 | 7 | const sanitizerPlaceholderTag = 'dynamic-hooks-sanitization-placeholder'; 8 | const sanitizerPlaceholderRegex = new RegExp(`<\/?${sanitizerPlaceholderTag}.*?>`, 'g'); 9 | 10 | /** 11 | * A utility service that sanitizes an Element and all of its children while exluding found hook elements 12 | */ 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class ContentSanitizer { 17 | 18 | attrWhitelist = [anchorAttrHookId, anchorAttrParseToken, 'class', 'href', 'src'] 19 | 20 | constructor(private platformService: AutoPlatformService) {} 21 | 22 | /** 23 | * Sanitizes an element while preserving marked hook anchors 24 | * 25 | * @param contentElement - The element to sanitize 26 | * @param hookIndex - The current hookIndex 27 | * @param token - The current ParseToken 28 | */ 29 | sanitize(contentElement: any, hookIndex: HookIndex, token: string): any { 30 | const originalHookAnchors: {[key: string]: any} = {}; 31 | 32 | // Replace all hook anchors with custom placeholder elements 33 | // This is so the browser has no predefined rules where they can and can't exist in the dom hierarchy and doesn't edit the html. 34 | for (const hook of Object.values(hookIndex)) { 35 | const anchorElement = this.platformService.querySelectorAll(contentElement, `[${anchorAttrHookId}="${hook.id}"][${anchorAttrParseToken}="${token}"]`)?.[0]; 36 | if (anchorElement) { 37 | originalHookAnchors[hook.id] = anchorElement; 38 | 39 | const parentElement = this.platformService.getParentNode(anchorElement); 40 | const childNodes = this.platformService.getChildNodes(anchorElement); 41 | 42 | const placeholderElement = this.platformService.createElement(sanitizerPlaceholderTag); 43 | this.platformService.setAttribute(placeholderElement, anchorAttrHookId, hook.id.toString()); 44 | this.platformService.setAttribute(placeholderElement, anchorAttrParseToken, token); 45 | this.platformService.insertBefore(parentElement, placeholderElement, anchorElement); 46 | this.platformService.removeChild(parentElement, anchorElement); 47 | for (const node of childNodes) { 48 | this.platformService.appendChild(placeholderElement, node); 49 | } 50 | } 51 | } 52 | 53 | // Encode sanitization placeholders (so they survive sanitization) 54 | let innerHTML = this.platformService.getInnerContent(contentElement); 55 | innerHTML = this.findAndEncodeTags(innerHTML, sanitizerPlaceholderRegex); 56 | 57 | // Sanitize (without warnings) 58 | const consoleWarnFn = console.warn; 59 | console.warn = () => {}; 60 | let sanitizedInnerHtml = this.platformService.sanitize(innerHTML); 61 | console.warn = consoleWarnFn; 62 | 63 | // Decode sanitization placeholders 64 | sanitizedInnerHtml = this.decodeTagString(sanitizedInnerHtml); 65 | contentElement.innerHTML = sanitizedInnerHtml || ''; 66 | 67 | // Restore original hook anchors 68 | for (const [hookId, anchorElement] of Object.entries(originalHookAnchors)) { 69 | const placeholderElement = this.platformService.querySelectorAll(contentElement, `${sanitizerPlaceholderTag}[${anchorAttrHookId}="${hookId}"]`)?.[0]; 70 | if (placeholderElement) { 71 | const parentElement = this.platformService.getParentNode(placeholderElement); 72 | const childNodes = this.platformService.getChildNodes(placeholderElement); 73 | this.platformService.insertBefore(parentElement, anchorElement, placeholderElement); 74 | this.platformService.removeChild(parentElement, placeholderElement); 75 | for (const node of childNodes) { 76 | this.platformService.appendChild(anchorElement, node); 77 | } 78 | 79 | // As a last step, sanitize the hook anchor attrs as well 80 | this.sanitizeElementAttrs(anchorElement); 81 | } 82 | } 83 | 84 | return contentElement; 85 | } 86 | 87 | /** 88 | * Sanitizes a single element's attributes 89 | * 90 | * @param element - The element in question 91 | */ 92 | private sanitizeElementAttrs(element: any): any { 93 | // Collect all existing attributes, put them on span-element, sanitize it, then copy surviving attrs back onto hook anchor element 94 | const attrs = this.platformService.getAttributeNames(element); 95 | const tmpWrapperElement = this.platformService.createElement('div'); 96 | const tmpElement = this.platformService.createElement('span'); 97 | this.platformService.appendChild(tmpWrapperElement, tmpElement); 98 | 99 | // Move attr to tmp 100 | for (const attr of attrs) { 101 | try { 102 | this.platformService.setAttribute(tmpElement, attr, this.platformService.getAttribute(element, attr)!); 103 | } catch (e) {} 104 | // Keep in separate try-catch, so the first doesn't stop the second 105 | try { 106 | // Always keep those two 107 | if (attr !== anchorAttrHookId && attr !== anchorAttrParseToken) { 108 | this.platformService.removeAttribute(element, attr); 109 | } 110 | } catch (e) {} 111 | } 112 | 113 | // Sanitize tmp 114 | tmpWrapperElement.innerHTML = this.platformService.sanitize(this.platformService.getInnerContent(tmpWrapperElement)); 115 | 116 | // Move surviving attrs back to element 117 | const sanitizedTmpElement = this.platformService.querySelectorAll(tmpWrapperElement, 'span')[0]; 118 | const survivingAttrs = this.platformService.getAttributeNames(sanitizedTmpElement); 119 | for (const survivingAttr of survivingAttrs) { 120 | try { 121 | this.platformService.setAttribute(element, survivingAttr, this.platformService.getAttribute(sanitizedTmpElement, survivingAttr)!); 122 | } catch (e) {} 123 | } 124 | 125 | return element; 126 | } 127 | 128 | // En/decoding placeholders 129 | // ------------------------ 130 | 131 | /** 132 | * Finds and encodes all tags that match the specified regex so that they survive sanitization 133 | * 134 | * @param content - The stringified html content to search 135 | * @param substrRegex - The regex that matches the element tags 136 | */ 137 | private findAndEncodeTags(content: string, substrRegex: RegExp): string { 138 | let encodedContent = content; 139 | 140 | const matches = matchAll(content, substrRegex); 141 | matches.sort((a, b) => b.index - a.index); 142 | 143 | for (const match of matches) { 144 | const startIndex = match.index; 145 | const endIndex = match.index + match[0].length; 146 | 147 | const textBeforeSelector = encodedContent.substring(0, startIndex); 148 | const encodedPlaceholder = this.encodeTagString(encodedContent.substring(startIndex, endIndex)); 149 | const textAfterSelector = encodedContent.substring(endIndex); 150 | encodedContent = textBeforeSelector + encodedPlaceholder + textAfterSelector; 151 | } 152 | 153 | return encodedContent; 154 | } 155 | 156 | /** 157 | * Encodes the special html chars in a html tag so that is is considered a harmless string 158 | * 159 | * @param element - The element as a string 160 | */ 161 | private encodeTagString(element: string): string { 162 | element = element.replace(//g, '@@@hook-gt@@@'); 164 | element = element.replace(/"/g, '@@@hook-dq@@@'); 165 | return element; 166 | } 167 | 168 | /** 169 | * Decodes the encoded html chars in a html tag again 170 | * 171 | * @param element - The element as a string 172 | */ 173 | private decodeTagString(element: string): string { 174 | element = element.replace(/@@@hook-lt@@@/g, '<'); 175 | element = element.replace(/@@@hook-gt@@@/g, '>'); 176 | element = element.replace(/@@@hook-dq@@@/g, '"'); 177 | return element; 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/lib/services/utils/deepComparer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Logger } from './logger'; 3 | import { getParseOptionDefaults, ParseOptions } from '../settings/options'; 4 | 5 | 6 | /** 7 | * The object returned by the detailedStringify function in DeepComparer. 8 | * Contains the stringified value as well as the number of times the maximum stringify depth was reached. 9 | */ 10 | export interface DetailedStringifyResult { 11 | result: string|null; 12 | depthReachedCount: number; 13 | } 14 | 15 | /** 16 | * A service for comparing two variables by value instead of by reference 17 | */ 18 | @Injectable({ 19 | providedIn: 'root' 20 | }) 21 | export class DeepComparer { 22 | 23 | // 1. Inputs 24 | // ----------------------------------------------------------------- 25 | 26 | constructor(private logger: Logger) { 27 | } 28 | 29 | /** 30 | * Tests if two objects are equal by value 31 | * 32 | * @param a - The first object 33 | * @param b - The second object 34 | * @param compareDepth - How many levels deep to compare 35 | * @param options - The current parseOptions 36 | */ 37 | isEqual(a: any, b: any, compareDepth?: number, options: ParseOptions = getParseOptionDefaults()): boolean { 38 | const aStringified = this.detailedStringify(a, compareDepth); 39 | const bStringified = this.detailedStringify(b, compareDepth); 40 | 41 | if (aStringified.result === null || bStringified.result === null) { 42 | this.logger.warn([ 43 | 'Objects could not be compared by value as one or both of them could not be stringified. Returning false. \n', 44 | 'Objects:', a, b 45 | ], options); 46 | return false; 47 | } 48 | 49 | return aStringified.result === bStringified.result; 50 | } 51 | 52 | /** 53 | * Like JSON.stringify, but stringifies additional datatypes that would have been 54 | * nulled otherwise. It also doesn't throw errors on cyclic property paths. 55 | * 56 | * If obj can't be stringified for whatever reason, returns null. 57 | * 58 | * @param obj - The object to stringify 59 | * @param depth - How many levels deep to stringify 60 | */ 61 | detailedStringify(obj: any, depth?: number): DetailedStringifyResult { 62 | try { 63 | // Null cyclic paths 64 | const depthReached = {count: 0}; 65 | const decylcedObj = this.decycle(obj, [], depth, depthReached); 66 | 67 | const stringified = JSON.stringify(decylcedObj, (key, value) => { 68 | // If undefined 69 | if (value === undefined) { 70 | return 'undefined'; 71 | } 72 | // If function or class 73 | if (typeof value === 'function') { 74 | return value.toString(); 75 | } 76 | // If symbol 77 | if (typeof value === 'symbol') { 78 | return value.toString(); 79 | } 80 | return value; 81 | }); 82 | 83 | return {result: stringified, depthReachedCount: depthReached.count}; 84 | } catch (e) { 85 | return {result: null, depthReachedCount: 0}; 86 | } 87 | } 88 | 89 | /** 90 | * Travels on object and replaces cyclical references with null 91 | * 92 | * @param obj - The object to travel 93 | * @param stack - To keep track of already travelled objects 94 | * @param depth - How many levels deep to decycle 95 | * @param depthReached - An object to track the number of times the max depth was reached 96 | */ 97 | decycle(obj: any, stack: any[] = [], depth: number = 5, depthReached: { count: number; }): any { 98 | if (stack.length > depth) { 99 | depthReached.count++; 100 | return null; 101 | } 102 | 103 | if (!obj || typeof obj !== 'object' || obj instanceof Date) { 104 | return obj; 105 | } 106 | 107 | // Check if cyclical and we've traveled this obj already 108 | // 109 | // Note: Test this not by object reference, but by object PROPERTY reference/equality. If an object has identical properties, 110 | // the object is to be considered identical even if it has a different reference itself. 111 | // 112 | // Explanation: This is to prevent a sneaky bug when comparing by value and a parser returns an object as an input that contains a reference to the object holding it 113 | // (like returning the context object that contains a reference to the parent component holding the context object). 114 | // In this example, when the context object changes by reference, the old input will be compared with the new input. However, as the old input consists of 115 | // the old context object that now (through the parent component) contains a reference to the new context object, while the new input references the new context 116 | // object exclusively, the decycle function would produce different results for them if it only checked cyclical paths by reference (even if the context object 117 | // remained identical in value!) 118 | // 119 | // Though an unlikely scenario, checking cyclical paths via object properties rather than the object reference itself solves this problem. 120 | for (const stackObj of stack) { 121 | if (this.objEqualsProperties(obj, stackObj)) { 122 | return null; 123 | } 124 | } 125 | 126 | const s = stack.concat([obj]); 127 | 128 | if (Array.isArray(obj)) { 129 | const newArray = []; 130 | for (const entry of obj) { 131 | newArray.push(this.decycle(entry, s, depth, depthReached)); 132 | } 133 | return newArray; 134 | } else { 135 | const newObj: any = {}; 136 | for (const key of Object.keys(obj)) { 137 | newObj[key] = this.decycle(obj[key], s, depth, depthReached); 138 | } 139 | return newObj; 140 | } 141 | } 142 | 143 | /** 144 | * Returns true when all the properties of one object equal those of another object, otherwise false. 145 | * 146 | * @param a - The first object 147 | * @param b - The second object 148 | */ 149 | objEqualsProperties(a: any, b: any): boolean { 150 | const aKeys = Object.keys(a); 151 | const bKeys = Object.keys(b); 152 | 153 | if (aKeys.length !== bKeys.length) { 154 | return false; 155 | } 156 | 157 | for (const aKey of aKeys) { 158 | if (a[aKey] !== b[aKey]) { 159 | return false; 160 | } 161 | } 162 | 163 | return true; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/lib/services/utils/hookFinder.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HookPosition } from '../../interfacesPublic'; 3 | import { matchAll } from './utils'; 4 | import { Logger } from './logger'; 5 | import { getParseOptionDefaults, ParseOptions } from '../settings/options'; 6 | 7 | /** 8 | * A utility service to easily parse hooks from text content 9 | */ 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class HookFinder { 14 | 15 | constructor(private logger: Logger) {} 16 | 17 | /** 18 | * Finds all text hooks in a piece of content, e.g.Here is an openingTag
16 |Somewhere in the middle of the content. And another openingTag at the end
17 | `; 18 | 19 | const position = hookFinder.find(content, openingTagRegex); 20 | 21 | expect(position).toEqual([ 22 | { 23 | openingTagStartIndex: 46, 24 | openingTagEndIndex: 56, 25 | closingTagStartIndex: null, 26 | closingTagEndIndex: null 27 | }, 28 | { 29 | openingTagStartIndex: 122, 30 | openingTagEndIndex: 132, 31 | closingTagStartIndex: null, 32 | closingTagEndIndex: null 33 | } 34 | ]); 35 | }); 36 | 37 | it('#should find enclosing hook positions as expected', () => { 38 | const openingTagRegex = /openingTag/g; 39 | const closingTagRegex = /closingTag/g; 40 | const content = ` 41 |Here is an openingTag
43 |Then we have a nested openingTag with a closingTag
44 |And then the outer closingTag
45 | `; 46 | 47 | const position = hookFinder.find(content, openingTagRegex, closingTagRegex); 48 | 49 | expect(position).toEqual([ 50 | { 51 | openingTagStartIndex: 102, 52 | openingTagEndIndex: 112, 53 | closingTagStartIndex: 120, 54 | closingTagEndIndex: 130 55 | }, 56 | { 57 | openingTagStartIndex: 56, 58 | openingTagEndIndex: 66, 59 | closingTagStartIndex: 163, 60 | closingTagEndIndex: 173 61 | } 62 | ]); 63 | }); 64 | 65 | it('#should ignore tags that start before previous tag has ended when finding enclosing hooks', () => { 66 | spyOn(console, 'warn').and.callThrough(); 67 | const openingTagRegex = /openingTag/g; 68 | const closingTagRegex = /Tag/g; 69 | const content = 'Here is an openingTag.'; 70 | 71 | expect(hookFinder.find(content, openingTagRegex, closingTagRegex)).toEqual([]); 72 | expect((console as any).warn['calls'].allArgs()[0]) 73 | .toContain('Syntax error - New tag "Tag" started at position 18 before previous tag "openingTag" ended at position 21. Ignoring.'); 74 | }); 75 | 76 | it('#should ignore closing tags that appear without a corresponding opening tag', () => { 77 | spyOn(console, 'warn').and.callThrough(); 78 | const openingTagRegex = /openingTag/g; 79 | const closingTagRegex = /closingTag/g; 80 | 81 | const content = 'Here is an openingTag and a closingTag. Here is just a closingTag.'; 82 | expect(hookFinder.find(content, openingTagRegex, closingTagRegex)).toEqual([{ openingTagStartIndex: 11, openingTagEndIndex: 21, closingTagStartIndex: 28, closingTagEndIndex: 38 }]); 83 | expect((console as any).warn['calls'].allArgs()[0]) 84 | .toContain('Syntax error - Closing tag without preceding opening tag found: "closingTag". Ignoring.'); 85 | }); 86 | 87 | it('#should skip nested hooks, if not allowed', () => { 88 | const openingTagRegex = /openingTag/g; 89 | const closingTagRegex = /closingTag/g; 90 | 91 | const content = 'Here is the outer openingTag, an inner openingTag, an inner closingTag and an outer closingTag.'; 92 | expect(hookFinder.find(content, openingTagRegex, closingTagRegex, false)).toEqual([{ openingTagStartIndex: 18, openingTagEndIndex: 28, closingTagStartIndex: 84, closingTagEndIndex: 94 }]); 93 | }); 94 | 95 | }); -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/src/tests/unit/polyfills.spec.ts: -------------------------------------------------------------------------------- 1 | import { matchAll } from '../testing-api'; 2 | 3 | // Straight Jasmine testing without Angular's testing support 4 | describe('Polyfills', () => { 5 | 6 | it('#should throw an error if given a non-global regex', () => { 7 | expect(() => matchAll('something', /test/)) 8 | .toThrow(new Error('TypeError: matchAll called with a non-global RegExp argument')); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-dynamic-hooks/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ], 14 | "angularCompilerOptions": { 15 | "strictTemplates": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "paths": { 13 | "ngx-dynamic-hooks": [ 14 | "./dist/ngx-dynamic-hooks" 15 | ] 16 | }, 17 | "esModuleInterop": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "node", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "ES2022", 25 | "useDefineForClassFields": false, 26 | "lib": [ 27 | "ES2022", 28 | "dom" 29 | ] 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } 38 | --------------------------------------------------------------------------------