UNPKG

hmpl-js

Version:

🐜 Server-oriented customizable templating for JavaScript

1,203 lines (1,202 loc) 61.1 kB
"use strict"; import JSON5 from "json5"; import DOMPurify from "dompurify"; /** * Constants representing various property names and error messages. */ const SOURCE = `src`; const METHOD = `method`; const ID = `initId`; const AFTER = `after`; const REPEAT = `repeat`; const MEMO = `memo`; const INDICATORS = `indicators`; const AUTO_BODY = `autoBody`; const COMMENT = `hmpl`; const FORM_DATA = `formData`; const DISALLOWED_TAGS = `disallowedTags`; const SANITIZE = `sanitize`; const ALLOWED_CONTENT_TYPES = "allowedContentTypes"; const REQUEST_INIT_GET = `get`; const INTERVAL = `interval`; const RESPONSE_ERROR = `BadResponseError`; const REQUEST_INIT_ERROR = `RequestInitError`; const RENDER_ERROR = `RenderError`; const REQUEST_COMPONENT_ERROR = `RequestComponentError`; const COMPILE_OPTIONS_ERROR = `CompileOptionsError`; const PARSE_ERROR = `ParseError`; const COMPILE_ERROR = `CompileError`; const DEFAULT_AUTO_BODY = { formData: true }; const DEFAULT_FALSE_AUTO_BODY = { formData: false }; /** * List of request options that are allowed. */ const REQUEST_OPTIONS = [ SOURCE, METHOD, ID, AFTER, REPEAT, INDICATORS, MEMO, AUTO_BODY, ALLOWED_CONTENT_TYPES, DISALLOWED_TAGS, SANITIZE, INTERVAL ]; /** * List of valid HTTP methods */ const VALID_METHODS = [ "get", "post", "put", "delete", "patch", "trace", "options" ]; /** * HTTP status codes without successful responses. * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for more details. */ const CODES = [ 100, 101, 102, 103, 300, 301, 302, 303, 304, 305, 306, 307, 308, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511 ]; /** * Tags available for deletion from response */ const DISALLOWED_TAGS_VALUES = ["script", "style", "iframe"]; /** * Default value for sanitize response settings. Plans to add config for DOMPurify */ const DEFAULT_SANITIZE = false; /** * Default value for the processed response content type */ const DEFAULT_ALLOWED_CONTENT_TYPES = ["text/html"]; /** * Default value for tags to remove from response */ const DEFAULT_DISALLOWED_TAGS = []; /** * Checks if the provided value is an object (excluding arrays and null). * @param val - The value to check. * @returns True if val is an object, false otherwise. */ const checkObject = (val) => { return typeof val === "object" && !Array.isArray(val) && val !== null; }; /** * Validates whether the provided value is an array of strings. * @param arr - The value to check, expected to be an array. * @param currentError - The error message prefix for non-string elements. * @returns `true` if the value is an array of strings, `false` otherwise. * If an element is found that is not of the string type, an error is created with details. */ const checkIsStringArray = (arr, currentError) => { if (!Array.isArray(arr)) return false; let isArrString = true; for (let i = 0; i < arr.length; i++) { const arrItem = arr[i]; if (typeof arrItem !== "string") { createError(`${currentError}: In the array, the element with index ${i} is not a string`); isArrString = false; break; } } return isArrString; }; /** * Checks if the provided value is a function. * @param val - The value to check. * @returns True if val is a function, false otherwise. */ const checkFunction = (val) => { return Object.prototype.toString.call(val) === "[object Function]"; }; /** * Throws a new error with the provided message. * @param text - The error message. */ const createError = (text) => { throw new Error(text); }; /** * Logs a warning message to the console. * @param text - The warning message. */ const createWarning = (text) => { console.warn(text); }; /** * Validates the HTTP method. * @param method - The HTTP method to validate. * @returns False if the method is valid, true otherwise. */ const getIsMethodValid = (method) => { return VALID_METHODS.includes(method.toLowerCase()); }; /** * Parses a string into a HTML template element. * @param str - The string to parse. * @returns The first child node of the parsed template. */ const getTemplateWrapper = (str, sanitize = false) => { let sanitizedStr = str; if (sanitize) { sanitizedStr = DOMPurify.sanitize(str); } const elementDocument = new DOMParser().parseFromString(`<template>${sanitizedStr}</template>`, "text/html"); const elWrapper = elementDocument.childNodes[0].childNodes[0].firstChild; return elWrapper; }; /** * Parses the response string into DOM elements, excluding scripts. * @param response - The response string to parse. * @param disallowedTags - A list of HTML tags that should be removed from the response. * @param sanitize - Sanitize the response content, ensuring it is safe to render. * @returns The parsed template wrapper. */ const getResponseElements = (response, disallowedTags = [], sanitize) => { const elWrapper = getTemplateWrapper(response, sanitize); const elContent = elWrapper["content"]; for (let i = 0; i < disallowedTags.length; i++) { const tag = disallowedTags[i]; const elements = elContent.querySelectorAll(tag); for (let j = 0; j < elements.length; j++) { elContent.removeChild(elements[j]); } } return elWrapper; }; /** * Checks if the provided content type is not allowed. * @param contentType - The content type to check (e.g., "text/html" or "application/json"). * @param allowedContentTypes - An array of allowed content type substrings. * @returns `true` if the content type is not allowed, `false` otherwise. */ const getIsNotAllowedContentType = (contentType, allowedContentTypes) => { if (!contentType) return true; let isContain = false; for (let i = 0; i < allowedContentTypes.length; i++) { const allowedContentType = allowedContentTypes[i]; if (contentType.includes(allowedContentType)) { isContain = true; break; } } return !isContain; }; /** * Makes an HTTP request and handles the response. * @param el - The element related to the request. * @param mainEl - The main element in the DOM. * @param dataObj - The node object containing data. * @param method - The HTTP method to use. * @param source - The source URL for the request. * @param isRequest - Indicates if it's a single request. * @param isRequests - Indicates if it's multiple requests. * @param isMemo - Indicates if memoization is enabled. * @param options - The request initialization options. * @param templateObject - The template instance. * @param allowedContentTypes - Allowed Content-Types for response processing. * @param disallowedTags - A list of HTML tags that should be removed from the response. * @param sanitize - A function or method used to sanitize the response content, ensuring it is safe to render. * @param reqObject - The request object. * @param indicators - Parsed indicators for the request. */ const makeRequest = (el, mainEl, dataObj, method, source, isRequest, isRequests, isMemo, options = {}, templateObject, allowedContentTypes, disallowedTags, sanitize, reqObject, indicators, currentClearInterval) => { const { mode, cache, redirect, get, referrerPolicy, signal, credentials, timeout, referrer, headers, body, window: windowOption, integrity } = options; const initRequest = { method: method.toUpperCase() }; // Assign optional properties if they are provided if (credentials !== undefined) { initRequest.credentials = credentials; } if (body !== undefined) { initRequest.body = body; } if (mode !== undefined) { initRequest.mode = mode; } if (cache !== undefined) { initRequest.cache = cache; } if (redirect !== undefined) { initRequest.redirect = redirect; } if (referrerPolicy !== undefined) { initRequest.referrerPolicy = referrerPolicy; } if (integrity !== undefined) { initRequest.integrity = integrity; } if (referrer !== undefined) { initRequest.referrer = referrer; } const isHaveSignal = signal !== undefined; if (isHaveSignal) { initRequest.signal = signal; } if (windowOption !== undefined) { initRequest.window = windowOption; } if (options.keepalive !== undefined) { createWarning(`${REQUEST_INIT_ERROR}: The "keepalive" property is not yet supported`); } // Handle headers if provided if (headers) { if (checkObject(headers)) { const newHeaders = new Headers(); for (const key in headers) { const value = headers[key]; const valueType = typeof value; if (valueType === "string") { newHeaders.set(key, value); } else { createError(`${REQUEST_INIT_ERROR}: Expected type string, but received type ${valueType}`); } } initRequest.headers = newHeaders; } else { createError(`${REQUEST_INIT_ERROR}: The "headers" property must contain a value object`); } } // Handle timeout and signal if (timeout) { if (!isHaveSignal) { initRequest.signal = AbortSignal.timeout(timeout); } else { createWarning(`${REQUEST_INIT_ERROR}: The "signal" property overwrote the AbortSignal from "timeout"`); } } const isRequestMemo = isMemo && !isRequest && dataObj?.memo; const getIsNotFullfilledStatus = (status) => status === "rejected" || (typeof status === "number" && (status < 200 || status > 299)); const requestContext = getInstanceContext(undefined, currentClearInterval); /** * Calls the 'get' function with the response if provided. * @param reqResponse - The response to pass to the 'get' function. */ const callGetResponse = (reqResponse) => { if (isRequests) { reqObject.response = reqResponse; get?.("response", reqResponse, requestContext, reqObject); } get?.("response", mainEl, requestContext); }; /** * Updates the DOM nodes with new content. * @param content - The content to insert. * @param isClone - Whether to clone the content. * @param isNodes - Whether to update nodes in dataObj. */ const updateNodes = (content, isClone = true, isNodes = false) => { if (isRequest) { templateObject.response = content.cloneNode(true); get?.("response", content, requestContext); } else { let reqResponse = []; const newContent = isClone ? content.cloneNode(true) : content; const nodes = [...newContent.content.childNodes]; if (dataObj.nodes) { const parentNode = dataObj.parentNode; const newNodes = []; const nodesLength = dataObj.nodes.length; for (let i = 0; i < nodesLength; i++) { const node = dataObj.nodes[i]; if (i === nodesLength - 1) { for (let j = 0; j < nodes.length; j++) { const reqNode = nodes[j]; const newNode = parentNode.insertBefore(reqNode, node); newNodes.push(newNode); } } parentNode.removeChild(node); } reqResponse = newNodes.slice(); dataObj.nodes = newNodes; } else { const parentNode = el.parentNode; const newNodes = []; const nodesLength = nodes.length; for (let i = 0; i < nodesLength; i++) { const node = nodes[i]; const newNode = parentNode.insertBefore(node, el); newNodes.push(newNode); } parentNode.removeChild(el); reqResponse = newNodes.slice(); dataObj.nodes = newNodes; dataObj.parentNode = parentNode; } if (isRequestMemo && isNodes) { dataObj.memo.nodes = dataObj.nodes; if (dataObj.memo.isPending) dataObj.memo.isPending = false; } callGetResponse(reqResponse); } }; let isNotHTMLResponse = false; /** * Replaces nodes with a comment node. */ const setComment = () => { if (isRequest) { templateObject.response = undefined; get?.("response", undefined, requestContext); } else { if (dataObj?.nodes) { const parentNode = dataObj.parentNode; const nodesLength = dataObj.nodes.length; for (let i = 0; i < nodesLength; i++) { const node = dataObj.nodes[i]; if (i === nodesLength - 1) { parentNode.insertBefore(dataObj.comment, node); } parentNode.removeChild(node); } dataObj.nodes = null; dataObj.parentNode = null; if (isRequests) { reqObject.response = undefined; get?.("response", undefined, requestContext, reqObject); } get?.("response", mainEl, requestContext); } } if (isRequestMemo) { if (dataObj.memo.response !== null) { dataObj.memo.response = null; delete dataObj.memo.isPending; delete dataObj.memo.nodes; } } }; /** * Updates the indicator based on the request status. * @param status - The current request status. */ const updateIndicator = (status) => { if (indicators) { if (isRequestMemo && status !== "pending" && getIsNotFullfilledStatus(status)) { if (dataObj.memo.isPending) dataObj.memo.isPending = false; } if (status === "pending") { const content = indicators["pending"]; if (content !== undefined) { if (isRequestMemo) { dataObj.memo.isPending = true; } updateNodes(content); } } else if (status === "rejected") { const content = indicators["rejected"]; if (content !== undefined) { updateNodes(content); } else { const errorContent = indicators["error"]; if (errorContent !== undefined) { updateNodes(errorContent); } else { setComment(); } } } else { const content = indicators[`${status}`]; if (status > 399) { if (content !== undefined) { updateNodes(content); } else { const errorContent = indicators["error"]; if (errorContent !== undefined) { updateNodes(errorContent); } else { setComment(); } } } else { if (status < 200 || status > 299) { isNotHTMLResponse = true; if (content !== undefined) { updateNodes(content); } else { setComment(); } } } } } }; /** * Updates the status and handles dependencies. * @param status - The new request status. */ const updateStatusDepenencies = (status) => { if (isRequests) { if (reqObject.status !== status) { reqObject.status = status; get?.("status", status, requestContext, reqObject); } } else { if (templateObject.status !== status) { templateObject.status = status; get?.("status", status, requestContext); } } if (isRequestMemo && getIsNotFullfilledStatus(status)) { dataObj.memo.response = null; delete dataObj.memo.nodes; } updateIndicator(status); }; /** * Uses cached nodes if available. */ const takeNodesFromCache = () => { if (dataObj.memo.isPending) { const parentNode = dataObj.parentNode; const memoNodes = dataObj.memo.nodes; const currentNodes = dataObj.nodes; const nodesLength = currentNodes.length; const newNodes = []; for (let i = 0; i < nodesLength; i++) { const node = currentNodes[i]; if (i === nodesLength - 1) { for (let j = 0; j < memoNodes.length; j++) { const reqNode = memoNodes[j]; const newNode = parentNode.insertBefore(reqNode, node); newNodes.push(newNode); } } parentNode.removeChild(node); } dataObj.nodes = newNodes.slice(); dataObj.memo.isPending = false; dataObj.memo.nodes = newNodes.slice(); } const reqResponse = dataObj.nodes.slice(); callGetResponse(reqResponse); }; let requestStatus = 200; updateStatusDepenencies("pending"); let isRejectedError = true; let isError = true; // Perform the fetch request fetch(source, initRequest) .then((response) => { isRejectedError = false; requestStatus = response.status; updateStatusDepenencies(requestStatus); if (!response.ok) { if (indicators) isError = false; createError(`${RESPONSE_ERROR}: Response with status code ${requestStatus}`); } if (Array.isArray(allowedContentTypes) && allowedContentTypes.length !== 0) { const contentType = response.headers.get("Content-Type"); if (getIsNotAllowedContentType(contentType, allowedContentTypes)) { createError(`${RESPONSE_ERROR}: Expected ${allowedContentTypes .map((type) => `"${type}"`) .join(", ")}, but received "${contentType}"`); } } return response.text(); }) .then((data) => { if (!isNotHTMLResponse) { if (isRequestMemo) { const { response } = dataObj.memo; if (response === null) { dataObj.memo.response = data; } else { if (response === data) { takeNodesFromCache(); return; } else { dataObj.memo.response = data; delete dataObj.memo.nodes; } } } const templateWrapper = getResponseElements(data, disallowedTags, sanitize); if (isRequest) { templateObject.response = templateWrapper; get?.("response", templateWrapper, requestContext); } else { const reqResponse = []; const nodes = [ ...templateWrapper.content.childNodes ]; if (dataObj) { updateNodes(templateWrapper, false, true); } else { const parentNode = el.parentNode; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const reqNode = parentNode.insertBefore(node, el); if (isRequests) { reqResponse.push(reqNode); } } parentNode.removeChild(el); if (isRequests) { reqObject.response = reqResponse; get?.("response", reqResponse, requestContext, reqObject); } get?.("response", mainEl, requestContext); } } } }) .catch((error) => { // Errors like CORS, timeout and others. if (isRejectedError) { updateStatusDepenencies("rejected"); if (!indicators) { setComment(); } } else { if (isError) { setComment(); } } throw error; }); }; /** * Creates a context object for HMPL instance with optional event and clearInterval function. * @param event - Optional event object. * @param currentClearInterval - Optional function to clear interval. * @returns HMPLInstanceContext object with request context. */ const getInstanceContext = (event, currentClearInterval) => { const request = {}; if (event !== undefined) { request.event = event; } if (currentClearInterval) { request.clearInterval = currentClearInterval; } return { request }; }; /** * Executes a HMPLRequestInitFunction to obtain request initialization options. * @param fn - The function to execute. * @param event - The event object (if any). * @returns The HMPLRequestInit object. */ const getRequestInitFromFn = (fn, event, currentClearInterval) => { const context = getInstanceContext(event, currentClearInterval); const result = fn(context); return result; }; /** * Renders the template by processing requests and applying options. * @param currentEl - The current element or comment node. * @param fn - The render function. * @param requests - Array of request objects. * @param compileOptions - Options provided during compilation. * @param isMemoUndefined - Indicates if memoization is undefined. * @param isAutoBodyUndefined - Indicates if autoBody is undefined. * @param isRequest - Indicates if it's a single request. * @param isAllowedContentTypesUndefined - Indicates if allowedContentTypes is undefined. * @param isDisallowedTagsUndefined - A flag indicating whether the disallowedTags property is undefined. * @param isSanitizeUndefined - A flag indicating whether the sanitize property is undefined. * @returns The rendered template function. */ const renderTemplate = (currentEl, fn, requests, compileOptions, isMemoUndefined, isAutoBodyUndefined, isAllowedContentTypesUndefined, isDisallowedTagsUndefined, isSanitizeUndefined, isRequest = false) => { const renderRequest = (req, mainEl) => { const source = req[SOURCE]; if (source) { const method = (req[METHOD] || "GET").toLowerCase(); if (!getIsMethodValid(method)) { createError(`${REQUEST_COMPONENT_ERROR}: The "${METHOD}" property has only GET, POST, PUT, PATCH, TRACE, OPTIONS or DELETE values`); } else { const after = req[AFTER]; if (after && isRequest) createError(`${RENDER_ERROR}: EventTarget is undefined`); const isModeUndefined = !req.hasOwnProperty(REPEAT); const oldMode = isModeUndefined ? true : req[REPEAT]; const modeAttr = oldMode ? "all" : "one"; const isAll = modeAttr === "all"; const interval = req[INTERVAL]; const isReqMemoUndefined = !req.hasOwnProperty(MEMO); const isReqIntervalUndefined = !req.hasOwnProperty(INTERVAL); let isMemo = isMemoUndefined ? false : compileOptions[MEMO]; if (!isReqMemoUndefined) { if (after) { if (req[MEMO]) { if (!isAll) { createError(`${REQUEST_COMPONENT_ERROR}: Memoization works in the enabled repetition mode`); } else { isMemo = true; } } else { isMemo = false; } } else { createError(`${REQUEST_COMPONENT_ERROR}: Memoization works in the enabled repetition mode`); } } else { if (isMemo) { if (after) { if (!isAll) { isMemo = false; } } else { isMemo = false; } } } if (!isReqIntervalUndefined) { if (isAll && after) { createError(`${REQUEST_COMPONENT_ERROR}: The "${INTERVAL}" property does not work with repetiton mode yet`); } } const isReqAutoBodyUndefined = !req.hasOwnProperty(AUTO_BODY); let autoBody = isAutoBodyUndefined ? false : compileOptions[AUTO_BODY]; if (!isReqAutoBodyUndefined) { if (after) { let reqAutoBody = req[AUTO_BODY]; validateAutoBody(reqAutoBody); if (autoBody === true) { autoBody = DEFAULT_AUTO_BODY; } if (reqAutoBody === true) { reqAutoBody = DEFAULT_AUTO_BODY; } if (reqAutoBody === false) { autoBody = false; } else { const newAutoBody = { ...(autoBody === false ? DEFAULT_FALSE_AUTO_BODY : autoBody), ...reqAutoBody }; autoBody = newAutoBody; } } else { autoBody = false; createError(`${REQUEST_COMPONENT_ERROR}: The "${AUTO_BODY}" property does not work without the "${AFTER}" property`); } } else { if (autoBody === true) { autoBody = DEFAULT_AUTO_BODY; } if (!after) { autoBody = false; } } const isReqAllowedContentTypesUndefined = !req.hasOwnProperty(ALLOWED_CONTENT_TYPES); let allowedContentTypes = isAllowedContentTypesUndefined ? DEFAULT_ALLOWED_CONTENT_TYPES : compileOptions[ALLOWED_CONTENT_TYPES]; if (!isReqAllowedContentTypesUndefined) { const currentAllowedContentTypes = req[ALLOWED_CONTENT_TYPES]; validateAllowedContentTypes(currentAllowedContentTypes); allowedContentTypes = currentAllowedContentTypes; } const isReqDisallowedTagsUndefined = !req.hasOwnProperty(DISALLOWED_TAGS); let disallowedTags = isDisallowedTagsUndefined ? DEFAULT_DISALLOWED_TAGS : compileOptions[DISALLOWED_TAGS]; if (!isReqDisallowedTagsUndefined) { const currentDisallowedTags = req[DISALLOWED_TAGS]; validateDisallowedTags(currentDisallowedTags); disallowedTags = currentDisallowedTags; } const isReqSanitizeUndefined = !req.hasOwnProperty(SANITIZE); let sanitize = isSanitizeUndefined ? DEFAULT_SANITIZE : compileOptions[SANITIZE]; if (!isReqSanitizeUndefined) { const currentSanitize = req[SANITIZE]; validateSanitize(currentSanitize); sanitize = currentSanitize; } const initId = req[ID]; const nodeId = req.nodeId; let indicators = req.indicators; if (indicators) { const parseIndicator = (val) => { const { trigger, content } = val; if (!trigger) createError(`${REQUEST_COMPONENT_ERROR}: Failed to activate or detect the indicator`); if (!content) createError(`${REQUEST_COMPONENT_ERROR}: Failed to activate or detect the indicator`); if (CODES.indexOf(trigger) === -1 && trigger !== "pending" && trigger !== "rejected" && trigger !== "error") { createError(`${REQUEST_COMPONENT_ERROR}: Failed to activate or detect the indicator`); } const elWrapper = getTemplateWrapper(content); return { ...val, content: elWrapper }; }; const newOn = {}; const uniqueTriggers = []; for (let i = 0; i < indicators.length; i++) { const currentIndicator = parseIndicator(indicators[i]); const { trigger } = currentIndicator; if (uniqueTriggers.indexOf(trigger) === -1) { uniqueTriggers.push(trigger); } else { createError(`${REQUEST_COMPONENT_ERROR}: Indicator trigger must be unique`); } newOn[`${trigger}`] = currentIndicator.content; } indicators = newOn; } const getOptions = (options, isArray = false) => { if (isArray) { if (initId) { let result; for (let i = 0; i < options.length; i++) { const currentOptions = options[i]; if (currentOptions.id === initId) { result = currentOptions.value; break; } } if (!result) { createError(`${REQUEST_COMPONENT_ERROR}: ID referenced by request not found`); } return result; } else { return {}; } } else { if (initId) createError(`${REQUEST_COMPONENT_ERROR}: ID referenced by request not found`); return options; } }; const isInterval = interval !== undefined; const isDataObj = (isAll && after) || isInterval; const reqFunction = (reqEl, options, templateObject, data, reqMainEl, isArray = false, reqObject, isRequests = false, currentHMPLElement, event, currentInterval) => { const id = data.currentId; if (isRequest) { if (!reqEl) reqEl = mainEl; } else { if (!reqEl) { let currentEl; const { els } = data; for (let i = 0; i < els.length; i++) { const e = els[i]; if (e.id === nodeId) { currentHMPLElement = e; currentEl = e.el; break; } } reqEl = currentEl; } } let dataObj = undefined; if (!isRequest) { if (isDataObj || indicators) { dataObj = currentHMPLElement.objNode; if (!dataObj) { dataObj = { id, nodes: null, parentNode: null, comment: reqEl }; if (isMemo) { dataObj.memo = { response: null }; if (indicators) { dataObj.memo.isPending = false; } } if (isInterval) { if (currentInterval) { dataObj.interval = { value: currentInterval, clearInterval: () => clearInterval(currentInterval) }; } } currentHMPLElement.objNode = dataObj; data.dataObjects.push(dataObj); data.currentId++; } } } let currentOptions = getOptions(options, isArray); const isOptionsFunction = checkFunction(currentOptions); if (!isOptionsFunction && currentOptions) currentOptions = { ...currentOptions }; if (autoBody && autoBody.formData && event && !isOptionsFunction) { const { type, target } = event; if (type === "submit" && target && target instanceof HTMLFormElement && target.nodeName === "FORM") { currentOptions.body = new FormData(target, event.submitter); } } let currentClearInterval = currentInterval ? () => clearInterval(currentInterval) : undefined; currentClearInterval = isRequest ? currentClearInterval : dataObj?.interval?.clearInterval; const requestInit = isOptionsFunction ? getRequestInitFromFn(currentOptions, event, currentClearInterval) : currentOptions; if (!checkObject(requestInit) && requestInit !== undefined) createError(`${REQUEST_INIT_ERROR}: Expected an object with initialization options`); makeRequest(reqEl, reqMainEl, dataObj, method, source, isRequest, isRequests, isMemo, requestInit, templateObject, allowedContentTypes, disallowedTags, sanitize, reqObject, indicators, currentClearInterval); }; let currentReqFunction = reqFunction; if (interval) { validateInterval(interval); const time = Number(interval); currentReqFunction = (reqEl, options, templateObject, data, reqMainEl, isArray = false, reqObject, isRequests = false, currentHMPLElement, event) => { let interval = null; interval = setInterval(() => { reqFunction(reqEl, options, templateObject, data, reqMainEl, isArray, reqObject, isRequests, currentHMPLElement, event, interval); }, time); }; } let requestFunction = currentReqFunction; if (after) { const setEvents = (reqEl, event, selector, options, templateObject, data, isArray, isRequests, reqMainEl, reqObject, currentHMPLElement) => { const els = reqMainEl.querySelectorAll(selector); if (els.length === 0) { createError(`${RENDER_ERROR}: Selectors nodes not found`); } const afterFn = isAll ? (evt) => { currentReqFunction(reqEl, options, templateObject, data, reqMainEl, isArray, reqObject, isRequests, currentHMPLElement, evt); } : (evt) => { currentReqFunction(reqEl, options, templateObject, data, reqMainEl, isArray, reqObject, isRequests, currentHMPLElement, evt); for (let j = 0; j < els.length; j++) { const currentAfterEl = els[j]; currentAfterEl.removeEventListener(event, afterFn); } }; for (let i = 0; i < els.length; i++) { const afterEl = els[i]; afterEl.addEventListener(event, afterFn); } }; if (after.indexOf(":") > 0) { const afterArr = after.split(":"); const event = afterArr[0]; const selector = afterArr.slice(1).join(":"); requestFunction = (reqEl, options, templateObject, data, reqMainEl, isArray = false, reqObject, isRequests = false, currentHMPLElement) => { setEvents(reqEl, event, selector, options, templateObject, data, isArray, isRequests, reqMainEl, reqObject, currentHMPLElement); }; } else { createError(`${REQUEST_COMPONENT_ERROR}: The "${AFTER}" property doesn't work without EventTargets`); } } else { if (!isModeUndefined) { createError(`${REQUEST_COMPONENT_ERROR}: The "${REPEAT}" property doesn't work without "${AFTER}" property`); } } return requestFunction; } } else { createError(`${REQUEST_COMPONENT_ERROR}: The "${SOURCE}" property are not found or empty`); } }; let reqFn; if (isRequest) { requests[0].el = currentEl; reqFn = renderRequest(requests[0]); } else { let id = -2; const getRequests = (currrentElement) => { id++; if (currrentElement.nodeType == 8) { let value = currrentElement.nodeValue; if (value && value.startsWith(COMMENT)) { value = value.slice(4); const currentIndex = Number(value); const currentRequest = requests[currentIndex]; if (Number.isNaN(currentIndex) || currentRequest === undefined) { createError(`${PARSE_ERROR}: Request object with id "${currentIndex}" not found`); } currentRequest.el = currrentElement; currentRequest.nodeId = id; } } if (currrentElement.hasChildNodes()) { const chNodes = currrentElement.childNodes; for (let i = 0; i < chNodes.length; i++) { getRequests(chNodes[i]); } } }; getRequests(currentEl); if (requests.length > 1) { const algorithm = []; for (let i = 0; i < requests.length; i++) { const currentRequest = requests[i]; algorithm.push(renderRequest(currentRequest, currentEl)); } reqFn = (reqEl, options, templateObject, data, mainEl, isArray = false) => { if (!reqEl) { reqEl = mainEl; } const requests = []; const els = data.els; for (let i = 0; i < els.length; i++) { const hmplElement = els[i]; const currentReqEl = hmplElement.el; const currentReqFn = algorithm[i]; const currentReq = { response: undefined }; currentReqFn(currentReqEl, options, templateObject, data, reqEl, isArray, currentReq, true, hmplElement); requests.push(currentReq); } templateObject.requests = requests; }; } else { const currentRequest = requests[0]; reqFn = renderRequest(currentRequest, currentEl); } } return fn(reqFn); }; /** * Validates the options provided for a request. * @param currentOptions - The options to validate. */ const validateOptions = (currentOptions) => { const isObject = checkObject(currentOptions); if (isObject && currentOptions.hasOwnProperty(`${REQUEST_INIT_GET}`)) { if (!checkFunction(currentOptions[REQUEST_INIT_GET])) { createError(`${REQUEST_INIT_ERROR}: The "${REQUEST_INIT_GET}" property has a function value`); } } }; /** * Validates the allowed content types for a request or response. * Ensures the value is either "*" (indicating all types are allowed) or an array of strings. * @param allowedContentTypes - The content types to validate, expected to be "*" or an array of strings. * @param isCompile - A flag indicating whether the validation is for compile-time options (default: `false`). * @throws An error if the input is not a "*" or a string array. */ const validateAllowedContentTypes = (allowedContentTypes, isCompile = false) => { const currentError = isCompile ? COMPILE_OPTIONS_ERROR : REQUEST_COMPONENT_ERROR; if (allowedContentTypes !== "*" && !checkIsStringArray(allowedContentTypes, currentError)) { createError(`${currentError}: Expected "*" or string array, but got neither`); } }; /** * Validates the `autoBody` option for a request or compile-time configuration. * Ensures the value is either a boolean or an object with specific properties. * @param autoBody - The `autoBody` option to validate, expected to be a boolean or an object. * @param isCompile - A flag indicating whether the validation is for compile-time options (default: `false`). * @throws An error if the input is not a boolean, not a HMPLAutoBodyOptions type object, or contains unexpected properties. */ const validateAutoBody = (autoBody, isCompile = false) => { const isObject = checkObject(autoBody); const currentError = isCompile ? COMPILE_OPTIONS_ERROR : REQUEST_COMPONENT_ERROR; if (typeof autoBody !== "boolean" && !isObject) createError(`${currentError}: Expected a boolean or object, but got neither`); if (isObject) { for (const key in autoBody) { switch (key) { case FORM_DATA: if (typeof autoBody[FORM_DATA] !== "boolean") createError(`${currentError}: The "${FORM_DATA}" property should be a boolean`); break; default: createError(`${currentError}: Unexpected property "${key}"`); break; } } } }; /** * Validates the `disallowedTags` option for a request or compile-time configuration. * Ensures the value is an array and contains only allowed disallowed tag values. * @param disallowedTags - The `disallowedTags` option to validate, expected to be an array. * @param isCompile - A flag indicating whether the validation is for compile-time options (default: `false`). * @throws An error if the input is not an array or contains unexpected disallowed tag values. */ const validateDisallowedTags = (disallowedTags, isCompile = false) => { const currentError = isCompile ? COMPILE_OPTIONS_ERROR : REQUEST_COMPONENT_ERROR; const isArray = Array.isArray(disallowedTags); if (!isArray) createError(`${currentError}: The value of the property "${DISALLOWED_TAGS}" must be an array`); for (let i = 0; i < disallowedTags.length; i++) { const disallowedTag = disallowedTags[i]; if (!DISALLOWED_TAGS_VALUES.includes(disallowedTag)) { createError(`${currentError}: The value "${disallowedTag}" is not processed`); } } }; /** * Validates the `sanitize` option for a request or compile-time configuration. * Ensures the value is a boolean. * @param sanitize - The `sanitize` option to validate, expected to be a boolean. * @param isCompile - A flag indicating whether the validation is for compile-time options (default: `false`). * @throws An error if the input is not a boolean. */ const validateSanitize = (sanitize, isCompile = false) => { const currentError = isCompile ? COMPILE_OPTIONS_ERROR : REQUEST_COMPONENT_ERROR; if (typeof sanitize !== "boolean") { createError(`${currentError}: The value of the property "${SANITIZE}" must be a boolean`); } }; /** * Validates the HMPLIdentificationRequestInit object. * @param currentOptions - The identification options to validate. */ const validateIdOptions = (currentOptions) => { if (!currentOptions.hasOwnProperty("id") || !currentOptions.hasOwnProperty("value")) { createError(`${REQUEST_INIT_ERROR}: Missing "id" or "value" property`); } }; /** * Validates an array of HMPLIdentificationRequestInit objects. * @param currentOptions - The array of identification options to validate. */ const validateIdentificationOptionsArray = (currentOptions) => { const ids = []; for (let i = 0; i < currentOptions.length; i++) { const idOptions = currentOptions[i]; if (!checkObject(idOptions)) createError(`${REQUEST_INIT_ERROR}: IdentificationRequestInit is of type object`); validateIdOptions(idOptions); const { id } = idOptions; const isIdString = typeof idOptions.id === "string"; if (!isIdString && typeof idOptions.id !== "number") createError(`${REQUEST_INIT_ERROR}: ID must be a string or a number`); if (ids.indexOf(id) > -1) { createError(`${REQUEST_INIT_ERROR}: ID with value ${isIdString ? `"${id}"` : id} already exists`); } else { ids.push(id); } } }; /** * Validates the interval time value against a number * @param time - The HMPLRequestInfo object. */ const validateInterval = (time) => { if (typeof time !== "number") { createError(`${REQUEST_COMPONENT_ERROR}: The "${INTERVAL}" value must be number`); } }; /** * Converts a HMPLRequestInfo object to a JSON string. * @param info - The HMPLRequestInfo object. * @returns Request block. */ export const stringify = (info) => { const formatValue = (value) => { if (typeof value === "string") { return `"${value}"`; } if (typeof value === "number" || typeof value === "boolean") { return `${value}`; } if (Array.isArray(value)) { return `[${value.map((item) => formatValue(item)).join(",")}]`; } if (typeof value === "object" && value !== null) { return `{${Object.entries(value) .map(([k, v]) => `${k}:${formatValue(v)}`) .join(",")}}`; } return ""; }; let body = Object.entries(info) .map(([key, value]) => `${key}=${formatValue(value)}`) .join(" "); if (body.endsWith("}")) { body += " "; } return `{{#request ${body}}}{{/request}}`; }; /** * Compiles a template string into a HMPLTemplateFunction. * @param template - The template string. * @param options - The compilation options. * @returns A function that creates template instances. */ export const compile = (template, options = {}) => {