diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d81b7bb..33fc922f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ -# Release 2.0.4 +# Release 2.0.6 -9ab5a3f update version in package.json to 2.0.4 (guseyn, Thu Apr 24 12:54:45 2025 +0400) -029d110 fix font in katex (guseyn, Thu Apr 24 12:54:43 2025 +0400) +66457f0 update version in package.json to 2.0.6 (guseyn, Sat May 17 00:20:58 2025 +0400) diff --git a/README.md b/README.md index b037c40d..d3e492e6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -**v2.0.4** +**v2.0.6** [![EHTML CI](https://github.com/Guseyn/EHTML/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/Guseyn/EHTML/actions/workflows/ci.yml) diff --git a/bundle.js b/bundle.js new file mode 100644 index 00000000..5c88c92d --- /dev/null +++ b/bundle.js @@ -0,0 +1,127 @@ +import path from 'node:path' +import { copyFile, readdir, stat, readFile } from 'node:fs/promises' +import { build } from 'esbuild' +import glob from 'fast-glob' + +const dynamicImportsByFiles = { + 'src/E/e-markdown.js': (contents) => { + if ( + contents.includes("import showdownHighlight from 'ehtml/third-party/showdown-highlight'") && + contents.includes("import showdownKatex from 'ehtml/third-party/showdown-katex/showdown-katex'") + ) { + contents = contents.replace( +`import showdownHighlight from 'ehtml/third-party/showdown-highlight' +import showdownKatex from 'ehtml/third-party/showdown-katex/showdown-katex'`, +` +let showdownHighlight = null +let showdownKatex = null +if (typeof _EHTML_MODE_ === 'string' && _EHTML_MODE_ === 'NORMAL') { + const [highlight, katex] = await Promise.all([ + import('ehtml/third-party/showdown-highlight'), + import('ehtml/third-party/showdown-katex/showdown-katex') + ]) + + showdownHighlight = highlight.default + showdownKatex = katex.default +}` + ) + return contents + } + } +} + +const replaceStaticImports = { + name: 'replace-optional-markdown-imports', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + let contents = await readFile(args.path, 'utf8') + + const fileKey = args.path.split('EHTML/')[1] + + if (dynamicImportsByFiles[fileKey]) { + contents = dynamicImportsByFiles[fileKey](contents) + } + + return { + contents, + loader: 'js', + } + }) + } +} + +const baseDir = path.resolve('./src') + +async function readDirRecursive(dir) { + const result = [] + + for (const entry of await readdir(dir)) { + const fullPath = path.join(dir, entry) + const stats = await stat(fullPath) + + if (stats.isDirectory()) { + result.push(...await readDirRecursive(fullPath)) + } else { + result.push(fullPath) + } + } + + return result +} + +const alias = {} + +const files = await glob('**/*.js', { cwd: baseDir }) + +for (const file of files) { + const importPath = 'ehtml/' + file + .replace(/\\/g, '/') + .replace(/\.min\.js$/, '') + .replace(/\.js$/, '') + const filePath = path.join(baseDir, file) + alias[importPath] = filePath +} + +await build({ + entryPoints: ['./src/main.js'], + bundle: true, + minify: true, + format: 'esm', + outfile: 'dist/ehtml.min.js', + alias, + minify: false, + sourcemap: true, + define: { + '_EHTML_MODE_': '"NORMAL"' + }, + plugins: [replaceStaticImports] +}) + +await build({ + entryPoints: ['./src/main.js'], + bundle: true, + minify: true, + format: 'esm', + outfile: 'dist/ehtml.light.min.js', + alias, + minify: false, + sourcemap: true, + external: [ + './src/third-party/he.js', + './src/third-party/highlight.min.js', + './src/third-party/katex/auto-render.js', + './src/third-party/katex/katex.min.js', + './src/third-party/showdown-highlight.js', + './src/third-party/showdown-katex/asciimath-to-tex.min.js', + './src/third-party/showdown-katex/showdown-katex.js', + ], + define: { + '_EHTML_MODE_': '"LIGHT"' + }, + plugins: [replaceStaticImports] +}) + +await copyFile('dist/ehtml.min.js', './examples/static/js/ehtml.min.js') +await copyFile('dist/ehtml.min.js.map', './examples/static/js/ehtml.min.js.map') +await copyFile('dist/ehtml.light.min.js', './examples/static/js/ehtml.light.min.js') +await copyFile('dist/ehtml.light.min.js.map', './examples/static/js/ehtml.light.min.js.map') diff --git a/dist/ehtml.light.min.js b/dist/ehtml.light.min.js new file mode 100644 index 00000000..8b077e8d --- /dev/null +++ b/dist/ehtml.light.min.js @@ -0,0 +1,3580 @@ +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// src/responseFromAjaxRequest.js +var responseFromAjaxRequest_default = (options, requestBody, callback) => { + let resObj = {}; + const req = new XMLHttpRequest(); + req.open(options.method, options.url, true, options.user || null, options.password || null); + req.withCredentials = options.withCredentials || false; + req.timeout = options.timeout || 0; + if (options.downloadResponseBodyAsFileWithName) { + req.responseType = "blob"; + } + if (options.overrideMimeType !== void 0) { + req.overrideMimeType(options.overrideMimeType); + } + let headers = options.headers || {}; + for (let headerName in headers) { + req.setRequestHeader(headerName, headers[headerName]); + } + req.onreadystatechange = function() { + if (req.readyState === req.DONE) { + let allHeadersStr = req.getAllResponseHeaders().trim(); + let headerMap = {}; + let headers2 = allHeadersStr.split(/[\r\n]+/); + headers2.forEach((line) => { + let parts = line.split(/:\s*/); + let header = parts.shift(); + let value = parts.join(": "); + headerMap[header] = value; + }); + resObj.statusCode = req.status; + resObj.headers = headerMap; + resObj.body = req.response; + if (options.downloadResponseBodyAsFileWithName) { + if (resObj.statusCode === 200) { + const fileURL = window.URL.createObjectURL(resObj.body); + const anchor = document.createElement("a"); + anchor.href = fileURL; + anchor.download = options.downloadResponseBodyAsFileWithName; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + callback(null, resObj); + } else { + req.response.text().then((value) => { + try { + resObj.body = JSON.parse(value); + } catch (error) { + resObj.body = value; + } + callback(null, resObj); + }); + } + } else { + callback(null, resObj); + } + } + }; + if (options.progressEvent) { + req.addEventListener("progress", options.progressEvent); + } + if (options.loadStartEvent) { + req.addEventListener("loadstart", options.loadStartEvent); + } + if (options.loadEndEvent) { + req.addEventListener("loadend", options.loadEndEvent); + } + if (options.uploadProgressEvent) { + req.upload.addEventListener("progress", options.uploadProgressEvent); + } + if (options.uploadStartEvent) { + req.upload.addEventListener("loadstart", options.loadStartEvent); + } + if (options.uploadEndEvent) { + req.upload.addEventListener("loadend", options.loadEndEvent); + } + req.send(requestBody); +}; + +// src/evaluatedStringWithParamsFromState.js +var pattern = /\${([^}]+)}/g; +function evaluatedStringWithParamsFromState_default(string, state, node) { + if (!string) { + return null; + } + state = state || {}; + return string.replace(pattern, (match, expression) => { + const inlinedExpression = expression.replace(/\n/g, " "); + const func = new Function("state", "thisElement", ` + with (state) { + return (${inlinedExpression}); + } + `); + const evaluationResult = func(state, node); + if (typeof evaluationResult === "object") { + return JSON.stringify(evaluationResult); + } + return evaluationResult; + }); +} + +// src/evaluateStringWithActionsOnProgress.js +function evaluateStringWithActionsOnProgress_default(string, node) { + const func = new Function( + "thisElement", + ` + (() => { + ${string} + })() + ` + ); + func(node); +} + +// src/unwrappedChildrenOfParent.js +function unwrappedChildrenOfParent_default(parent) { + const docFrag = document.createDocumentFragment(); + while (parent.firstChild) { + const child = parent.removeChild(parent.firstChild); + docFrag.appendChild(child); + } + parent.parentNode.replaceChild(docFrag, parent); + return docFrag; +} + +// src/actions/scrollToHash.js +function scrollToHash() { + if (window.location.hash.length > 1) { + const hashElm = document.getElementById(window.location.hash.split("#")[1]); + if (hashElm) { + hashElm.scrollIntoView({ behaviour: "smooth" }); + } + } +} +window.scrollToHash = scrollToHash; + +// src/E/e-html.js +var e_html_default = (node) => { + if (node.hasAttribute("data-actions-on-progress-start")) { + evaluateStringWithActionsOnProgress_default( + node.getAttribute("data-actions-on-progress-start"), + node + ); + } + if (!node.hasAttribute("data-src")) { + throw new Error('e-html must have "data-src" attribute'); + } + responseFromAjaxRequest_default({ + url: encodeURI( + evaluatedStringWithParamsFromState_default( + node.getAttribute("data-src"), + node.__ehtmlState__, + node + ) + ), + method: "GET", + headers: JSON.parse( + evaluatedStringWithParamsFromState_default( + node.getAttribute("data-request-headers"), + node.__ehtmlState__, + node + ) || "{}" + ) + }, void 0, (err, resObj) => { + if (err) { + throw err; + } + const responseBody = resObj.body; + node.innerHTML = responseBody; + unwrappedChildrenOfParent_default(node); + if (node.hasAttribute("data-actions-on-progress-end")) { + evaluateStringWithActionsOnProgress_default( + node.getAttribute("data-actions-on-progress-end"), + node + ); + } + scrollToHash(); + }); +}; + +// src/evaluateStringWithActionsOnResponse.js +function evaluateStringWithActionsOnResponse_default(string, resName, resObj, node) { + const dynamicFunctionBody = ` + const thisElement = node + const ${resName} = resObj + ${string} + `; + const func = new Function("node", "resObj", dynamicFunctionBody); + func(node, resObj); +} + +// src/E/e-json.js +var e_json_default = (node) => { + const ajaxIconSelector = node.getAttribute("data-ajax-icon"); + const ajaxIcon = document.querySelector(ajaxIconSelector); + if (ajaxIcon) { + ajaxIcon.style.display = ""; + } + const socketName = node.getAttribute("data-socket"); + if (socketName) { + if (!window.__ehtmlState__["webSockets"] || !window.__ehtmlState__["webSockets"][socketName]) { + throw new Error(`socket with name "${socketName}" is not defined or not opened yet`); + } + const socket = window.__ehtmlState__["webSockets"][socketName]; + socket.addEventListener("message", (event) => { + const response = JSON.parse(event.data); + evaluateStringWithActionsOnResponse_default( + node.getAttribute("data-actions-on-response"), + node.getAttribute("data-response-name"), + response, + node + ); + }); + unwrappedChildrenOfParent_default(node); + return; + } + const cacheFromAttribute = node.getAttribute("data-cache-from"); + if (cacheFromAttribute) { + const evaluatedCacheAsString = evaluatedStringWithParamsFromState_default( + cacheFromAttribute, + node.__ehtmlState__, + node + ); + if (evaluatedCacheAsString !== "undefined" && evaluatedCacheAsString !== "null") { + const cacheObj = JSON.parse(evaluatedCacheAsString); + if (cacheObj) { + evaluateStringWithActionsOnResponse_default( + node.getAttribute("data-actions-on-response"), + node.getAttribute("data-response-name"), + cacheObj, + node + ); + unwrappedChildrenOfParent_default(node); + scrollToHash(); + return; + } + } + } + const progressBarSelector = node.getAttribute("data-progress-bar"); + const progressBar = document.querySelector(progressBarSelector); + if (progressBar) { + progressBar.max = 100; + progressBar.value = 0; + progressBar.style.display = "none"; + } + if (node.hasAttribute("data-actions-on-progress-start")) { + evaluateStringWithActionsOnProgress_default( + node.getAttribute("data-actions-on-progress-start"), + node + ); + } + if (!node.hasAttribute("data-src")) { + throw new Error(`e-json must have "data-src" attribute if it's not connected to a socket`); + } + responseFromAjaxRequest_default({ + url: encodeURI( + evaluatedStringWithParamsFromState_default( + node.getAttribute("data-src"), + node.__ehtmlState__, + node + ) + ), + method: "GET", + headers: JSON.parse( + evaluatedStringWithParamsFromState_default( + node.getAttribute("data-request-headers") || "{}", + node.__ehtmlState__, + node + ) + ), + progressEvent: (event) => { + if (progressBar) { + if (event.lengthComputable) { + progressBar.style.display = ""; + const percentComplete = parseInt(event.loaded / event.total * 100); + progressBar.value = percentComplete; + if (progressBar.value === 100) { + progressBar.style.display = "none"; + } + } + } + } + }, void 0, (err, resObj) => { + if (err) { + throw err; + } + if (ajaxIcon) { + ajaxIcon.style.display = "none"; + } + const responseBodyAsBuffer = resObj.body; + const responseBodyAsObject = JSON.parse( + responseBodyAsBuffer.toString("utf-8", 0, responseBodyAsBuffer.length) + ); + evaluateStringWithActionsOnResponse_default( + node.getAttribute("data-actions-on-response"), + node.getAttribute("data-response-name"), + { + body: responseBodyAsObject, + statusCode: resObj.statusCode, + headers: resObj.headers + }, + node + ); + unwrappedChildrenOfParent_default(node); + if (node.hasAttribute("data-actions-on-progress-end")) { + evaluateStringWithActionsOnProgress_default( + node.getAttribute("data-actions-on-progress-end"), + node + ); + } + scrollToHash(); + }); +}; + +// src/elm.js +var elm_default = (elmSelectorOrElm) => { + if (typeof elmSelectorOrElm === "string") { + return document.querySelector(elmSelectorOrElm); + } + return elmSelectorOrElm; +}; + +// src/isTemplate.js +function isTemplate_default(node) { + return node.nodeName.toLowerCase() === "template"; +} + +// src/isTemplateWithType.js +function isTemplateWithType_default(node, type) { + if (isTemplate_default(node)) { + const templateType = node.getAttribute("is"); + if (templateType) { + return templateType === type; + } + return false; + } + return false; +} + +// src/evaluatedStringWithParams.js +var pattern2 = /\${([^}]+)}/g; +function evaluatedStringWithParams_default(string, node) { + if (!string) { + return null; + } + return string.replace(pattern2, (match, expression) => { + const inlinedExpression = expression.replace(/\n/g, " "); + const func = new Function("thisElement", `return (${inlinedExpression});`); + const evaluationResult = func(node); + if (typeof evaluationResult === "object") { + return JSON.stringify(evaluationResult); + } + return evaluationResult; + }); +} + +// src/observeNodeAttributes.js +var ATTRIBUTE_NAMES_TO_IGNORE_SINCE_THEY_MUST_BE_RESOLVED_IN_THEIR_OWN_SCOPE_AND_TIME = [ + "data-actions-on-response", + "data-list-to-iterate", + "data-item-name", + "data-bound-to", + "data-cache-from", + "data-src", + "data-request-headers", + "data-request-url", + "data-socket" +]; +var TAGS_WITH_SRC_ATTRIBUTE = [ + "audio", + "embed", + "iframe", + "img", + "input", + "script", + "source", + "track", + "video", + "midi-player" +]; +function observeNodeAttributes_default(node, state) { + if (node.attributes) { + const nodeAttributes = Array.from(node.attributes); + for (let i = 0; i < nodeAttributes.length; i++) { + const attr = nodeAttributes[i]; + const isAttributeToBeIgnored = ATTRIBUTE_NAMES_TO_IGNORE_SINCE_THEY_MUST_BE_RESOLVED_IN_THEIR_OWN_SCOPE_AND_TIME.indexOf(attr.name) >= 0 || attr.name === "data-src" && TAGS_WITH_SRC_ATTRIBUTE.indexOf(node.tagName.toLowerCase()) === -1; + const isAttributeWithParams = /\$\{([^${}]+)\}/gm.test(attr.value); + const isAttributeToBeObserved = !isAttributeToBeIgnored && isAttributeWithParams; + if (isAttributeToBeIgnored && isAttributeWithParams) { + node.__ehtmlState__ = state; + } + if (!isAttributeToBeObserved) { + continue; + } + node.setAttribute( + attr.name, + state ? evaluatedStringWithParamsFromState_default(attr.value, state, node) : evaluatedStringWithParams_default(attr.value, node) + ); + if (attr.name === "data-text") { + const textNode = document.createTextNode( + attr.value + ); + if (node.childNodes.length === 0) { + node.appendChild(textNode); + } else { + node.insertBefore(textNode, node.childNodes[0]); + } + node.removeAttribute("data-text"); + continue; + } + if (attr.name === "data-value") { + node.value = attr.value; + node.removeAttribute("data-value"); + continue; + } + if (attr.name === "data-src" && TAGS_WITH_SRC_ATTRIBUTE.indexOf(node.tagName.toLowerCase()) !== -1) { + node.setAttribute("src", node.getAttribute("data-src")); + node.removeAttribute("data-src"); + continue; + } + if (attr.name === "data-inner-html") { + node.innerHTML = attr.value; + node.removeAttribute("data-inner-html"); + continue; + } + if (attr.name === "disabled" && attr.value === "false") { + node.removeAttribute("disabled"); + continue; + } + } + } +} + +// src/releaseTemplateWithItsContent.js +function releaseTemplateWithItsContent_default(template, contentNode) { + if (isTemplateWithType_default(template, "e-reusable")) { + if (template.hasAttribute("data-prepend-to")) { + const parentNode = document.querySelector(template.getAttribute("data-prepend-to")); + if (!parentNode) { + throw new Error('element is not found by the selector in the attribute "data-prepend-to"'); + } + parentNode.prepend(contentNode); + } else if (template.hasAttribute("data-append-to")) { + const parentNode = document.querySelector(template.getAttribute("data-append-to")); + if (!parentNode) { + throw new Error('element is not found by the selector in the attribute "data-append-to"'); + } + parentNode.append(contentNode); + } else if (template.hasAttribute("data-insert-into")) { + const parentNode = document.querySelector(template.getAttribute("data-insert-into")); + if (!parentNode) { + throw new Error('element is not found by the selector in the attribute "data-insert-into"'); + } + parentNode.innerHTML = ""; + parentNode.append(contentNode); + } else { + template.parentNode.insertBefore(contentNode, template); + } + } else { + template.parentNode.replaceChild(contentNode, template); + } +} + +// src/actions/mapToTemplate.js +function mapToTemplate(elmSelectorOrElm, obj) { + const mappingElement = elm_default(elmSelectorOrElm); + if (mappingElement === null || mappingElement === void 0) { + throw new Error("Mapping element is not found"); + } + if (!isTemplate_default(mappingElement)) { + throw new Error("Mapping element must be