From d0cd1d0dc97e83fae1da66cbe642cc587efbdb74 Mon Sep 17 00:00:00 2001 From: nihmadev Date: Tue, 28 Jul 2026 22:49:16 +0300 Subject: [PATCH] refactor: migrate renderer utilities to TypeScript --- assets/js/lib.d.ts | 27 + assets/js/lib.js | 1029 ++-------------------- assets/js/lib/dom.d.ts | 38 + assets/js/lib/dom.js | 219 +++++ assets/js/lib/encoding.d.ts | 4 + assets/js/lib/encoding.js | 11 + assets/js/lib/format.d.ts | 10 + assets/js/lib/format.js | 75 ++ assets/js/lib/history.d.ts | 24 + assets/js/lib/history.js | 37 + assets/js/lib/platform.d.ts | 22 + assets/js/lib/platform.js | 60 ++ assets/js/lib/runtimeErrors.d.ts | 13 + assets/js/lib/runtimeErrors.js | 54 ++ assets/js/lib/sandbox.d.ts | 14 + assets/js/lib/sandbox.js | 49 ++ assets/js/lib/text.d.ts | 14 + assets/js/lib/text.js | 51 ++ assets/js/lib/ui.d.ts | 2 + assets/js/lib/ui.js | 16 + assets/js/lib/validation.d.ts | 2 + assets/js/lib/validation.js | 11 + assets/js/lib/values.d.ts | 14 + assets/js/lib/values.js | 45 + assets/js/libClasses/EditorAdapter.js | 4 +- assets/js/libClasses/options.js | 2 +- assets/js/libClasses/topbarElement.js | 4 +- assets/ts/globals.d.ts | 26 + assets/ts/lib.ts | 116 +++ assets/ts/lib/dom.ts | 242 +++++ assets/ts/lib/encoding.ts | 12 + assets/ts/lib/format.ts | 78 ++ assets/ts/lib/history.ts | 61 ++ assets/ts/lib/platform.ts | 73 ++ assets/ts/lib/runtimeErrors.ts | 78 ++ assets/ts/lib/sandbox.ts | 61 ++ assets/ts/lib/text.ts | 61 ++ assets/ts/lib/ui.ts | 17 + assets/ts/lib/validation.ts | 9 + assets/ts/lib/values.ts | 42 + helpers/debuggerWindow/debuggerWindow.js | 4 +- package.json | 3 +- tests/unit/lib.test.mjs | 44 + tsconfig.renderer.json | 18 + 44 files changed, 1828 insertions(+), 968 deletions(-) create mode 100644 assets/js/lib.d.ts create mode 100644 assets/js/lib/dom.d.ts create mode 100644 assets/js/lib/dom.js create mode 100644 assets/js/lib/encoding.d.ts create mode 100644 assets/js/lib/encoding.js create mode 100644 assets/js/lib/format.d.ts create mode 100644 assets/js/lib/format.js create mode 100644 assets/js/lib/history.d.ts create mode 100644 assets/js/lib/history.js create mode 100644 assets/js/lib/platform.d.ts create mode 100644 assets/js/lib/platform.js create mode 100644 assets/js/lib/runtimeErrors.d.ts create mode 100644 assets/js/lib/runtimeErrors.js create mode 100644 assets/js/lib/sandbox.d.ts create mode 100644 assets/js/lib/sandbox.js create mode 100644 assets/js/lib/text.d.ts create mode 100644 assets/js/lib/text.js create mode 100644 assets/js/lib/ui.d.ts create mode 100644 assets/js/lib/ui.js create mode 100644 assets/js/lib/validation.d.ts create mode 100644 assets/js/lib/validation.js create mode 100644 assets/js/lib/values.d.ts create mode 100644 assets/js/lib/values.js create mode 100644 assets/ts/globals.d.ts create mode 100644 assets/ts/lib.ts create mode 100644 assets/ts/lib/dom.ts create mode 100644 assets/ts/lib/encoding.ts create mode 100644 assets/ts/lib/format.ts create mode 100644 assets/ts/lib/history.ts create mode 100644 assets/ts/lib/platform.ts create mode 100644 assets/ts/lib/runtimeErrors.ts create mode 100644 assets/ts/lib/sandbox.ts create mode 100644 assets/ts/lib/text.ts create mode 100644 assets/ts/lib/ui.ts create mode 100644 assets/ts/lib/validation.ts create mode 100644 assets/ts/lib/values.ts create mode 100644 tests/unit/lib.test.mjs create mode 100644 tsconfig.renderer.json diff --git a/assets/js/lib.d.ts b/assets/js/lib.d.ts new file mode 100644 index 0000000..1396e7a --- /dev/null +++ b/assets/js/lib.d.ts @@ -0,0 +1,27 @@ +/** Shared mutable renderer state retained for extension compatibility. */ +export declare const GLOBAL: Record; +export declare const Languages: any; +export declare const Filenames: any; +export declare const Dirs: any; +export declare const DragDrop: any; +export declare const Notificator: any; +export declare const TopBarElement: any; +export declare const SideBarIconManager: any; +export declare const Options: any; +export declare const ContextMenuLoader: any; +export declare const Loader: any; +export declare const GLS: any; +export declare const CodeTemplates: any; +export declare const EditorAdapter: any; +export declare const Task: any; +export declare const GetOrgAvatar: any; +export { idify, toBase64 } from "./lib/encoding.js"; +export { SmoothScroll, animate, changeTagName, fitAceHeight, generateAvatar, getAllCSSVariables, handleOnWheelScrollX, handlePopups, hideCodeWindowVisuals, scrollToBottomSmooth, setTabName, setTabNameCounter, showCodeWindowVisuals, showIndicator, tabName, } from "./lib/dom.js"; +export { formatUnix, getCodeByName, normalizePath, secondsToMinutes, transparentColor, } from "./lib/format.js"; +export { addToBug, addToHistory } from "./lib/history.js"; +export { copyText, createNotify, getGithubToken, getTheme, parseTwemojiElement, parseTwemojiString, setAppTitle, } from "./lib/platform.js"; +export { addRuntimeError, clearRuntimeErrors } from "./lib/runtimeErrors.js"; +export { runCode, runSandbox } from "./lib/sandbox.js"; +export { capitilize, dedent, escapeHtml, getInitials, linkify, splitCamelCase, truncateString, } from "./lib/text.js"; +export { showNeedReloadTopBar } from "./lib/ui.js"; +export { eventLog, isArray, isFloat, isObject, isStringifiedObject, type } from "./lib/values.js"; diff --git a/assets/js/lib.js b/assets/js/lib.js index 200e6ad..6e8e412 100644 --- a/assets/js/lib.js +++ b/assets/js/lib.js @@ -1,961 +1,68 @@ -import { ErrorReporter } from "./ErrorReporter.js" -import { BottomWindow } from "./handlers/BottomWindowHandler.js" - -import { _Languages } from "./libClasses/languages.js" -import { _Dirs } from "./libClasses/dirs.js" -import { _Notificator } from "./libClasses/notificator.js" -import { _DragDrop } from "./libClasses/dragndrop.js" -import { _TopBarElement } from "./libClasses/topbarElement.js" -import { _SideBarIconManager } from "./libClasses/sidebarIconManager.js" -import { _Options } from "./libClasses/options.js" -import { _ContextMenuLoader } from "./libClasses/contextMenuLoader.js" -import { _Loader } from "./libClasses/loader.js" -import { valid } from "./modalsHandler/engine.js" -import { createDIV, createIcon } from "./modalsHandler/handlers/helpers.js" -import { _GLS } from "./libClasses/gls.js" -import { _Filenames } from "./libClasses/fillenames.js" -import { _CodeTemplates } from "./libClasses/codeTemplates.js" -import { _EditorAdapter } from "./libClasses/EditorAdapter.js" -import { _GetOrgAvatar } from "./libClasses/avatar.js" -import { _Task } from "./libClasses/task.js" - -let runtimeErrors = [] -let runtimeErrorsCount = 0 - -export const GLOBAL = {} - -export const Languages = _Languages -export const Filenames = _Filenames -export const Dirs = _Dirs -export const DragDrop = _DragDrop -export const Notificator = _Notificator -export const TopBarElement = _TopBarElement -export const SideBarIconManager = _SideBarIconManager -export const Options = _Options -export const ContextMenuLoader = _ContextMenuLoader -export const Loader = _Loader -export const GLS = _GLS -export const CodeTemplates = _CodeTemplates -export const EditorAdapter = _EditorAdapter -export const Task = _Task - -export const GetOrgAvatar = _GetOrgAvatar - -// Language: adds a image icons -const imageIcons = ["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "ico", "avif", "tif", "tiff", "heic", "heif"] - -imageIcons.forEach(id => { - Languages.add( - { - id: id, - name: "Image", - icon: "image", - iconExt: "svg", - mode: "text" - } - ) -}) - -// Language: adds a font icons -const fontIcons = ["ttf", "otf", "woff", "woff2", "eot"] - -fontIcons.forEach(id => { - Languages.add( - { - id: id, - name: "Font", - icon: "font", - iconExt: "svg", - mode: "text" - } - ) -}) - -export const tabName = document.querySelector("#tab-name"); - -export function setTabNameCounter(count) { - if (!tabName) return; - const old = tabName.querySelector(".counter"); - - if (count === false) { - if (old) old.remove(); - return; - } - - if (old) old.remove(); - tabName.insertAdjacentHTML("beforeend", `${count}`); -} -export function setTabName(text) { - if (!tabName) return; - tabName.innerHTML = text -} - -export function toBase64(str) { - return btoa(unescape(encodeURIComponent(str))); -} -export function escapeHtml(s) { - return String(s) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} -export function getCodeByName(name, raw = false) { - const ext = (name || "").toLowerCase(); - - const names = { - html: "html", - js: "javascript", - css: "css", - json: "json", - md: "markdown", - todo: "markdown", - ps: "prettyscript", - py: "python", - php: "php" - }; - const rawNames = { - html: "html", - js: "javascript", - ps: "prettyscript", - py: "python", - md: "markdown", - css: "css", - php: "php" - }; - - if (!raw) return names[ext] || "plaintext"; - return rawNames[ext] || "text"; -} -async function getFileIconByName(name) { - // fileicon - file name - // ext - extenstion of icon, default: svg - let ext = "svg" - - let alts = { - ps: { - ext: "svg", - fileicon: "prettyscript" - }, - todo: { - ext: "svg", - fileicon: "todo" - }, - ttc: { - ext: "svg", - fileicon: "ttf" - }, - otf: { - ext: "svg", - fileicon: "ttf" - }, - woff: { - ext: "svg", - fileicon: "ttf" - } - } - - if (name in alts) { - ext = alts[name].ext - name = alts[name].fileicon - } - else { - name = getCodeByName(name).replaceAll(/[0-9]/g, "") - } - - let list = await window.electron.getAllIcons(); - let listNames = [] - - list.forEach(e => { - let filteredName = e.name.replaceAll("." + e.name.split(".").pop(), "") - - if (filteredName != "default") { - listNames.push(e.name.replaceAll("." + e.name.split(".").pop(), "")) - } - }) - - if (listNames.includes(name)) { - return `./assets/media/icons/symbols/files/${name}.${ext}` - } - else { - return `./assets/media/icons/symbols/files/document.svg` - } -} - -let inputs = document.querySelectorAll("input") - -if (inputs.length > 0) { - inputs.forEach(input => { - input.addEventListener("input", () => { - if (input.value > 0) { - input.classList.add("focused") - } - else { - input.classList.remove("focused") - } - }) - }) -} - -export function capitilize(text) { - return String(text).charAt(0).toUpperCase() + String(text).slice(1) -} - -export function addToHistory({ id, actionType, value, desc, today }) { - const historyID = id != undefined ? id : Object.keys(historyObject).length + 1 - const historyValue = value != undefined ? value : "Untitled" - const historyDesc = desc != undefined ? desc : "No description provided" - const historyToday = today != undefined ? today : new Date().format("H:i") - - historyObject[historyID] = { - time: historyToday, - action: actionType, - value: historyValue, - description: historyDesc - }; -} - -export function addToBug({ id, priority, value, desc, today, isSelf, org, resolved, author, assignedTo, type }) { - const bugID = id != undefined ? id : Object.keys(bugsObject).length + 1 - const bugPriority = (priority != undefined && !Number.isNaN(priority)) ? priority : 0 - const bugDesc = desc != undefined ? desc : "No description provided" - const bugToday = today != undefined ? today : new Date().format("H:i") - const bugIsSelf = isSelf != undefined ? isSelf : false - const bugResolved = resolved != undefined ? resolved : false - const bugAuthor = author != undefined ? author : false - const bugAssignedTo = assignedTo != undefined ? assignedTo : {} - - const priorityInfo = priorityClasses[String(bugPriority)] || priorityClasses["0"] - addToHistory({ actionType: "bug-added", value: value, desc: `Bug "${value}" added with ${priorityInfo.name} priority` }); - - bugsObject[bugID] = { - id: bugID, - time: bugToday, - priority: bugPriority, - value: value, - description: bugDesc, - self: bugIsSelf, - organization: org, - resolved: bugResolved, - author: bugAuthor, - assignedTo: bugAssignedTo, - type: type - }; - - return bugsObject -} - -export function showIndicator(time = 1500, callback) { - let statusIndicator = document.querySelector(".status-indicator") - - statusIndicator.classList.remove("hidden") - - if (typeof callback === "function") { - callback(statusIndicator) - } - - statusIndicator.addEventListener("transitionend", () => { - setTimeout(() => { - statusIndicator.classList.add("hidden") - }, time) - }, { once: true }) -} - -export const animate = { - blurReplace: ({ add, remove }) => { - if (!add || !remove) return; - - add.classList.remove("hidden", "blur-hidden"); - remove.classList.remove("hidden", "blur-hidden"); - - remove.classList.add("blur-hidden"); - - const onTransitionEnd = () => { - remove.classList.add("hidden"); - remove.removeEventListener("transitionend", onTransitionEnd); - - add.classList.remove("hidden"); - add.classList.add("blur-hidden"); - - requestAnimationFrame(() => { - add.classList.remove("blur-hidden"); - }); - }; - - remove.addEventListener("transitionend", onTransitionEnd); - } -}; - -export function handlePopups() { - const popups = document.querySelectorAll("[popup]"); - - document.addEventListener("click", (e) => { - let clickedInsidePopup = false; - - popups.forEach(popup => { - const popupContent = popup.querySelector(".popup-content"); - const popupTitle = popup.querySelector(".popup-title"); - - if (popupTitle.contains(e.target)) { - e.stopPropagation(); - - const isOpen = !popupContent.classList.contains("hidden"); - - popups.forEach(p => p.querySelector(".popup-content").classList.add("hidden")); - popups.forEach(p => p.querySelector(".popup-title").classList.remove("active")); - - if (!isOpen) { - popupContent.classList.remove("hidden"); - popupTitle.classList.add("active") - } - - clickedInsidePopup = true; - } - - if (e.target.closest(".popup-content__item") && popup.contains(e.target)) { - popupContent.classList.add("hidden"); - popupTitle.classList.remove("active") - clickedInsidePopup = true; - } - - if (popup.contains(e.target)) { - clickedInsidePopup = true; - } - }); - - if (!clickedInsidePopup) { - popups.forEach(p => { p.querySelector(".popup-content").classList.add("hidden"); p.querySelector(".popup-title").classList.remove("active") }); - } - }); -} - -export function SmoothScroll(target, speed, smooth) { - if (target === document) - target = (document.scrollingElement - || document.documentElement - || document.body.parentNode - || document.body) // cross browser support for document scrolling - - var moving = false - var pos = target.scrollTop - var frame = target === document.body - && document.documentElement - ? document.documentElement - : target // safari is the new IE - - target.addEventListener('mousewheel', scrolled, { passive: false }) - target.addEventListener('DOMMouseScroll', scrolled, { passive: false }) - - function scrolled(e) { - e.preventDefault(); // disable default scrolling - - var delta = normalizeWheelDelta(e) - - pos += -delta * speed - pos = Math.max(0, Math.min(pos, target.scrollHeight - frame.clientHeight)) // limit scrolling - - if (!moving) update() - } - - function normalizeWheelDelta(e) { - if (e.detail) { - if (e.wheelDelta) - return e.wheelDelta / e.detail / 40 * (e.detail > 0 ? 1 : -1) // Opera - else - return -e.detail / 3 // Firefox - } else - return e.wheelDelta / 120 // IE,Safari,Chrome - } - - function update() { - moving = true - - var delta = (pos - target.scrollTop) / smooth - - target.scrollTop += delta - - if (Math.abs(delta) > 0.5) - requestFrame(update) - else - moving = false - } - - var requestFrame = function () { - return ( - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function (func) { - window.setTimeout(func, 1000 / 50); - } - ); - }() -} - -export function runSandbox(code) { - const logs = [] - - function getPos() { - const stack = new Error().stack.split("\n")[3] - const match = stack.match(/:(\d+):(\d+)/) - - if (match) { - return { - line: Number(match[1]), - col: Number(match[2]) - } - } - - return {} - } - - function formatArgs(args) { - return args.map(arg => { - if (typeof arg === "object" && arg !== null) { - try { - return JSON.stringify(arg) - } catch { - return "[Object]" - } - } - return arg - }) - } - - const consoleProxy = { - log: (...args) => { - const pos = getPos() - - logs.push({ - type: "log", - line: pos.line, - col: pos.col, - args: formatArgs(args) - }) - }, - - warn: (...args) => { - const pos = getPos() - - logs.push({ - type: "warn", - line: pos.line, - col: pos.col, - args: formatArgs(args) - }) - }, - - error: (...args) => { - const pos = getPos() - - logs.push({ - type: "error", - line: pos.line, - col: pos.col, - args: formatArgs(args) - }) - } - } - - try { - const fn = new Function( - "console", - code + "\n//# sourceURL=sandbox.js" - ) - - fn(consoleProxy) - } catch (e) { - logs.push({ - type: "error", - args: [e.message] - }) - } - - return logs -} -export function runCode(code, acorn) { - try { - // syntax check - acorn.parse(code, { - ecmaVersion: "latest", - locations: true, - sourceType: "module" - }) - } catch (err) { - return ErrorReporter.fromAcorn(err) - } - - try { - // sandbox run check - runSandbox(code) - } catch (err) { - return ErrorReporter.fromRuntime(err) - } - - return null -} -export function addRuntimeError({ msg, line = null, col = null, time = null, isNull = false, win = null }) { - const exists = runtimeErrors.some( - e => e.msg === msg && e.line === line && e.col === col - ) - - const wrapper = BottomWindow.get("errorsHistory") - const badge = document.querySelector("#runtimeErrors .badge") - const el = document.createElement("div") - const items = document.querySelectorAll(".runtime-item#runTimeErrorItem") - const lastItem = items[items.length - 1] - - if (isNull && lastItem?.classList.contains("success")) return - if (exists) return - - const error = { msg, line, col } - - if (!isNull) { - runtimeErrors.push(error) - runtimeErrorsCount += 1 - } - else { - runtimeErrors = [] - badge.classList.add("hidden") - } - - badge.classList.remove("hidden") - badge.textContent = runtimeErrors.length - - if (runtimeErrors.length == 0) { - badge.classList.add("hidden") - } - - items.forEach(e => { e.classList.add("prev") }) - - el.classList.add("runtime-item", "bottom-window__item") - el.id = "runTimeErrorItem" - - if (!isNull) { - el.innerHTML = ` - error - ${msg} - ${line !== null ? `${line}:${col ?? 0}` : ""} - ${time !== null ? `${formatUnix(time)}` : ""} - ` - } - else { - el.classList.add("success") - el.innerHTML = ` - check_circle - All errors fixed - ${runtimeErrorsCount > 0 ? `(${runtimeErrorsCount})` : ""} - ${time !== null ? `${formatUnix(time)}` : ""} - ` - runtimeErrorsCount = 0 - } - - wrapper.add(el) - - wrapper.win.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' }); -} -export function clearRuntimeErrors() { - runtimeErrors = [] - runtimeErrorsCount = 0 - - addRuntimeError( - { - isNull: true, - time: Math.floor(Date.now() / 1000) - } - ) -} - -export function formatUnix(ts, format = "{dd}.{mm}.{yyyy}, {hh}:{ii}:{ss}") { - const date = new Date(ts * 1000); - - const yyyy = String(date.getFullYear()); - const dd = String(date.getDate()).padStart(2, "0"); - const mm = String(date.getMonth() + 1).padStart(2, "0"); - - const hh = String(date.getHours()).padStart(2, "0"); - const ii = String(date.getMinutes()).padStart(2, "0"); - const ss = String(date.getSeconds()).padStart(2, "0"); - - if(format) { - return format - .replaceAll("{dd}", dd) - .replaceAll("{mm}", mm) - .replaceAll("{hh}", hh) - .replaceAll("{ii}", ii) - .replaceAll("{ss}", ss) - .replaceAll("{yyyy}", yyyy) - } - else { - return `${dd}.${mm}, ${hh}:${ii}:${ss}`; - } -} - -export function getInitials(name) { - if (!name) return 'A'; - const words = name.trim().split(/\s+/); - return words.slice(0, 2).map(w => w[0].toUpperCase()).join(''); -} - -export function generateAvatar(name) { - function stringToColorPair(input) { - let hash = 0; - for (let i = 0; i < input.length; i++) { - hash = input.charCodeAt(i) + ((hash << 5) - hash); - } - const hue = Math.abs(hash) % 360; - const saturation = 60; - const fgLightness = 30; - const bgLightness = 90; - - const hslToHex = (h, s, l) => { - s /= 100; - l /= 100; - const k = n => (n + h / 30) % 12; - const a = s * Math.min(l, 1 - l); - const f = n => - Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))))) - .toString(16) - .padStart(2, '0'); - return `#${f(0)}${f(8)}${f(4)}`; - }; - - return { - foreground: hslToHex(hue, saturation, fgLightness), - background: hslToHex(hue, saturation, bgLightness), - background_second: hslToHex(hue, saturation, bgLightness - 10) - }; - } - - let initials = getInitials(name) - let color = stringToColorPair(name) - - const generated = document.createElement("div") - generated.classList.add("generated-avatar") - generated.style.cssText = `--background: ${color.background};--background-second: ${color.background_second};--foreground: ${color.foreground};` - generated.textContent = initials - - return generated.outerHTML -} - -export function isFloat(n) { - return typeof n === 'number' && !Number.isInteger(n); -} - -export function isStringifiedObject(str) { - try { - const parsed = JSON.parse(str); - - if (Array.isArray(parsed)) return "array" - if (typeof parsed === 'object' && parsed !== null) { - return "object" - } - return null - } catch (e) { - return false; - } -} - -export function truncateString(str, maxLength) { - if (str.length <= maxLength) { - return str; - } - - return str.slice(0, maxLength) + '...'; -} - -export function createNotify(properties = {}) { - const timeDefault = 3000 - - const type = valid(properties.type) ?? "info_i" - const icon = valid(properties.icon) ?? "info_i" - const title = valid(properties.title) ?? "Untitled" - const content = valid(properties.content) ?? "No description provided" - let time = valid(properties.time) ?? 3000 - const image = valid(properties.image) ?? false - - if(time < timeDefault) { - time = timeDefault - } - if(time > 10000) { - time = timeDefault - } - - const notifyObject = { - title: title, - description: content, - timeout: time - } - - if(icon) notifyObject["icon"] = icon - if(type) notifyObject["type"] = type - if(image) notifyObject["image"] = image - - window.electron.createNotification(notifyObject) -} - -export function getTheme() { - return document.body.getAttribute("theme") != null ? document.body.getAttribute("theme") : "default" -} - -export function handleOnWheelScrollX() { - const elements = document.querySelectorAll(".code-tabs, .commands .commands-suggest") - - elements.forEach(el => { - el.addEventListener("wheel", (event) => { - event.preventDefault() - - el.scrollBy({ - left: event.deltaY / 5 - }) - }, { passive: false }) - }) -} - -export function idify(string) { - const bytes = new TextEncoder().encode(string); - let binary = ""; - - bytes.forEach(b => binary += String.fromCharCode(b)); - - return btoa(binary).replaceAll("=", ""); -} - -export function linkify(text) { - return text.replace( - /(https?:\/\/[^\s<]+)/g, - '$1' - ); -} - -export function splitCamelCase(str) { - return str - .replace(/([a-z])([A-Z])/g, "$1 $2") - .split(" ") - .map((w, i) => i === 0 - ? w.charAt(0).toUpperCase() + w.slice(1) - : w.toLowerCase() - ) -} - -export function copyText(text) { - navigator.clipboard.writeText(text).then(() => { - console.log("Text copied to clipboard successfully!"); - }).catch(err => { - console.error('Failed to copy text: ', err); - }); -} - -export function normalizePath(path) { - return path - .replaceAll("\\", "/") - .replaceAll(/\\/g, "/") -} - -export function parseTwemojiString(text) { - return twemoji.parse(text, { - folder: "svg", - ext: ".svg" - }) -} - -export function parseTwemojiElement(element) { - if (!element) return - - twemoji.parse(element, { - folder: "svg", - ext: ".svg" - }) -} - -export function scrollToBottomSmooth(el) { - el.scrollTo({ - top: el.scrollHeight, - behavior: "smooth" - }); -} - -export function getAllCSSVariables() { - return Array.from(document.styleSheets) - .filter( - sheet => - sheet.href === null || sheet.href.startsWith(window.location.origin) - ) - .reduce( - (acc, sheet) => - (acc = [ - ...acc, - ...Array.from(sheet.cssRules).reduce( - (def, rule) => - (def = - rule.selectorText === ":root" - ? [ - ...def, - ...Array.from(rule.style).filter(name => - name.startsWith("--") - ) - ] - : def), - [] - ) - ]), - [] - ); -} - -export function type(value) { - const str = value.toString().trim() - - if (/^-?\d+$/.test(str)) return "int" - if (/^-?\d*\.\d+$/.test(str)) return "float" - if (/^(true|false)$/.test(str)) return "boolean" - if (/^\[.*\]$/.test(str)) return "array" - if (/^\{.*\}$/.test(str)) return "object" - - return "string" -} - -export function eventLog(...args) { - console.warn(`[EVENT LOG] -----------\n`, ...args) -} - -const CODE_WINDOW_VISUALS_TABS = document.querySelector(".code-tabs") -const CODE_WINDOW_VISUALS_FOOTER = document.querySelector(".code-footer") - -export function isObject(item) { - return typeof item == "object" && !Array.isArray(item) -} -export function isArray(item) { - return typeof item == "object" && Array.isArray(item) -} - -export function showCodeWindowVisuals() { - CODE_WINDOW_VISUALS_TABS.classList.remove("hidden") - CODE_WINDOW_VISUALS_FOOTER.classList.remove("hidden") -} -export function hideCodeWindowVisuals() { - CODE_WINDOW_VISUALS_TABS.classList.add("hidden") - CODE_WINDOW_VISUALS_FOOTER.classList.add("hidden") -} - -export function changeTagName(oldElement, newTagName) { - const newElement = document.createElement(newTagName); - - for (const attr of oldElement.attributes) { - newElement.setAttribute(attr.name, attr.value); - } - - while (oldElement.firstChild) { - newElement.appendChild(oldElement.firstChild); - } - - oldElement.replaceWith(newElement); -} - -export function showNeedReloadTopBar() { - const needToReloadTopBar = new TopBarElement("needReload") - needToReloadTopBar.content({ icon: "cached", text: "You need to reload application", type: "danger" }) - - setTimeout(() => { - needToReloadTopBar.show() - - setTimeout(() => { - needToReloadTopBar.hide({ iconVisible: true }) - }, 3000) - }, 1000) - - needToReloadTopBar.on("hover", (instance) => { instance.show() }) - needToReloadTopBar.on("unhover", (instance) => { instance.hide({ iconVisible: true }) }) -} - -export function secondsToMinutes(seconds) { - return seconds / 60; -} - -export function transparentColor(color, alpha = 1) { - alpha = Math.max(0, Math.min(1, alpha)); - color = color.trim(); - - if (color.startsWith('#')) { - let hex = color.slice(1); - - if (hex.length === 3) { - hex = hex.split('').map(c => c + c).join(''); - } - - if (hex.length !== 6) { - throw new Error('Invalid HEX color'); - } - - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - - return `rgba(${r}, ${g}, ${b}, ${alpha})`; - } - - const match = color.match( - /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/i - ); - - if (match) { - const [, r, g, b] = match; - - return `rgba(${r}, ${g}, ${b}, ${alpha})`; - } - - throw new Error('Unsupported color format'); -} -export function dedent(str) { - const lines = str.replace(/^\n/, "").split("\n"); - - const indent = Math.min( - ...lines - .filter(line => line.trim()) - .map(line => line.match(/^ */)[0].length) - ); - - return lines - .map(line => line.slice(indent)) - .join("\n") - .trimEnd(); -} -export function fitAceHeight(editor, minHeight = 50, maxHeight = 800) { - const lines = editor.session.getLength(); - const lineHeight = editor.renderer.lineHeight; - - const height = Math.min( - maxHeight, - Math.max(minHeight, lines * lineHeight) - ); - - editor.container.style.height = height + "px"; - editor.resize(); -} -export function setAppTitle(title) { - window.electron.setAppTitle(title) -} - -export async function getGithubToken() { - const localData = await window.electron.getLocal() - - if ( - localData && - typeof localData === "object" && - typeof localData.githubToken === "string" && - localData.githubToken.length > 0 - ) { - return localData.githubToken - } - - return false -} - -window.Notificator = Notificator -window.addToBug = addToBug -window.addToHistory = addToHistory -window.showIndicator = showIndicator -window.animate = animate \ No newline at end of file +import { _GetOrgAvatar } from "./libClasses/avatar.js"; +import { _CodeTemplates } from "./libClasses/codeTemplates.js"; +import { _ContextMenuLoader } from "./libClasses/contextMenuLoader.js"; +import { _Dirs } from "./libClasses/dirs.js"; +import { _DragDrop } from "./libClasses/dragndrop.js"; +import { _EditorAdapter } from "./libClasses/EditorAdapter.js"; +import { _Filenames } from "./libClasses/fillenames.js"; +import { _GLS } from "./libClasses/gls.js"; +import { _Languages } from "./libClasses/languages.js"; +import { _Loader } from "./libClasses/loader.js"; +import { _Notificator } from "./libClasses/notificator.js"; +import { _Options } from "./libClasses/options.js"; +import { _SideBarIconManager } from "./libClasses/sidebarIconManager.js"; +import { _Task } from "./libClasses/task.js"; +import { _TopBarElement } from "./libClasses/topbarElement.js"; +import { animate, initializeInputFocusState, showIndicator } from "./lib/dom.js"; +import { addToBug, addToHistory } from "./lib/history.js"; +/** Shared mutable renderer state retained for extension compatibility. */ +export const GLOBAL = {}; +export const Languages = _Languages; +export const Filenames = _Filenames; +export const Dirs = _Dirs; +export const DragDrop = _DragDrop; +export const Notificator = _Notificator; +export const TopBarElement = _TopBarElement; +export const SideBarIconManager = _SideBarIconManager; +export const Options = _Options; +export const ContextMenuLoader = _ContextMenuLoader; +export const Loader = _Loader; +export const GLS = _GLS; +export const CodeTemplates = _CodeTemplates; +export const EditorAdapter = _EditorAdapter; +export const Task = _Task; +export const GetOrgAvatar = _GetOrgAvatar; +export { idify, toBase64 } from "./lib/encoding.js"; +export { SmoothScroll, animate, changeTagName, fitAceHeight, generateAvatar, getAllCSSVariables, handleOnWheelScrollX, handlePopups, hideCodeWindowVisuals, scrollToBottomSmooth, setTabName, setTabNameCounter, showCodeWindowVisuals, showIndicator, tabName, } from "./lib/dom.js"; +export { formatUnix, getCodeByName, normalizePath, secondsToMinutes, transparentColor, } from "./lib/format.js"; +export { addToBug, addToHistory } from "./lib/history.js"; +export { copyText, createNotify, getGithubToken, getTheme, parseTwemojiElement, parseTwemojiString, setAppTitle, } from "./lib/platform.js"; +export { addRuntimeError, clearRuntimeErrors } from "./lib/runtimeErrors.js"; +export { runCode, runSandbox } from "./lib/sandbox.js"; +export { capitilize, dedent, escapeHtml, getInitials, linkify, splitCamelCase, truncateString, } from "./lib/text.js"; +export { showNeedReloadTopBar } from "./lib/ui.js"; +export { eventLog, isArray, isFloat, isObject, isStringifiedObject, type } from "./lib/values.js"; +const imageIcons = [ + "png", + "jpg", + "jpeg", + "gif", + "webp", + "bmp", + "svg", + "ico", + "avif", + "tif", + "tiff", + "heic", + "heif", +]; +const fontIcons = ["ttf", "otf", "woff", "woff2", "eot"]; +imageIcons.forEach((id) => Languages.add({ id, name: "Image", icon: "image", iconExt: "svg", mode: "text" })); +fontIcons.forEach((id) => Languages.add({ id, name: "Font", icon: "font", iconExt: "svg", mode: "text" })); +initializeInputFocusState(); +window.Notificator = Notificator; +window.addToBug = addToBug; +window.addToHistory = addToHistory; +window.showIndicator = showIndicator; +window.animate = animate; diff --git a/assets/js/lib/dom.d.ts b/assets/js/lib/dom.d.ts new file mode 100644 index 0000000..7ace3a9 --- /dev/null +++ b/assets/js/lib/dom.d.ts @@ -0,0 +1,38 @@ +/** The tab title element captured when the compatibility module is evaluated. */ +export declare const tabName: HTMLElement | null; +/** Updates or removes the counter displayed beside the tab title. */ +export declare function setTabNameCounter(count: string | number | false): void; +/** Replaces the current tab title markup. */ +export declare function setTabName(text: string): void; +/** Shows the status indicator and schedules it to hide after its transition. */ +export declare function showIndicator(time?: number, callback?: (indicator: Element) => void): void; +/** Transition helpers shared by legacy UI components. */ +export declare const animate: { + /** Replaces one element with another using the existing blur transition. */ + blurReplace({ add, remove }: { + add?: Element; + remove?: Element; + }): void; +}; +/** Installs the document-level popup interaction handler. */ +export declare function handlePopups(): void; +/** Adds animated wheel scrolling to an element or the document. */ +export declare function SmoothScroll(target: Document | HTMLElement, speed: number, smooth: number): void; +/** Generates the HTML for a deterministic initials avatar. */ +export declare function generateAvatar(name: string): string; +/** Enables horizontal wheel scrolling for tab and command rows. */ +export declare function handleOnWheelScrollX(): void; +/** Smoothly scrolls an element to its bottom edge. */ +export declare function scrollToBottomSmooth(element: HTMLElement): void; +/** Lists custom property names declared by accessible `:root` rules. */ +export declare function getAllCSSVariables(): string[]; +/** Displays the code tabs and footer. */ +export declare function showCodeWindowVisuals(): void; +/** Hides the code tabs and footer. */ +export declare function hideCodeWindowVisuals(): void; +/** Replaces an element while retaining its attributes and child nodes. */ +export declare function changeTagName(oldElement: Element, newTagName: string): Element; +/** Fits an Ace editor to its current line count within the given bounds. */ +export declare function fitAceHeight(editor: any, minHeight?: number, maxHeight?: number): void; +/** Adds focused styling to inputs present when the module loads. */ +export declare function initializeInputFocusState(): void; diff --git a/assets/js/lib/dom.js b/assets/js/lib/dom.js new file mode 100644 index 0000000..016eb46 --- /dev/null +++ b/assets/js/lib/dom.js @@ -0,0 +1,219 @@ +import { getInitials } from "./text.js"; +/** The tab title element captured when the compatibility module is evaluated. */ +export const tabName = document.querySelector("#tab-name"); +/** Updates or removes the counter displayed beside the tab title. */ +export function setTabNameCounter(count) { + if (!tabName) + return; + tabName.querySelector(".counter")?.remove(); + if (count !== false) { + tabName.insertAdjacentHTML("beforeend", `${count}`); + } +} +/** Replaces the current tab title markup. */ +export function setTabName(text) { + if (tabName) + tabName.innerHTML = text; +} +/** Shows the status indicator and schedules it to hide after its transition. */ +export function showIndicator(time = 1500, callback) { + const indicator = document.querySelector(".status-indicator"); + if (!indicator) + return; + indicator.classList.remove("hidden"); + callback?.(indicator); + indicator.addEventListener("transitionend", () => setTimeout(() => indicator.classList.add("hidden"), time), { once: true }); +} +/** Transition helpers shared by legacy UI components. */ +export const animate = { + /** Replaces one element with another using the existing blur transition. */ + blurReplace({ add, remove }) { + if (!(add && remove)) + return; + add.classList.remove("hidden", "blur-hidden"); + remove.classList.remove("hidden", "blur-hidden"); + remove.classList.add("blur-hidden"); + const onTransitionEnd = () => { + remove.classList.add("hidden"); + remove.removeEventListener("transitionend", onTransitionEnd); + add.classList.remove("hidden"); + add.classList.add("blur-hidden"); + requestAnimationFrame(() => add.classList.remove("blur-hidden")); + }; + remove.addEventListener("transitionend", onTransitionEnd); + }, +}; +/** Installs the document-level popup interaction handler. */ +export function handlePopups() { + const popups = document.querySelectorAll("[popup]"); + document.addEventListener("click", (event) => { + const target = event.target; + if (!(target instanceof Element)) + return; + let clickedInsidePopup = false; + popups.forEach((popup) => { + const content = popup.querySelector(".popup-content"); + const title = popup.querySelector(".popup-title"); + if (!(content && title)) + return; + if (title.contains(target)) { + event.stopPropagation(); + const isOpen = !content.classList.contains("hidden"); + popups.forEach((item) => { + item.querySelector(".popup-content")?.classList.add("hidden"); + item.querySelector(".popup-title")?.classList.remove("active"); + }); + if (!isOpen) { + content.classList.remove("hidden"); + title.classList.add("active"); + } + clickedInsidePopup = true; + } + if (target.closest(".popup-content__item") && popup.contains(target)) { + content.classList.add("hidden"); + title.classList.remove("active"); + clickedInsidePopup = true; + } + if (popup.contains(target)) + clickedInsidePopup = true; + }); + if (!clickedInsidePopup) { + popups.forEach((popup) => { + popup.querySelector(".popup-content")?.classList.add("hidden"); + popup.querySelector(".popup-title")?.classList.remove("active"); + }); + } + }); +} +/** Adds animated wheel scrolling to an element or the document. */ +export function SmoothScroll(target, speed, smooth) { + let scrollTarget; + if (target === document) { + scrollTarget = (document.scrollingElement || + document.documentElement || + document.body); + } + else { + scrollTarget = target; + } + let moving = false; + let position = scrollTarget.scrollTop; + const frame = scrollTarget === document.body && document.documentElement + ? document.documentElement + : scrollTarget; + const update = () => { + moving = true; + const delta = (position - scrollTarget.scrollTop) / smooth; + scrollTarget.scrollTop += delta; + if (Math.abs(delta) > 0.5) + requestFrame(update); + else + moving = false; + }; + const scrolled = (event) => { + event.preventDefault(); + const wheel = event; + const delta = wheel.detail + ? wheel.wheelDelta + ? (wheel.wheelDelta / wheel.detail / 40) * (wheel.detail > 0 ? 1 : -1) + : -wheel.detail / 3 + : (wheel.wheelDelta ?? -wheel.deltaY) / 120; + position += -delta * speed; + position = Math.max(0, Math.min(position, scrollTarget.scrollHeight - frame.clientHeight)); + if (!moving) + update(); + }; + const requestFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + ((callback) => window.setTimeout(callback, 1000 / 50)); + scrollTarget.addEventListener("mousewheel", scrolled, { passive: false }); + scrollTarget.addEventListener("DOMMouseScroll", scrolled, { passive: false }); +} +/** Generates the HTML for a deterministic initials avatar. */ +export function generateAvatar(name) { + let hash = 0; + for (let index = 0; index < name.length; index++) { + hash = name.charCodeAt(index) + ((hash << 5) - hash); + } + const hue = Math.abs(hash) % 360; + const hslToHex = (lightness) => { + const saturation = 0.6; + const normalizedLightness = lightness / 100; + const k = (part) => (part + hue / 30) % 12; + const a = saturation * Math.min(normalizedLightness, 1 - normalizedLightness); + const channel = (part) => Math.round(255 * + (normalizedLightness - + a * Math.max(-1, Math.min(k(part) - 3, Math.min(9 - k(part), 1))))) + .toString(16) + .padStart(2, "0"); + return `#${channel(0)}${channel(8)}${channel(4)}`; + }; + const generated = document.createElement("div"); + generated.classList.add("generated-avatar"); + generated.style.cssText = `--background: ${hslToHex(90)};--background-second: ${hslToHex(80)};--foreground: ${hslToHex(30)};`; + generated.textContent = getInitials(name); + return generated.outerHTML; +} +/** Enables horizontal wheel scrolling for tab and command rows. */ +export function handleOnWheelScrollX() { + document + .querySelectorAll(".code-tabs, .commands .commands-suggest") + .forEach((el) => { + el.addEventListener("wheel", (event) => { + event.preventDefault(); + el.scrollBy({ left: event.deltaY / 5 }); + }, { passive: false }); + }); +} +/** Smoothly scrolls an element to its bottom edge. */ +export function scrollToBottomSmooth(element) { + element.scrollTo({ top: element.scrollHeight, behavior: "smooth" }); +} +/** Lists custom property names declared by accessible `:root` rules. */ +export function getAllCSSVariables() { + return Array.from(document.styleSheets) + .filter((sheet) => sheet.href === null || sheet.href.startsWith(window.location.origin)) + .flatMap((sheet) => Array.from(sheet.cssRules)) + .filter((rule) => "selectorText" in rule && rule.selectorText === ":root") + .flatMap((rule) => Array.from(rule.style).filter((name) => name.startsWith("--"))); +} +const codeTabs = document.querySelector(".code-tabs"); +const codeFooter = document.querySelector(".code-footer"); +/** Displays the code tabs and footer. */ +export function showCodeWindowVisuals() { + codeTabs?.classList.remove("hidden"); + codeFooter?.classList.remove("hidden"); +} +/** Hides the code tabs and footer. */ +export function hideCodeWindowVisuals() { + codeTabs?.classList.add("hidden"); + codeFooter?.classList.add("hidden"); +} +/** Replaces an element while retaining its attributes and child nodes. */ +export function changeTagName(oldElement, newTagName) { + const newElement = document.createElement(newTagName); + for (const attribute of oldElement.attributes) { + newElement.setAttribute(attribute.name, attribute.value); + } + while (oldElement.firstChild) + newElement.appendChild(oldElement.firstChild); + oldElement.replaceWith(newElement); + return newElement; +} +/** Fits an Ace editor to its current line count within the given bounds. */ +export function fitAceHeight(editor, minHeight = 50, maxHeight = 800) { + const height = Math.min(maxHeight, Math.max(minHeight, editor.session.getLength() * editor.renderer.lineHeight)); + editor.container.style.height = height + "px"; + editor.resize(); +} +/** Adds focused styling to inputs present when the module loads. */ +export function initializeInputFocusState() { + document.querySelectorAll("input").forEach((input) => { + input.addEventListener("input", () => { + input.classList.toggle("focused", Number(input.value) > 0); + }); + }); +} diff --git a/assets/js/lib/encoding.d.ts b/assets/js/lib/encoding.d.ts new file mode 100644 index 0000000..fa4214c --- /dev/null +++ b/assets/js/lib/encoding.d.ts @@ -0,0 +1,4 @@ +/** Encodes a UTF-8 string as base64. */ +export declare function toBase64(value: string): string; +/** Produces the base64 identifier format used by existing DOM elements. */ +export declare function idify(value: string): string; diff --git a/assets/js/lib/encoding.js b/assets/js/lib/encoding.js new file mode 100644 index 0000000..5b543e0 --- /dev/null +++ b/assets/js/lib/encoding.js @@ -0,0 +1,11 @@ +/** Encodes a UTF-8 string as base64. */ +export function toBase64(value) { + return btoa(unescape(encodeURIComponent(value))); +} +/** Produces the base64 identifier format used by existing DOM elements. */ +export function idify(value) { + const bytes = new TextEncoder().encode(value); + let binary = ""; + bytes.forEach((byte) => (binary += String.fromCharCode(byte))); + return btoa(binary).replaceAll("=", ""); +} diff --git a/assets/js/lib/format.d.ts b/assets/js/lib/format.d.ts new file mode 100644 index 0000000..4806abe --- /dev/null +++ b/assets/js/lib/format.d.ts @@ -0,0 +1,10 @@ +/** Resolves an extension to the editor mode used by CodeMotion. */ +export declare function getCodeByName(name: string | null | undefined, raw?: boolean): string; +/** Formats a Unix timestamp with the placeholders supported by the legacy helper. */ +export declare function formatUnix(timestamp: number, format?: string): string; +/** Converts seconds to minutes without rounding. */ +export declare function secondsToMinutes(seconds: number): number; +/** Normalizes Windows separators to forward slashes. */ +export declare function normalizePath(path: string): string; +/** Applies an alpha channel to a HEX, RGB, or RGBA color. */ +export declare function transparentColor(color: string, alpha?: number): string; diff --git a/assets/js/lib/format.js b/assets/js/lib/format.js new file mode 100644 index 0000000..d3cace6 --- /dev/null +++ b/assets/js/lib/format.js @@ -0,0 +1,75 @@ +/** Resolves an extension to the editor mode used by CodeMotion. */ +export function getCodeByName(name, raw = false) { + const extension = (name || "").toLowerCase(); + const names = { + html: "html", + js: "javascript", + css: "css", + json: "json", + md: "markdown", + todo: "markdown", + ps: "prettyscript", + py: "python", + php: "php", + }; + const rawNames = { + html: "html", + js: "javascript", + ps: "prettyscript", + py: "python", + md: "markdown", + css: "css", + php: "php", + }; + return raw ? rawNames[extension] || "text" : names[extension] || "plaintext"; +} +/** Formats a Unix timestamp with the placeholders supported by the legacy helper. */ +export function formatUnix(timestamp, format = "{dd}.{mm}.{yyyy}, {hh}:{ii}:{ss}") { + const date = new Date(timestamp * 1000); + const yyyy = String(date.getFullYear()); + const dd = String(date.getDate()).padStart(2, "0"); + const mm = String(date.getMonth() + 1).padStart(2, "0"); + const hh = String(date.getHours()).padStart(2, "0"); + const ii = String(date.getMinutes()).padStart(2, "0"); + const ss = String(date.getSeconds()).padStart(2, "0"); + if (!format) + return `${dd}.${mm}, ${hh}:${ii}:${ss}`; + return format + .replaceAll("{dd}", dd) + .replaceAll("{mm}", mm) + .replaceAll("{hh}", hh) + .replaceAll("{ii}", ii) + .replaceAll("{ss}", ss) + .replaceAll("{yyyy}", yyyy); +} +/** Converts seconds to minutes without rounding. */ +export function secondsToMinutes(seconds) { + return seconds / 60; +} +/** Normalizes Windows separators to forward slashes. */ +export function normalizePath(path) { + return path.replaceAll("\\", "/").replaceAll(/\\/g, "/"); +} +/** Applies an alpha channel to a HEX, RGB, or RGBA color. */ +export function transparentColor(color, alpha = 1) { + const normalizedAlpha = Math.max(0, Math.min(1, alpha)); + const normalizedColor = color.trim(); + if (normalizedColor.startsWith("#")) { + let hex = normalizedColor.slice(1); + if (hex.length === 3) + hex = hex + .split("") + .map((part) => part + part) + .join(""); + if (hex.length !== 6) + throw new Error("Invalid HEX color"); + const red = Number.parseInt(hex.slice(0, 2), 16); + const green = Number.parseInt(hex.slice(2, 4), 16); + const blue = Number.parseInt(hex.slice(4, 6), 16); + return `rgba(${red}, ${green}, ${blue}, ${normalizedAlpha})`; + } + const match = normalizedColor.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/i); + if (match) + return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${normalizedAlpha})`; + throw new Error("Unsupported color format"); +} diff --git a/assets/js/lib/history.d.ts b/assets/js/lib/history.d.ts new file mode 100644 index 0000000..8a850d5 --- /dev/null +++ b/assets/js/lib/history.d.ts @@ -0,0 +1,24 @@ +export interface HistoryProperties { + id?: string | number; + actionType?: unknown; + value?: unknown; + desc?: unknown; + today?: unknown; +} +export interface BugProperties { + id?: string | number; + priority?: number; + value?: unknown; + desc?: unknown; + today?: unknown; + isSelf?: boolean; + org?: unknown; + resolved?: boolean; + author?: unknown; + assignedTo?: unknown; + type?: unknown; +} +/** Appends an entry to the legacy global history store. */ +export declare function addToHistory({ id, actionType, value, desc, today }: HistoryProperties): void; +/** Adds a bug and records the corresponding history event. */ +export declare function addToBug(properties: BugProperties): Record; diff --git a/assets/js/lib/history.js b/assets/js/lib/history.js new file mode 100644 index 0000000..c11c41f --- /dev/null +++ b/assets/js/lib/history.js @@ -0,0 +1,37 @@ +/** Appends an entry to the legacy global history store. */ +export function addToHistory({ id, actionType, value, desc, today }) { + const historyId = id === undefined ? Object.keys(historyObject).length + 1 : id; + historyObject[historyId] = { + time: today === undefined ? new Date().format("H:i") : today, + action: actionType, + value: value === undefined ? "Untitled" : value, + description: desc === undefined ? "No description provided" : desc, + }; +} +/** Adds a bug and records the corresponding history event. */ +export function addToBug(properties) { + const bugId = properties.id ?? Object.keys(bugsObject).length + 1; + const priority = properties.priority !== undefined && !Number.isNaN(properties.priority) + ? properties.priority + : 0; + const priorityInfo = priorityClasses[String(priority)] || priorityClasses["0"]; + addToHistory({ + actionType: "bug-added", + value: properties.value, + desc: `Bug "${properties.value}" added with ${priorityInfo.name} priority`, + }); + bugsObject[bugId] = { + id: bugId, + time: properties.today ?? new Date().format("H:i"), + priority, + value: properties.value, + description: properties.desc ?? "No description provided", + self: properties.isSelf ?? false, + organization: properties.org, + resolved: properties.resolved ?? false, + author: properties.author ?? false, + assignedTo: properties.assignedTo ?? {}, + type: properties.type, + }; + return bugsObject; +} diff --git a/assets/js/lib/platform.d.ts b/assets/js/lib/platform.d.ts new file mode 100644 index 0000000..5be13c6 --- /dev/null +++ b/assets/js/lib/platform.d.ts @@ -0,0 +1,22 @@ +export interface NotificationProperties { + type?: unknown; + icon?: unknown; + title?: unknown; + content?: unknown; + time?: unknown; + image?: unknown; +} +/** Creates a native notification through the Electron preload API. */ +export declare function createNotify(properties?: NotificationProperties): void; +/** Returns the current body theme or the default theme name. */ +export declare function getTheme(): string; +/** Copies text to the system clipboard and reports failures to the console. */ +export declare function copyText(text: string): void; +/** Converts emoji in a string to Twemoji SVG markup. */ +export declare function parseTwemojiString(text: string): string; +/** Converts emoji contained by an element to Twemoji SVG markup. */ +export declare function parseTwemojiElement(element?: Element | null): void; +/** Updates the native application title. */ +export declare function setAppTitle(title?: string): void; +/** Returns the persisted GitHub token or the historical `false` sentinel. */ +export declare function getGithubToken(): Promise; diff --git a/assets/js/lib/platform.js b/assets/js/lib/platform.js new file mode 100644 index 0000000..8a8fdaa --- /dev/null +++ b/assets/js/lib/platform.js @@ -0,0 +1,60 @@ +import { valid } from "./validation.js"; +/** Creates a native notification through the Electron preload API. */ +export function createNotify(properties = {}) { + const type = valid(properties.type) ?? "info_i"; + const icon = valid(properties.icon) ?? "info_i"; + const title = valid(properties.title) ?? "Untitled"; + const content = valid(properties.content) ?? "No description provided"; + let time = Number(valid(properties.time) ?? 3000); + const image = valid(properties.image) ?? false; + if (time < 3000 || time > 10_000) + time = 3000; + const notification = { + title, + description: content, + timeout: time, + }; + if (icon) + notification.icon = icon; + if (type) + notification.type = type; + if (image) + notification.image = image; + window.electron.createNotification(notification); +} +/** Returns the current body theme or the default theme name. */ +export function getTheme() { + return document.body.getAttribute("theme") ?? "default"; +} +/** Copies text to the system clipboard and reports failures to the console. */ +export function copyText(text) { + navigator.clipboard + .writeText(text) + .then(() => console.log("Text copied to clipboard successfully!")) + .catch((error) => console.error("Failed to copy text: ", error)); +} +/** Converts emoji in a string to Twemoji SVG markup. */ +export function parseTwemojiString(text) { + return twemoji.parse(text, { folder: "svg", ext: ".svg" }); +} +/** Converts emoji contained by an element to Twemoji SVG markup. */ +export function parseTwemojiElement(element) { + if (element) + twemoji.parse(element, { folder: "svg", ext: ".svg" }); +} +/** Updates the native application title. */ +export function setAppTitle(title) { + window.electron.setAppTitle(title); +} +/** Returns the persisted GitHub token or the historical `false` sentinel. */ +export async function getGithubToken() { + const localData = await window.electron.getLocal(); + if (localData && + typeof localData === "object" && + "githubToken" in localData && + typeof localData.githubToken === "string" && + localData.githubToken.length > 0) { + return localData.githubToken; + } + return false; +} diff --git a/assets/js/lib/runtimeErrors.d.ts b/assets/js/lib/runtimeErrors.d.ts new file mode 100644 index 0000000..8554f28 --- /dev/null +++ b/assets/js/lib/runtimeErrors.d.ts @@ -0,0 +1,13 @@ +interface RuntimeErrorProperties { + msg?: string; + line?: number | null; + col?: number | null; + time?: number | null; + isNull?: boolean; + win?: unknown; +} +/** Adds a unique runtime result to the errors history window. */ +export declare function addRuntimeError({ msg, line, col, time, isNull, }: RuntimeErrorProperties): void; +/** Clears runtime errors and records a successful check result. */ +export declare function clearRuntimeErrors(): void; +export {}; diff --git a/assets/js/lib/runtimeErrors.js b/assets/js/lib/runtimeErrors.js new file mode 100644 index 0000000..d5902db --- /dev/null +++ b/assets/js/lib/runtimeErrors.js @@ -0,0 +1,54 @@ +import { BottomWindow } from "../handlers/BottomWindowHandler.js"; +import { formatUnix } from "./format.js"; +let runtimeErrors = []; +let runtimeErrorsCount = 0; +/** Adds a unique runtime result to the errors history window. */ +export function addRuntimeError({ msg, line = null, col = null, time = null, isNull = false, }) { + const exists = runtimeErrors.some((error) => error.msg === msg && error.line === line && error.col === col); + const wrapper = BottomWindow.get("errorsHistory"); + const badge = document.querySelector("#runtimeErrors .badge"); + const items = document.querySelectorAll(".runtime-item#runTimeErrorItem"); + const lastItem = items[items.length - 1]; + if ((isNull && lastItem?.classList.contains("success")) || exists || !(wrapper && badge)) + return; + const element = document.createElement("div"); + if (isNull) { + runtimeErrors = []; + badge.classList.add("hidden"); + } + else { + runtimeErrors.push({ msg, line, col }); + runtimeErrorsCount += 1; + } + badge.classList.toggle("hidden", runtimeErrors.length === 0); + badge.textContent = String(runtimeErrors.length); + items.forEach((item) => item.classList.add("prev")); + element.classList.add("runtime-item", "bottom-window__item"); + element.id = "runTimeErrorItem"; + if (isNull) { + element.classList.add("success"); + element.innerHTML = ` + check_circle + All errors fixed + ${runtimeErrorsCount > 0 ? `(${runtimeErrorsCount})` : ""} + ${time === null ? "" : `${formatUnix(time)}`} + `; + runtimeErrorsCount = 0; + } + else { + element.innerHTML = ` + error + ${msg} + ${line === null ? "" : `${line}:${col ?? 0}`} + ${time === null ? "" : `${formatUnix(time)}`} + `; + } + wrapper.add(element); + wrapper.win.lastElementChild?.scrollIntoView({ behavior: "smooth", block: "end" }); +} +/** Clears runtime errors and records a successful check result. */ +export function clearRuntimeErrors() { + runtimeErrors = []; + runtimeErrorsCount = 0; + addRuntimeError({ isNull: true, time: Math.floor(Date.now() / 1000) }); +} diff --git a/assets/js/lib/sandbox.d.ts b/assets/js/lib/sandbox.d.ts new file mode 100644 index 0000000..bf69da3 --- /dev/null +++ b/assets/js/lib/sandbox.d.ts @@ -0,0 +1,14 @@ +export interface SandboxLog { + type: "log" | "warn" | "error"; + line?: number; + col?: number; + args: unknown[]; +} +/** Executes code with a captured console and returns its emitted records. */ +export declare function runSandbox(code: string): SandboxLog[]; +interface Parser { + parse(code: string, options: Record): unknown; +} +/** Performs the syntax and runtime checks expected by the editor. */ +export declare function runCode(code: string, parser: Parser): unknown; +export {}; diff --git a/assets/js/lib/sandbox.js b/assets/js/lib/sandbox.js new file mode 100644 index 0000000..cc3aaa1 --- /dev/null +++ b/assets/js/lib/sandbox.js @@ -0,0 +1,49 @@ +import { ErrorReporter } from "../../js/ErrorReporter.js"; +/** Executes code with a captured console and returns its emitted records. */ +export function runSandbox(code) { + const logs = []; + const getPosition = () => { + const stack = new Error().stack?.split("\n")[3] ?? ""; + const match = stack.match(/:(\d+):(\d+)/); + return match ? { line: Number(match[1]), col: Number(match[2]) } : {}; + }; + const formatArgs = (args) => args.map((arg) => { + if (typeof arg !== "object" || arg === null) + return arg; + try { + return JSON.stringify(arg); + } + catch { + return "[Object]"; + } + }); + const capture = (type) => (...args) => logs.push({ type, ...getPosition(), args: formatArgs(args) }); + const consoleProxy = { log: capture("log"), warn: capture("warn"), error: capture("error") }; + try { + const execute = new Function("console", code + "\n//# sourceURL=sandbox.js"); + execute(consoleProxy); + } + catch (error) { + logs.push({ + type: "error", + args: [error instanceof Error ? error.message : String(error)], + }); + } + return logs; +} +/** Performs the syntax and runtime checks expected by the editor. */ +export function runCode(code, parser) { + try { + parser.parse(code, { ecmaVersion: "latest", locations: true, sourceType: "module" }); + } + catch (error) { + return ErrorReporter.fromAcorn(error); + } + try { + runSandbox(code); + } + catch (error) { + return ErrorReporter.fromRuntime(error); + } + return null; +} diff --git a/assets/js/lib/text.d.ts b/assets/js/lib/text.d.ts new file mode 100644 index 0000000..b3bbd53 --- /dev/null +++ b/assets/js/lib/text.d.ts @@ -0,0 +1,14 @@ +/** Escapes a value for insertion into HTML markup. */ +export declare function escapeHtml(value: unknown): string; +/** Capitalizes the first character. The misspelled name is retained for API compatibility. */ +export declare function capitilize(text: unknown): string; +/** Returns up to two initials, or the historical `A` fallback. */ +export declare function getInitials(name: string): string; +/** Truncates a string and appends an ellipsis when needed. */ +export declare function truncateString(value: string, maxLength: number): string; +/** Converts HTTP links in plain text to safe external anchors. */ +export declare function linkify(text: string): string; +/** Splits a camel-cased identifier into display words. */ +export declare function splitCamelCase(value: string): string[]; +/** Removes the common leading indentation from a multiline string. */ +export declare function dedent(value: string): string; diff --git a/assets/js/lib/text.js b/assets/js/lib/text.js new file mode 100644 index 0000000..4071a73 --- /dev/null +++ b/assets/js/lib/text.js @@ -0,0 +1,51 @@ +/** Escapes a value for insertion into HTML markup. */ +export function escapeHtml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} +/** Capitalizes the first character. The misspelled name is retained for API compatibility. */ +export function capitilize(text) { + return String(text).charAt(0).toUpperCase() + String(text).slice(1); +} +/** Returns up to two initials, or the historical `A` fallback. */ +export function getInitials(name) { + if (!name) + return "A"; + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((word) => word[0].toUpperCase()) + .join(""); +} +/** Truncates a string and appends an ellipsis when needed. */ +export function truncateString(value, maxLength) { + return value.length <= maxLength ? value : value.slice(0, maxLength) + "..."; +} +/** Converts HTTP links in plain text to safe external anchors. */ +export function linkify(text) { + return text.replace(/(https?:\/\/[^\s<]+)/g, '$1'); +} +/** Splits a camel-cased identifier into display words. */ +export function splitCamelCase(value) { + return value + .replace(/([a-z])([A-Z])/g, "$1 $2") + .split(" ") + .map((word, index) => index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase()); +} +/** Removes the common leading indentation from a multiline string. */ +export function dedent(value) { + const lines = value.replace(/^\n/, "").split("\n"); + const indents = lines + .filter((line) => line.trim()) + .map((line) => line.match(/^ */)?.[0].length ?? 0); + const indent = Math.min(...indents); + return lines + .map((line) => line.slice(indent)) + .join("\n") + .trimEnd(); +} diff --git a/assets/js/lib/ui.d.ts b/assets/js/lib/ui.d.ts new file mode 100644 index 0000000..81279c6 --- /dev/null +++ b/assets/js/lib/ui.d.ts @@ -0,0 +1,2 @@ +/** Displays the persistent top-bar prompt requesting an application reload. */ +export declare function showNeedReloadTopBar(): void; diff --git a/assets/js/lib/ui.js b/assets/js/lib/ui.js new file mode 100644 index 0000000..82af8ed --- /dev/null +++ b/assets/js/lib/ui.js @@ -0,0 +1,16 @@ +import { _TopBarElement } from "../libClasses/topbarElement.js"; +/** Displays the persistent top-bar prompt requesting an application reload. */ +export function showNeedReloadTopBar() { + const prompt = new _TopBarElement("needReload"); + prompt.content({ + icon: "cached", + text: "You need to reload application", + type: "danger", + }); + setTimeout(() => { + prompt.show(); + setTimeout(() => prompt.hide({ iconVisible: true }), 3000); + }, 1000); + prompt.on("hover", (instance) => instance.show()); + prompt.on("unhover", (instance) => instance.hide({ iconVisible: true })); +} diff --git a/assets/js/lib/validation.d.ts b/assets/js/lib/validation.d.ts new file mode 100644 index 0000000..ad5ab74 --- /dev/null +++ b/assets/js/lib/validation.d.ts @@ -0,0 +1,2 @@ +/** Returns a value only when it contains meaningful notification data. */ +export declare function valid(value: T): T | undefined; diff --git a/assets/js/lib/validation.js b/assets/js/lib/validation.js new file mode 100644 index 0000000..d7139b7 --- /dev/null +++ b/assets/js/lib/validation.js @@ -0,0 +1,11 @@ +/** Returns a value only when it contains meaningful notification data. */ +export function valid(value) { + if (value === undefined || value === null || value === false) + return undefined; + if (Array.isArray(value) && value.length === 0) + return undefined; + if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) { + return undefined; + } + return value; +} diff --git a/assets/js/lib/values.d.ts b/assets/js/lib/values.d.ts new file mode 100644 index 0000000..7335f6d --- /dev/null +++ b/assets/js/lib/values.d.ts @@ -0,0 +1,14 @@ +/** Returns whether a value is a non-integer number. */ +export declare function isFloat(value: unknown): value is number; +/** Identifies JSON strings containing an array or object. */ +export declare function isStringifiedObject(value: string): "array" | "object" | null | false; +/** Infers the editor's historical scalar type name from a value. */ +export declare function type(value: { + toString(): string; +}): string; +/** Preserves the legacy object check, including its `null` behavior. */ +export declare function isObject(value: unknown): boolean; +/** Returns whether a value is an array. */ +export declare function isArray(value: unknown): value is unknown[]; +/** Writes a visually distinct event diagnostic. */ +export declare function eventLog(...args: unknown[]): void; diff --git a/assets/js/lib/values.js b/assets/js/lib/values.js new file mode 100644 index 0000000..02b5a6c --- /dev/null +++ b/assets/js/lib/values.js @@ -0,0 +1,45 @@ +/** Returns whether a value is a non-integer number. */ +export function isFloat(value) { + return typeof value === "number" && !Number.isInteger(value); +} +/** Identifies JSON strings containing an array or object. */ +export function isStringifiedObject(value) { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) + return "array"; + if (typeof parsed === "object" && parsed !== null) + return "object"; + return null; + } + catch { + return false; + } +} +/** Infers the editor's historical scalar type name from a value. */ +export function type(value) { + const text = value.toString().trim(); + if (/^-?\d+$/.test(text)) + return "int"; + if (/^-?\d*\.\d+$/.test(text)) + return "float"; + if (/^(true|false)$/.test(text)) + return "boolean"; + if (/^\[.*\]$/.test(text)) + return "array"; + if (/^\{.*\}$/.test(text)) + return "object"; + return "string"; +} +/** Preserves the legacy object check, including its `null` behavior. */ +export function isObject(value) { + return typeof value === "object" && !Array.isArray(value); +} +/** Returns whether a value is an array. */ +export function isArray(value) { + return typeof value === "object" && Array.isArray(value); +} +/** Writes a visually distinct event diagnostic. */ +export function eventLog(...args) { + console.warn("[EVENT LOG] -----------\n", ...args); +} diff --git a/assets/js/libClasses/EditorAdapter.js b/assets/js/libClasses/EditorAdapter.js index eb62575..f57ef20 100644 --- a/assets/js/libClasses/EditorAdapter.js +++ b/assets/js/libClasses/EditorAdapter.js @@ -1,5 +1,5 @@ -import { Languages } from "../lib.js"; import { fromJSONToTextMate } from "../../../app/dist-esm/textmate/compile.js" +import { _Languages as Languages } from "./languages.js" export class _EditorAdapter { constructor( @@ -410,4 +410,4 @@ export class _EditorAdapter { extends: rules.extends }) } -} \ No newline at end of file +} diff --git a/assets/js/libClasses/options.js b/assets/js/libClasses/options.js index d14ff97..1ba8c45 100644 --- a/assets/js/libClasses/options.js +++ b/assets/js/libClasses/options.js @@ -1,4 +1,4 @@ -import { transparentColor } from "../lib.js"; +import { transparentColor } from "../lib/format.js"; export class _Options { static instances = new Map(); diff --git a/assets/js/libClasses/topbarElement.js b/assets/js/libClasses/topbarElement.js index 7a2286f..2acd1b7 100644 --- a/assets/js/libClasses/topbarElement.js +++ b/assets/js/libClasses/topbarElement.js @@ -1,4 +1,4 @@ -import { idify } from "../lib.js" +import { idify } from "../lib/encoding.js" export class _TopBarElement { static instances = new Map() @@ -130,4 +130,4 @@ export class _TopBarElement { this.item.remove() _TopBarElement.instances.delete(this.item.id) } -} \ No newline at end of file +} diff --git a/assets/ts/globals.d.ts b/assets/ts/globals.d.ts new file mode 100644 index 0000000..2474234 --- /dev/null +++ b/assets/ts/globals.d.ts @@ -0,0 +1,26 @@ +declare module "*.js"; + +interface Date { + format(pattern: string): string; +} + +interface Window { + electron: Record any>; + CodeMirror: any; + Notificator: any; + addToBug: (...args: any[]) => any; + addToHistory: (...args: any[]) => any; + showIndicator: (...args: any[]) => any; + animate: any; + webkitRequestAnimationFrame?: typeof requestAnimationFrame; + mozRequestAnimationFrame?: typeof requestAnimationFrame; + oRequestAnimationFrame?: typeof requestAnimationFrame; + msRequestAnimationFrame?: typeof requestAnimationFrame; +} + +declare const historyObject: Record; +declare const bugsObject: Record; +declare const priorityClasses: Record; +declare const twemoji: { + parse(value: string | Element, options: { folder: string; ext: string }): string; +}; diff --git a/assets/ts/lib.ts b/assets/ts/lib.ts new file mode 100644 index 0000000..5aaa253 --- /dev/null +++ b/assets/ts/lib.ts @@ -0,0 +1,116 @@ +import { _GetOrgAvatar } from "./libClasses/avatar.js"; +import { _CodeTemplates } from "./libClasses/codeTemplates.js"; +import { _ContextMenuLoader } from "./libClasses/contextMenuLoader.js"; +import { _Dirs } from "./libClasses/dirs.js"; +import { _DragDrop } from "./libClasses/dragndrop.js"; +import { _EditorAdapter } from "./libClasses/EditorAdapter.js"; +import { _Filenames } from "./libClasses/fillenames.js"; +import { _GLS } from "./libClasses/gls.js"; +import { _Languages } from "./libClasses/languages.js"; +import { _Loader } from "./libClasses/loader.js"; +import { _Notificator } from "./libClasses/notificator.js"; +import { _Options } from "./libClasses/options.js"; +import { _SideBarIconManager } from "./libClasses/sidebarIconManager.js"; +import { _Task } from "./libClasses/task.js"; +import { _TopBarElement } from "./libClasses/topbarElement.js"; +import { animate, initializeInputFocusState, showIndicator } from "./lib/dom.js"; +import { addToBug, addToHistory } from "./lib/history.js"; + +/** Shared mutable renderer state retained for extension compatibility. */ +export const GLOBAL: Record = {}; + +export const Languages = _Languages; +export const Filenames = _Filenames; +export const Dirs = _Dirs; +export const DragDrop = _DragDrop; +export const Notificator = _Notificator; +export const TopBarElement = _TopBarElement; +export const SideBarIconManager = _SideBarIconManager; +export const Options = _Options; +export const ContextMenuLoader = _ContextMenuLoader; +export const Loader = _Loader; +export const GLS = _GLS; +export const CodeTemplates = _CodeTemplates; +export const EditorAdapter = _EditorAdapter; +export const Task = _Task; +export const GetOrgAvatar = _GetOrgAvatar; + +export { idify, toBase64 } from "./lib/encoding.js"; +export { + SmoothScroll, + animate, + changeTagName, + fitAceHeight, + generateAvatar, + getAllCSSVariables, + handleOnWheelScrollX, + handlePopups, + hideCodeWindowVisuals, + scrollToBottomSmooth, + setTabName, + setTabNameCounter, + showCodeWindowVisuals, + showIndicator, + tabName, +} from "./lib/dom.js"; +export { + formatUnix, + getCodeByName, + normalizePath, + secondsToMinutes, + transparentColor, +} from "./lib/format.js"; +export { addToBug, addToHistory } from "./lib/history.js"; +export { + copyText, + createNotify, + getGithubToken, + getTheme, + parseTwemojiElement, + parseTwemojiString, + setAppTitle, +} from "./lib/platform.js"; +export { addRuntimeError, clearRuntimeErrors } from "./lib/runtimeErrors.js"; +export { runCode, runSandbox } from "./lib/sandbox.js"; +export { + capitilize, + dedent, + escapeHtml, + getInitials, + linkify, + splitCamelCase, + truncateString, +} from "./lib/text.js"; +export { showNeedReloadTopBar } from "./lib/ui.js"; +export { eventLog, isArray, isFloat, isObject, isStringifiedObject, type } from "./lib/values.js"; + +const imageIcons = [ + "png", + "jpg", + "jpeg", + "gif", + "webp", + "bmp", + "svg", + "ico", + "avif", + "tif", + "tiff", + "heic", + "heif", +]; +const fontIcons = ["ttf", "otf", "woff", "woff2", "eot"]; + +imageIcons.forEach((id) => + Languages.add({ id, name: "Image", icon: "image", iconExt: "svg", mode: "text" }), +); +fontIcons.forEach((id) => + Languages.add({ id, name: "Font", icon: "font", iconExt: "svg", mode: "text" }), +); +initializeInputFocusState(); + +window.Notificator = Notificator; +window.addToBug = addToBug; +window.addToHistory = addToHistory; +window.showIndicator = showIndicator; +window.animate = animate; diff --git a/assets/ts/lib/dom.ts b/assets/ts/lib/dom.ts new file mode 100644 index 0000000..aef1fe2 --- /dev/null +++ b/assets/ts/lib/dom.ts @@ -0,0 +1,242 @@ +import { getInitials } from "./text.js"; + +/** The tab title element captured when the compatibility module is evaluated. */ +export const tabName = document.querySelector("#tab-name"); + +/** Updates or removes the counter displayed beside the tab title. */ +export function setTabNameCounter(count: string | number | false): void { + if (!tabName) return; + tabName.querySelector(".counter")?.remove(); + if (count !== false) { + tabName.insertAdjacentHTML("beforeend", `${count}`); + } +} + +/** Replaces the current tab title markup. */ +export function setTabName(text: string): void { + if (tabName) tabName.innerHTML = text; +} + +/** Shows the status indicator and schedules it to hide after its transition. */ +export function showIndicator(time = 1500, callback?: (indicator: Element) => void): void { + const indicator = document.querySelector(".status-indicator"); + if (!indicator) return; + indicator.classList.remove("hidden"); + callback?.(indicator); + indicator.addEventListener( + "transitionend", + () => setTimeout(() => indicator.classList.add("hidden"), time), + { once: true }, + ); +} + +/** Transition helpers shared by legacy UI components. */ +export const animate = { + /** Replaces one element with another using the existing blur transition. */ + blurReplace({ add, remove }: { add?: Element; remove?: Element }): void { + if (!(add && remove)) return; + add.classList.remove("hidden", "blur-hidden"); + remove.classList.remove("hidden", "blur-hidden"); + remove.classList.add("blur-hidden"); + const onTransitionEnd = () => { + remove.classList.add("hidden"); + remove.removeEventListener("transitionend", onTransitionEnd); + add.classList.remove("hidden"); + add.classList.add("blur-hidden"); + requestAnimationFrame(() => add.classList.remove("blur-hidden")); + }; + remove.addEventListener("transitionend", onTransitionEnd); + }, +}; + +/** Installs the document-level popup interaction handler. */ +export function handlePopups(): void { + const popups = document.querySelectorAll("[popup]"); + document.addEventListener("click", (event) => { + const target = event.target; + if (!(target instanceof Element)) return; + let clickedInsidePopup = false; + popups.forEach((popup) => { + const content = popup.querySelector(".popup-content"); + const title = popup.querySelector(".popup-title"); + if (!(content && title)) return; + if (title.contains(target)) { + event.stopPropagation(); + const isOpen = !content.classList.contains("hidden"); + popups.forEach((item) => { + item.querySelector(".popup-content")?.classList.add("hidden"); + item.querySelector(".popup-title")?.classList.remove("active"); + }); + if (!isOpen) { + content.classList.remove("hidden"); + title.classList.add("active"); + } + clickedInsidePopup = true; + } + if (target.closest(".popup-content__item") && popup.contains(target)) { + content.classList.add("hidden"); + title.classList.remove("active"); + clickedInsidePopup = true; + } + if (popup.contains(target)) clickedInsidePopup = true; + }); + if (!clickedInsidePopup) { + popups.forEach((popup) => { + popup.querySelector(".popup-content")?.classList.add("hidden"); + popup.querySelector(".popup-title")?.classList.remove("active"); + }); + } + }); +} + +/** Adds animated wheel scrolling to an element or the document. */ +export function SmoothScroll(target: Document | HTMLElement, speed: number, smooth: number): void { + let scrollTarget: HTMLElement; + if (target === document) { + scrollTarget = (document.scrollingElement || + document.documentElement || + document.body) as HTMLElement; + } else { + scrollTarget = target as HTMLElement; + } + let moving = false; + let position = scrollTarget.scrollTop; + const frame = + scrollTarget === document.body && document.documentElement + ? document.documentElement + : scrollTarget; + const update = () => { + moving = true; + const delta = (position - scrollTarget.scrollTop) / smooth; + scrollTarget.scrollTop += delta; + if (Math.abs(delta) > 0.5) requestFrame(update); + else moving = false; + }; + const scrolled = (event: Event) => { + event.preventDefault(); + const wheel = event as WheelEvent & { detail?: number; wheelDelta?: number }; + const delta = wheel.detail + ? wheel.wheelDelta + ? (wheel.wheelDelta / wheel.detail / 40) * (wheel.detail > 0 ? 1 : -1) + : -wheel.detail / 3 + : (wheel.wheelDelta ?? -wheel.deltaY) / 120; + position += -delta * speed; + position = Math.max(0, Math.min(position, scrollTarget.scrollHeight - frame.clientHeight)); + if (!moving) update(); + }; + const requestFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + ((callback: FrameRequestCallback) => window.setTimeout(callback, 1000 / 50)); + scrollTarget.addEventListener("mousewheel", scrolled, { passive: false }); + scrollTarget.addEventListener("DOMMouseScroll", scrolled, { passive: false }); +} + +/** Generates the HTML for a deterministic initials avatar. */ +export function generateAvatar(name: string): string { + let hash = 0; + for (let index = 0; index < name.length; index++) { + hash = name.charCodeAt(index) + ((hash << 5) - hash); + } + const hue = Math.abs(hash) % 360; + const hslToHex = (lightness: number) => { + const saturation = 0.6; + const normalizedLightness = lightness / 100; + const k = (part: number) => (part + hue / 30) % 12; + const a = saturation * Math.min(normalizedLightness, 1 - normalizedLightness); + const channel = (part: number) => + Math.round( + 255 * + (normalizedLightness - + a * Math.max(-1, Math.min(k(part) - 3, Math.min(9 - k(part), 1)))), + ) + .toString(16) + .padStart(2, "0"); + return `#${channel(0)}${channel(8)}${channel(4)}`; + }; + const generated = document.createElement("div"); + generated.classList.add("generated-avatar"); + generated.style.cssText = `--background: ${hslToHex(90)};--background-second: ${hslToHex(80)};--foreground: ${hslToHex(30)};`; + generated.textContent = getInitials(name); + return generated.outerHTML; +} + +/** Enables horizontal wheel scrolling for tab and command rows. */ +export function handleOnWheelScrollX(): void { + document + .querySelectorAll(".code-tabs, .commands .commands-suggest") + .forEach((el) => { + el.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + el.scrollBy({ left: event.deltaY / 5 }); + }, + { passive: false }, + ); + }); +} + +/** Smoothly scrolls an element to its bottom edge. */ +export function scrollToBottomSmooth(element: HTMLElement): void { + element.scrollTo({ top: element.scrollHeight, behavior: "smooth" }); +} + +/** Lists custom property names declared by accessible `:root` rules. */ +export function getAllCSSVariables(): string[] { + return Array.from(document.styleSheets) + .filter((sheet) => sheet.href === null || sheet.href.startsWith(window.location.origin)) + .flatMap((sheet) => Array.from(sheet.cssRules)) + .filter( + (rule): rule is CSSStyleRule => "selectorText" in rule && rule.selectorText === ":root", + ) + .flatMap((rule) => Array.from(rule.style).filter((name) => name.startsWith("--"))); +} + +const codeTabs = document.querySelector(".code-tabs"); +const codeFooter = document.querySelector(".code-footer"); + +/** Displays the code tabs and footer. */ +export function showCodeWindowVisuals(): void { + codeTabs?.classList.remove("hidden"); + codeFooter?.classList.remove("hidden"); +} + +/** Hides the code tabs and footer. */ +export function hideCodeWindowVisuals(): void { + codeTabs?.classList.add("hidden"); + codeFooter?.classList.add("hidden"); +} + +/** Replaces an element while retaining its attributes and child nodes. */ +export function changeTagName(oldElement: Element, newTagName: string): Element { + const newElement = document.createElement(newTagName); + for (const attribute of oldElement.attributes) { + newElement.setAttribute(attribute.name, attribute.value); + } + while (oldElement.firstChild) newElement.appendChild(oldElement.firstChild); + oldElement.replaceWith(newElement); + return newElement; +} + +/** Fits an Ace editor to its current line count within the given bounds. */ +export function fitAceHeight(editor: any, minHeight = 50, maxHeight = 800): void { + const height = Math.min( + maxHeight, + Math.max(minHeight, editor.session.getLength() * editor.renderer.lineHeight), + ); + editor.container.style.height = height + "px"; + editor.resize(); +} + +/** Adds focused styling to inputs present when the module loads. */ +export function initializeInputFocusState(): void { + document.querySelectorAll("input").forEach((input) => { + input.addEventListener("input", () => { + input.classList.toggle("focused", Number(input.value) > 0); + }); + }); +} diff --git a/assets/ts/lib/encoding.ts b/assets/ts/lib/encoding.ts new file mode 100644 index 0000000..52f95ff --- /dev/null +++ b/assets/ts/lib/encoding.ts @@ -0,0 +1,12 @@ +/** Encodes a UTF-8 string as base64. */ +export function toBase64(value: string): string { + return btoa(unescape(encodeURIComponent(value))); +} + +/** Produces the base64 identifier format used by existing DOM elements. */ +export function idify(value: string): string { + const bytes = new TextEncoder().encode(value); + let binary = ""; + bytes.forEach((byte) => (binary += String.fromCharCode(byte))); + return btoa(binary).replaceAll("=", ""); +} diff --git a/assets/ts/lib/format.ts b/assets/ts/lib/format.ts new file mode 100644 index 0000000..8174605 --- /dev/null +++ b/assets/ts/lib/format.ts @@ -0,0 +1,78 @@ +/** Resolves an extension to the editor mode used by CodeMotion. */ +export function getCodeByName(name: string | null | undefined, raw = false): string { + const extension = (name || "").toLowerCase(); + const names: Record = { + html: "html", + js: "javascript", + css: "css", + json: "json", + md: "markdown", + todo: "markdown", + ps: "prettyscript", + py: "python", + php: "php", + }; + const rawNames: Record = { + html: "html", + js: "javascript", + ps: "prettyscript", + py: "python", + md: "markdown", + css: "css", + php: "php", + }; + return raw ? rawNames[extension] || "text" : names[extension] || "plaintext"; +} + +/** Formats a Unix timestamp with the placeholders supported by the legacy helper. */ +export function formatUnix(timestamp: number, format = "{dd}.{mm}.{yyyy}, {hh}:{ii}:{ss}"): string { + const date = new Date(timestamp * 1000); + const yyyy = String(date.getFullYear()); + const dd = String(date.getDate()).padStart(2, "0"); + const mm = String(date.getMonth() + 1).padStart(2, "0"); + const hh = String(date.getHours()).padStart(2, "0"); + const ii = String(date.getMinutes()).padStart(2, "0"); + const ss = String(date.getSeconds()).padStart(2, "0"); + if (!format) return `${dd}.${mm}, ${hh}:${ii}:${ss}`; + return format + .replaceAll("{dd}", dd) + .replaceAll("{mm}", mm) + .replaceAll("{hh}", hh) + .replaceAll("{ii}", ii) + .replaceAll("{ss}", ss) + .replaceAll("{yyyy}", yyyy); +} + +/** Converts seconds to minutes without rounding. */ +export function secondsToMinutes(seconds: number): number { + return seconds / 60; +} + +/** Normalizes Windows separators to forward slashes. */ +export function normalizePath(path: string): string { + return path.replaceAll("\\", "/").replaceAll(/\\/g, "/"); +} + +/** Applies an alpha channel to a HEX, RGB, or RGBA color. */ +export function transparentColor(color: string, alpha = 1): string { + const normalizedAlpha = Math.max(0, Math.min(1, alpha)); + const normalizedColor = color.trim(); + if (normalizedColor.startsWith("#")) { + let hex = normalizedColor.slice(1); + if (hex.length === 3) + hex = hex + .split("") + .map((part) => part + part) + .join(""); + if (hex.length !== 6) throw new Error("Invalid HEX color"); + const red = Number.parseInt(hex.slice(0, 2), 16); + const green = Number.parseInt(hex.slice(2, 4), 16); + const blue = Number.parseInt(hex.slice(4, 6), 16); + return `rgba(${red}, ${green}, ${blue}, ${normalizedAlpha})`; + } + const match = normalizedColor.match( + /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/i, + ); + if (match) return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${normalizedAlpha})`; + throw new Error("Unsupported color format"); +} diff --git a/assets/ts/lib/history.ts b/assets/ts/lib/history.ts new file mode 100644 index 0000000..924af6d --- /dev/null +++ b/assets/ts/lib/history.ts @@ -0,0 +1,61 @@ +export interface HistoryProperties { + id?: string | number; + actionType?: unknown; + value?: unknown; + desc?: unknown; + today?: unknown; +} + +export interface BugProperties { + id?: string | number; + priority?: number; + value?: unknown; + desc?: unknown; + today?: unknown; + isSelf?: boolean; + org?: unknown; + resolved?: boolean; + author?: unknown; + assignedTo?: unknown; + type?: unknown; +} + +/** Appends an entry to the legacy global history store. */ +export function addToHistory({ id, actionType, value, desc, today }: HistoryProperties): void { + const historyId = id === undefined ? Object.keys(historyObject).length + 1 : id; + historyObject[historyId] = { + time: today === undefined ? new Date().format("H:i") : today, + action: actionType, + value: value === undefined ? "Untitled" : value, + description: desc === undefined ? "No description provided" : desc, + }; +} + +/** Adds a bug and records the corresponding history event. */ +export function addToBug(properties: BugProperties): Record { + const bugId = properties.id ?? Object.keys(bugsObject).length + 1; + const priority = + properties.priority !== undefined && !Number.isNaN(properties.priority) + ? properties.priority + : 0; + const priorityInfo = priorityClasses[String(priority)] || priorityClasses["0"]; + addToHistory({ + actionType: "bug-added", + value: properties.value, + desc: `Bug "${properties.value}" added with ${priorityInfo.name} priority`, + }); + bugsObject[bugId] = { + id: bugId, + time: properties.today ?? new Date().format("H:i"), + priority, + value: properties.value, + description: properties.desc ?? "No description provided", + self: properties.isSelf ?? false, + organization: properties.org, + resolved: properties.resolved ?? false, + author: properties.author ?? false, + assignedTo: properties.assignedTo ?? {}, + type: properties.type, + }; + return bugsObject; +} diff --git a/assets/ts/lib/platform.ts b/assets/ts/lib/platform.ts new file mode 100644 index 0000000..33af321 --- /dev/null +++ b/assets/ts/lib/platform.ts @@ -0,0 +1,73 @@ +import { valid } from "./validation.js"; + +export interface NotificationProperties { + type?: unknown; + icon?: unknown; + title?: unknown; + content?: unknown; + time?: unknown; + image?: unknown; +} + +/** Creates a native notification through the Electron preload API. */ +export function createNotify(properties: NotificationProperties = {}): void { + const type = valid(properties.type) ?? "info_i"; + const icon = valid(properties.icon) ?? "info_i"; + const title = valid(properties.title) ?? "Untitled"; + const content = valid(properties.content) ?? "No description provided"; + let time = Number(valid(properties.time) ?? 3000); + const image = valid(properties.image) ?? false; + if (time < 3000 || time > 10_000) time = 3000; + const notification: Record = { + title, + description: content, + timeout: time, + }; + if (icon) notification.icon = icon; + if (type) notification.type = type; + if (image) notification.image = image; + window.electron.createNotification(notification); +} + +/** Returns the current body theme or the default theme name. */ +export function getTheme(): string { + return document.body.getAttribute("theme") ?? "default"; +} + +/** Copies text to the system clipboard and reports failures to the console. */ +export function copyText(text: string): void { + navigator.clipboard + .writeText(text) + .then(() => console.log("Text copied to clipboard successfully!")) + .catch((error: unknown) => console.error("Failed to copy text: ", error)); +} + +/** Converts emoji in a string to Twemoji SVG markup. */ +export function parseTwemojiString(text: string): string { + return twemoji.parse(text, { folder: "svg", ext: ".svg" }); +} + +/** Converts emoji contained by an element to Twemoji SVG markup. */ +export function parseTwemojiElement(element?: Element | null): void { + if (element) twemoji.parse(element, { folder: "svg", ext: ".svg" }); +} + +/** Updates the native application title. */ +export function setAppTitle(title?: string): void { + window.electron.setAppTitle(title); +} + +/** Returns the persisted GitHub token or the historical `false` sentinel. */ +export async function getGithubToken(): Promise { + const localData: unknown = await window.electron.getLocal(); + if ( + localData && + typeof localData === "object" && + "githubToken" in localData && + typeof localData.githubToken === "string" && + localData.githubToken.length > 0 + ) { + return localData.githubToken; + } + return false; +} diff --git a/assets/ts/lib/runtimeErrors.ts b/assets/ts/lib/runtimeErrors.ts new file mode 100644 index 0000000..249ac8c --- /dev/null +++ b/assets/ts/lib/runtimeErrors.ts @@ -0,0 +1,78 @@ +import { BottomWindow } from "../handlers/BottomWindowHandler.js"; +import { formatUnix } from "./format.js"; + +interface RuntimeErrorProperties { + msg?: string; + line?: number | null; + col?: number | null; + time?: number | null; + isNull?: boolean; + win?: unknown; +} + +interface RuntimeError { + msg?: string; + line: number | null; + col: number | null; +} + +let runtimeErrors: RuntimeError[] = []; +let runtimeErrorsCount = 0; + +/** Adds a unique runtime result to the errors history window. */ +export function addRuntimeError({ + msg, + line = null, + col = null, + time = null, + isNull = false, +}: RuntimeErrorProperties): void { + const exists = runtimeErrors.some( + (error) => error.msg === msg && error.line === line && error.col === col, + ); + const wrapper = BottomWindow.get("errorsHistory"); + const badge = document.querySelector("#runtimeErrors .badge"); + const items = document.querySelectorAll(".runtime-item#runTimeErrorItem"); + const lastItem = items[items.length - 1]; + if ((isNull && lastItem?.classList.contains("success")) || exists || !(wrapper && badge)) + return; + const element = document.createElement("div"); + if (isNull) { + runtimeErrors = []; + badge.classList.add("hidden"); + } else { + runtimeErrors.push({ msg, line, col }); + runtimeErrorsCount += 1; + } + badge.classList.toggle("hidden", runtimeErrors.length === 0); + badge.textContent = String(runtimeErrors.length); + items.forEach((item) => item.classList.add("prev")); + element.classList.add("runtime-item", "bottom-window__item"); + element.id = "runTimeErrorItem"; + if (isNull) { + element.classList.add("success"); + element.innerHTML = ` + check_circle + All errors fixed + ${runtimeErrorsCount > 0 ? `(${runtimeErrorsCount})` : ""} + ${time === null ? "" : `${formatUnix(time)}`} + `; + runtimeErrorsCount = 0; + } else { + element.innerHTML = ` + error + ${msg} + ${line === null ? "" : `${line}:${col ?? 0}`} + ${time === null ? "" : `${formatUnix(time)}`} + `; + } + wrapper.add(element); + wrapper.win.lastElementChild?.scrollIntoView({ behavior: "smooth", block: "end" }); +} + +/** Clears runtime errors and records a successful check result. */ +export function clearRuntimeErrors(): void { + runtimeErrors = []; + runtimeErrorsCount = 0; + addRuntimeError({ isNull: true, time: Math.floor(Date.now() / 1000) }); +} diff --git a/assets/ts/lib/sandbox.ts b/assets/ts/lib/sandbox.ts new file mode 100644 index 0000000..08ff068 --- /dev/null +++ b/assets/ts/lib/sandbox.ts @@ -0,0 +1,61 @@ +import { ErrorReporter } from "../../js/ErrorReporter.js"; + +export interface SandboxLog { + type: "log" | "warn" | "error"; + line?: number; + col?: number; + args: unknown[]; +} + +/** Executes code with a captured console and returns its emitted records. */ +export function runSandbox(code: string): SandboxLog[] { + const logs: SandboxLog[] = []; + const getPosition = () => { + const stack = new Error().stack?.split("\n")[3] ?? ""; + const match = stack.match(/:(\d+):(\d+)/); + return match ? { line: Number(match[1]), col: Number(match[2]) } : {}; + }; + const formatArgs = (args: unknown[]) => + args.map((arg) => { + if (typeof arg !== "object" || arg === null) return arg; + try { + return JSON.stringify(arg); + } catch { + return "[Object]"; + } + }); + const capture = + (type: SandboxLog["type"]) => + (...args: unknown[]) => + logs.push({ type, ...getPosition(), args: formatArgs(args) }); + const consoleProxy = { log: capture("log"), warn: capture("warn"), error: capture("error") }; + try { + const execute = new Function("console", code + "\n//# sourceURL=sandbox.js"); + execute(consoleProxy); + } catch (error) { + logs.push({ + type: "error", + args: [error instanceof Error ? error.message : String(error)], + }); + } + return logs; +} + +interface Parser { + parse(code: string, options: Record): unknown; +} + +/** Performs the syntax and runtime checks expected by the editor. */ +export function runCode(code: string, parser: Parser): unknown { + try { + parser.parse(code, { ecmaVersion: "latest", locations: true, sourceType: "module" }); + } catch (error) { + return ErrorReporter.fromAcorn(error); + } + try { + runSandbox(code); + } catch (error) { + return ErrorReporter.fromRuntime(error); + } + return null; +} diff --git a/assets/ts/lib/text.ts b/assets/ts/lib/text.ts new file mode 100644 index 0000000..6f6f510 --- /dev/null +++ b/assets/ts/lib/text.ts @@ -0,0 +1,61 @@ +/** Escapes a value for insertion into HTML markup. */ +export function escapeHtml(value: unknown): string { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** Capitalizes the first character. The misspelled name is retained for API compatibility. */ +export function capitilize(text: unknown): string { + return String(text).charAt(0).toUpperCase() + String(text).slice(1); +} + +/** Returns up to two initials, or the historical `A` fallback. */ +export function getInitials(name: string): string { + if (!name) return "A"; + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((word) => word[0].toUpperCase()) + .join(""); +} + +/** Truncates a string and appends an ellipsis when needed. */ +export function truncateString(value: string, maxLength: number): string { + return value.length <= maxLength ? value : value.slice(0, maxLength) + "..."; +} + +/** Converts HTTP links in plain text to safe external anchors. */ +export function linkify(text: string): string { + return text.replace( + /(https?:\/\/[^\s<]+)/g, + '$1', + ); +} + +/** Splits a camel-cased identifier into display words. */ +export function splitCamelCase(value: string): string[] { + return value + .replace(/([a-z])([A-Z])/g, "$1 $2") + .split(" ") + .map((word, index) => + index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(), + ); +} + +/** Removes the common leading indentation from a multiline string. */ +export function dedent(value: string): string { + const lines = value.replace(/^\n/, "").split("\n"); + const indents = lines + .filter((line) => line.trim()) + .map((line) => line.match(/^ */)?.[0].length ?? 0); + const indent = Math.min(...indents); + return lines + .map((line) => line.slice(indent)) + .join("\n") + .trimEnd(); +} diff --git a/assets/ts/lib/ui.ts b/assets/ts/lib/ui.ts new file mode 100644 index 0000000..5bb577e --- /dev/null +++ b/assets/ts/lib/ui.ts @@ -0,0 +1,17 @@ +import { _TopBarElement } from "../libClasses/topbarElement.js"; + +/** Displays the persistent top-bar prompt requesting an application reload. */ +export function showNeedReloadTopBar(): void { + const prompt = new _TopBarElement("needReload"); + prompt.content({ + icon: "cached", + text: "You need to reload application", + type: "danger", + }); + setTimeout(() => { + prompt.show(); + setTimeout(() => prompt.hide({ iconVisible: true }), 3000); + }, 1000); + prompt.on("hover", (instance: any) => instance.show()); + prompt.on("unhover", (instance: any) => instance.hide({ iconVisible: true })); +} diff --git a/assets/ts/lib/validation.ts b/assets/ts/lib/validation.ts new file mode 100644 index 0000000..2488eb2 --- /dev/null +++ b/assets/ts/lib/validation.ts @@ -0,0 +1,9 @@ +/** Returns a value only when it contains meaningful notification data. */ +export function valid(value: T): T | undefined { + if (value === undefined || value === null || value === false) return undefined; + if (Array.isArray(value) && value.length === 0) return undefined; + if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) { + return undefined; + } + return value; +} diff --git a/assets/ts/lib/values.ts b/assets/ts/lib/values.ts new file mode 100644 index 0000000..f0ceda9 --- /dev/null +++ b/assets/ts/lib/values.ts @@ -0,0 +1,42 @@ +/** Returns whether a value is a non-integer number. */ +export function isFloat(value: unknown): value is number { + return typeof value === "number" && !Number.isInteger(value); +} + +/** Identifies JSON strings containing an array or object. */ +export function isStringifiedObject(value: string): "array" | "object" | null | false { + try { + const parsed: unknown = JSON.parse(value); + if (Array.isArray(parsed)) return "array"; + if (typeof parsed === "object" && parsed !== null) return "object"; + return null; + } catch { + return false; + } +} + +/** Infers the editor's historical scalar type name from a value. */ +export function type(value: { toString(): string }): string { + const text = value.toString().trim(); + if (/^-?\d+$/.test(text)) return "int"; + if (/^-?\d*\.\d+$/.test(text)) return "float"; + if (/^(true|false)$/.test(text)) return "boolean"; + if (/^\[.*\]$/.test(text)) return "array"; + if (/^\{.*\}$/.test(text)) return "object"; + return "string"; +} + +/** Preserves the legacy object check, including its `null` behavior. */ +export function isObject(value: unknown): boolean { + return typeof value === "object" && !Array.isArray(value); +} + +/** Returns whether a value is an array. */ +export function isArray(value: unknown): value is unknown[] { + return typeof value === "object" && Array.isArray(value); +} + +/** Writes a visually distinct event diagnostic. */ +export function eventLog(...args: unknown[]): void { + console.warn("[EVENT LOG] -----------\n", ...args); +} diff --git a/helpers/debuggerWindow/debuggerWindow.js b/helpers/debuggerWindow/debuggerWindow.js index 55c19ad..eddf2e8 100644 --- a/helpers/debuggerWindow/debuggerWindow.js +++ b/helpers/debuggerWindow/debuggerWindow.js @@ -7,6 +7,8 @@ const bus = require("../eventBus.js") const { getAppIcon } = require("../../app/main/helpers/requests.js") const { ASSETS_PATH } = require("../../app/main/helpers/paths.js") +let debuggerWindow = null; + async function createDebuggerWindow(mainWindow, title = "Debugger") { const overlayIconPath = path.join(ASSETS_PATH, "media", "debugger_icon.png") const appIcon = await getAppIcon() @@ -109,4 +111,4 @@ async function createDebuggerWindow(mainWindow, title = "Debugger") { return win } -module.exports = { createDebuggerWindow } \ No newline at end of file +module.exports = { createDebuggerWindow } diff --git a/package.json b/package.json index c8ebed3..6c9083c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "author": "Yurba.Developers", "main": "app/dist/main.js", "scripts": { - "build": "tsc && tsc -p tsconfig.esm.json", + "build": "tsc && tsc -p tsconfig.esm.json && tsc -p tsconfig.renderer.json", + "build:renderer": "tsc -p tsconfig.renderer.json", "test": "node --test tests/unit/*.test.mjs", "cdbuild": "cd codemirror && npm i && npm run build", "start": "npm run cdbuild && npm run build && electron .", diff --git a/tests/unit/lib.test.mjs b/tests/unit/lib.test.mjs new file mode 100644 index 0000000..cb71070 --- /dev/null +++ b/tests/unit/lib.test.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +async function importModule(path) { + const source = await readFile(new URL(path, import.meta.url), "utf8"); + return import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); +} + +const encoding = await importModule("../../assets/js/lib/encoding.js"); +const format = await importModule("../../assets/js/lib/format.js"); +const text = await importModule("../../assets/js/lib/text.js"); +const values = await importModule("../../assets/js/lib/values.js"); + +test("text helpers preserve legacy output", () => { + assert.equal(text.escapeHtml(`&`), "<a href="'">&"); + assert.equal(text.capitilize("hello"), "Hello"); + assert.equal(text.getInitials("Ada Lovelace Byron"), "AL"); + assert.equal(text.getInitials(""), "A"); + assert.equal(text.truncateString("abcdef", 3), "abc..."); + assert.deepEqual(text.splitCamelCase("helloWorldAgain"), ["Hello", "world", "again"]); + assert.equal(text.dedent("\n first\n second"), "first\n second"); +}); + +test("encoding and format helpers preserve legacy contracts", () => { + assert.equal(encoding.toBase64("Привет"), "0J/RgNC40LLQtdGC"); + assert.equal(encoding.idify("test"), "dGVzdA"); + assert.equal(format.getCodeByName("JS"), "javascript"); + assert.equal(format.getCodeByName("unknown", true), "text"); + assert.equal(format.normalizePath("C:\\tmp\\file.js"), "C:/tmp/file.js"); + assert.equal(format.secondsToMinutes(90), 1.5); + assert.equal(format.transparentColor("#abc", 0.5), "rgba(170, 187, 204, 0.5)"); + assert.equal(format.transparentColor("rgb(1, 2, 3)", 2), "rgba(1, 2, 3, 1)"); +}); + +test("value helpers retain sentinel and null behavior", () => { + assert.equal(values.isStringifiedObject("[]"), "array"); + assert.equal(values.isStringifiedObject("{}"), "object"); + assert.equal(values.isStringifiedObject("1"), null); + assert.equal(values.isStringifiedObject("{"), false); + assert.equal(values.isObject(null), true); + assert.equal(values.isArray([]), true); + assert.equal(values.type("1.5"), "float"); +}); diff --git a/tsconfig.renderer.json b/tsconfig.renderer.json new file mode 100644 index 0000000..c34cd0d --- /dev/null +++ b/tsconfig.renderer.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "rootDir": "assets/ts", + "outDir": "assets/js", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": false, + "sourceMap": false, + "allowJs": false, + "verbatimModuleSyntax": true + }, + "include": ["assets/ts/**/*.ts", "assets/ts/**/*.d.ts"] +}