"use strict"; /** * @callback wmarkdown_ajax_callback * @param {?string} response * @param {!number} status * @param {!number} state * @param {!boolean} ok * @param {!string} message * @return {void} */ /** * @callback wmarkdown_preload_callback * @param {?HTMLElement} element * @param {!boolean} asynchronous * @param {!integer} error * @returns {void} */ /** * @callback wmarkdown_element_event_callback * @param {!HTMLElement} element * @param {!Event} event * @returns {void} */ /** * @class * @constructor * @param {?string|Object.} [inputs] * @returns {void} * @access public */ export const WMarkDown = (function(){ /** * @constructs WMarkDown * @param {?string|Object.} [inputs] * @returns {void} * @access private */ const WMarkDown = function(inputs){ /** @type {WMarkDown} */ const self = this, /** @type {Array.} */ dictionary_done = [], /** @type {Array.|Array.>>} */ dictionary = [], /** @type {Array.} */ root_boxes = [], /** @type {string} */ hash_alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", /** @type {number} */ hash_length = 11, /** @type {Array.} */ hashes = [], /** @type {Array.>} */ type_dictionary = [ ["JavaScript/ECMAScript", "js", "javascript", "ecma", "ecmascript", "node", "nodejs", "typescript", "ts"], ["Python", "python", "py"] ]; /** @type {number|null} */ let thread_inteval = null, /** @type {boolean} */ dictionary_loaded = false, /** @type {Array.} */ dictionary_boxes = [], /** @type {boolean|null} */ check_gui_controls = null; /** * @returns {void} * @access private */ const constructor = () => { if(typeof inputs == "string") inputs = {dictionary : inputs}; thread_inteval = setInterval(thread_method, 250); if(inputs.dictionary){ /** @type {number} */ let loaded = 0; /** @type {Array.} */ const dictionaries = inputs.dictionary instanceof Array ? inputs.dictionary : [inputs.dictionary], /** * @returns {void} */ end = () => ++ loaded == dictionaries.length && (dictionary_loaded = true); dictionaries.forEach(url => WMarkDown.get(url, data => { try{ self.add_to_dictionary(JSON.parse(data)); }catch(exception){ console.error(exception); }; end(); })); }; }; /** * @param {!Array., Array., Array.>} data * @returns {void} * @access public */ this.add_to_dictionary = data => data.forEach(([patterns, definition, links]) => { /** @type {number|null} */ let i = null; patterns[0] instanceof Array || (patterns = [patterns]); patterns.forEach(([pattern, text]) => { if(!dictionary_done.includes(text)){ if(i === null) dictionary[i = dictionary.length] = { patterns : [[WMarkDown.format_pattern(pattern), text]], definition : typeof definition == "string" ? definition : definition.join(""), links : links }; else dictionary[i].patterns.push([WMarkDown.format_pattern(pattern), text]); }; }); }); /** * @returns {string} * @access public */ this.get_hash = () => { /** @type {string} */ let hash; /** @type {number} */ const l = hash_alphabet.length; do{ hash = ""; while((hash += hash_alphabet[Math.random() * l >> 0]).length < hash_length); }while( hashes.includes(hash) || /^[0-9]/.test(hash) || document.querySelector("." + hash + ",#" + hash + ",[name=" + hash + "]") ); hashes.push(hash); return hash; }; /** * @param {!NodeList} block * @returns {void} * @access public */ this.format_dictionary = block => block.childNodes.forEach((item, i) => { /** @type {Array.>} */ const blocks = []; if(item.nodeName == "#text"){ if(item.textContent.trim()){ /** @type {string} */ let html = item.textContent; /** @type {Array.} */ const items = []; dictionary.forEach((item, k) => { item.patterns.forEach(([pattern, text]) => { html = html.replace(pattern, (...matches) => { /** @type {number} */ const j = items.length; items.push([k, matches, text.replace(/\$([0-9])/g, (all, match_i) => { return matches[match_i] !== null && matches[match_i] !== undefined ? matches[match_i] : ""; })]); return dictionary_item_mark[0] + j + dictionary_item_mark[1]; }); }); }); if(html != item.textContent){ /** @type {HTMLSpanElement} */ const element = document.createElement("span"); blocks.push([i, element]); element.innerHTML = html.replace(dictionary_item_mark[2], (_, j) => { /** @type {Array.} */ const [k, matches, text] = items[Number(j)]; return `` + text + ``; }); }; }; }else if( !["img", "a", "audio", "canvas", "picture"].includes((item.tagName || item.nodeName).toLowerCase()) && !["wmd-excluded", "wmd-code-block", "wmd-code-doc"].some(class_name => item.classList && item.classList.contains(class_name)) ){ self.format_dictionary(item); addEventListener("click", self.dictionary_out); }; blocks.forEach(([i, element]) => { block.insertBefore(element, block.childNodes[i]); block.childNodes[i + 1].remove(); }); }); /** * @param {!HTMLElement} item * @returns {HTMLElement|null} * @access public */ this.get_root_box = item => { /** @type {HTMLElement|null} */ let box = null; while(item && item.classList && (item = item.parentNode)) item.classList && item.classList.contains("wmarkdown") && (box = item); return box; }; /** * @param {!HTMLUListElement} data_box * @param {!String} i18n * @param {!String} text * @param {!String} action * @returns {HTMLLIElement} * @access private */ const add_button_data = (data_box, i18n, text, action) => { /** @type {HTMLLIElement} */ const button = data_box.appendChild(document.createElement("li")); button.setAttribute("data-i18n", i18n); button.setAttribute("data-i18n-without", true); button.setAttribute("title", text); button.addEventListener("click", action); button.innerHTML = (` ` + text + ` ` + text + ` `); return button; }; /** * @param {!HTMLElement} box * @param {!String} name * @param {!Array.} [buttons] * @returns {void} * @access private */ const set_special_type = (box, name, buttons) => { while(!box.classList.contains("wmd-code-block") && (box = box.parentNode)); /** @type {HTMLUListElement} */ const data = box.querySelector(".data"); box.querySelector("li[data-i18n=type]>.value").innerHTML = name; add_button_data(data, "view_switch", "View switch", event => WMarkDown.view_switch(event.target, event)); buttons && buttons.forEach(([i18n, text, action]) => add_button_data(data, i18n, text, action)); }; /** * @param {!HTMLElement} content * @param {!String} language * @returns {HTMLDivElement} * @access private */ const build_special_type = (content, language) => { content.parentNode.childNodes.forEach(item => item.tagName && item.setAttribute("data-visible", false)); /** @type {HTMLDivElement} */ const box = content.parentNode.appendChild(document.createElement("div")); box.setAttribute("class", "view"); box.setAttribute("data-visible", true); set_special_type(content, language); return box; }; /** * @param {HTMLElement} item * @returns {HTMLElement|null} * @access public */ this.get_anp = item => { while(!item.classList.contains("anp") && (item = item.parentNode)) if(!item.classList){ item = null; break; }; return item; }; const code_block_format = () => { document.querySelectorAll(".wmd-code-block[data-processed=false]").forEach(block => { /** @type {string} */ const language = block.getAttribute("data-type").toLowerCase(), /** @type {HTMLElement} */ content = block.querySelector(".content"), /** @type {String} */ type = block.getAttribute("data-type").toLowerCase(), /** @type {HTMLElement|null} */ anp_item = self.get_anp(block), /** @type {Boolean} */ dark_mode = ( anp_item ? (anp_item.getAttribute("data-gui-mode") == "dark" || (anp_item.getAttribute("data-gui-mode") == "default" && anp_item.getAttribute("data-dark-mode") == "true")) : window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); block.setAttribute("data-processed", true); mermaid.initialize({ theme : dark_mode ? "dark" : "default", themeVariables : {fontSize : "16px"} }); if(["math", "maths", "mathjax"].includes(language)){ build_special_type(content, "MathJax").innerHTML = MathJax.tex2chtml(content.innerText).outerHTML; MathJax.startup.document.clear(); MathJax.startup.document.updateDocument(); }else if(["mermaid", "mermaidjs", "mermaid_js"].includes(language)){ /** @type {HTMLDivElement} */ const box = build_special_type(content, "Mermaid JS"); mermaid.render(self.get_hash(), content.innerText).then(graph => box.innerHTML = graph.svg); }else try{ content.innerHTML = hljs.highlight(content.innerText, {language : ( ["wmd-examples", "wmd", "wmarkdown"].includes(language) ? "markdown" : language)}).value; type_dictionary.some(alternatives => { if(alternatives.includes(type)){ block.querySelector("[data-i18n=type]>.value").innerText = alternatives[0]; return true; }; return false; }); }catch(exception){}; }); }; const build_dictionary_items = () => { if(dictionary_loaded){ /** @type {HTMLElement} */ const block = document.querySelector("[data-dictionary-processed=false]"); if(block && [...block.childNodes].slice(-3).some(item => ( item.classList && item.classList.contains("wmd-process-and-loaded") ))){ block.querySelectorAll(".wmd-process-and-loaded").forEach(item => { item.parentNode.hasAttribute("data-dictionary-processed") && item.parentNode.setAttribute("data-dictionary-processed", true); item.remove(); }); self.format_dictionary(block); }; }; }; const process_lists = () => document.querySelectorAll("[data-list-unprocessed=true]").forEach(list => { /** @type {HTMLSpanElement} */ const deployer = list.parentNode.insertBefore(document.createElement("span"), list.parentNode.childNodes[0]); deployer.addEventListener("click", WMarkDown.deploy); deployer.setAttribute("data-deployed", list.getAttribute("data-deployed")); deployer.innerHTML = (` Deploy `); list.setAttribute("data-list-unprocessed", false); }); const autoload_media = body => document.querySelectorAll(".wmd-media[data-status=unloaded]").forEach((item, i) => { /** @type {DOMRect} */ const bounds = item.getBoundingClientRect(); if( (bounds.y + bounds.height > -100 && bounds.y < body.offsetHeight + 100) && (bounds.x + bounds.width > -100 && bounds.x < body.offsetWidth + 100) ){ /** @type {HTMLElement} */ const main_item = item.querySelector("noscript+*"); item.setAttribute("data-status", "loading"); if(main_item.tagName.toLowerCase() == "img"){ main_item.addEventListener("load", event => WMarkDown.image_loaded(event.target, event)); main_item.addEventListener("error", event => WMarkDown.image_loaded(event.target, event)); item.setAttribute("data-status", "loading"); WMarkDown.image_load(main_item); }; }; }); /** * @returns {void} * @access private */ const thread_method = () => { /** @type {HTMLBodyElement} */ const body = document.querySelector("body"); code_block_format(); build_dictionary_items(); process_lists(); document.querySelectorAll(".wmd-media[data-status=unprocessed]").forEach(item => item.setAttribute("data-status", "unloaded")); if(body){ autoload_media(body); document.querySelectorAll(".wmarkdown[data-menu-processed=false]").forEach(block => { if(block.getAttribute("data-menu-processed") == "true") return; /** @type {HTMLUListElement|null} */ let menu = document.querySelector(".wmd-main-menu>ul"); /** @type {Array.} */ const items = block.querySelectorAll("h1,h2,h3,h4,h5,h6"); if(items.length){ /** @type {number} */ let current_level = 0; if(!menu){ /** @type {HTMLDivElement} */ const button = document.querySelector("header").appendChild(document.createElement("div")); (menu = ( document.querySelector("[data-cells]") || document.querySelector("body") ).appendChild(document.createElement("nav"))).appendChild(document.createElement("ul")); menu.setAttribute("class", "wmd-main-menu"); menu.setAttribute("data-visible", false); menu = menu.childNodes[0]; addEventListener("click", WMarkDown.hide_menu); button.innerHTML += (` WMarkDown.show_menu(event.target, event)); }) + `"> Menu `); button.setAttribute("class", "wmd-main-menu-button"); button.setAttribute("data-role", "link"); }; current_level = [...items].reduce((lower, item) => { const level = Number(item.tagName[1]); return level < lower ? level : lower }, 6); items.forEach(item => { const level = Number(item.tagName[1]), menu_item = document.createElement("li"), anchor = menu_item.appendChild(document.createElement("a")); menu_item.setAttribute("data-level", level); anchor.innerText = item.innerText; anchor.setAttribute("href", "#" + item.getAttribute("id")); anchor.setAttribute("target", "_self"); anchor.setAttribute("title", item.innerText); if(current_level < level){ const subblock = menu.childNodes[menu.childNodes.length - 1]; if(!(menu = subblock.childNodes[menu.childNodes.length - 1]) || menu.tagName.toLowerCase() != "ul"){ const button_deployer = subblock.insertBefore(document.createElement("span"), subblock.childNodes[0]); menu = subblock.appendChild(document.createElement("ul")); button_deployer.setAttribute("data-deployed", false); button_deployer.addEventListener("click", WMarkDown.deploy); button_deployer.innerHTML = (` Deploy `); button_deployer.setAttribute("data-role", "link"); }; current_level ++; }else while(current_level > level && menu.parentNode.parentNode.tagName.toLowerCase() == "ul"){ current_level --; menu = menu.parentNode.parentNode; }; menu.appendChild(menu_item); }); }; block.setAttribute("data-menu-processed", true); block.querySelectorAll(".wmarkdown[data-menu-processed=false]").forEach(subblock => subblock.setAttribute("data-menu-processed", true)); window.location.hash && (window.location.href = window.location.hash); }); }; }; /** * @param {!HTMLElement} box * @returns {number} * @access public */ this.get_next_z = box => { /** @type {number} */ let z = 10; box.childNodes.forEach(node => { if(node && node.style){ /** @type {number} */ const item_z = Number(node.style.zIndex) || 0; item_z >= z && (z = item_z + 1); }; }); return z; }; /** * @param {!MouseEvent} event * @returns {void} * @access public */ this.dictionary_over = event => setTimeout(() => { /** @type {string|null} */ const hash = event.target.getAttribute("data-hash"); if(!hash || dictionary_boxes.includes(hash)) return; /** @type {HTMLBodyElement} */ const body = document.querySelector("body"), /** @type {HTMLDivElement} */ box = document.querySelector("body").appendChild(document.createElement("div")), /** @type {DOMRect} */ bounds = event.target.getBoundingClientRect(), /** @type {number} */ x = bounds.x + (bounds.width / 2), /** @type {number} */ y = bounds.y + (bounds.height / 2), /** @type {number} */ i = Number(event.target.getAttribute("data-i")), /** @type {string} */ own_keys = []; box.setAttribute("class", "wmd-dictionary-box"); box.setAttribute("data-dictionary-box", hash); dictionary_boxes.push(hash); box.innerHTML = (`
` + dictionary[i].patterns.reduce((definition, [pattern, text]) => ( definition.replace(pattern, () => { own_keys.push(text.replace(/\$[0-9]/g, "")); return dictionary_item_mark[0] + (own_keys.length - 1) + dictionary_item_mark[1]; }) ), dictionary[i].definition).replace(dictionary_item_mark[2], (all, i) => `` + own_keys[i] + ``) + `
`); // box.style.zIndex = dictionary_z ++; box.style.zIndex = self.get_next_z(box.parentNode); if(x > body.offsetWidth / 2) box.style.right = (body.offsetWidth - x) + "px"; else box.style.left = x + "px"; if(y > body.offsetHeight / 2) box.style.bottom = (body.offsetHeight - y) + "px"; else box.style.top = y + "px"; setTimeout(() => self.format_dictionary(box.querySelector(".definition")), 100); }, 100); /** * @param {!MouseEvent} event * @returns {void} * @access public * @static */ this.dictionary_out = event => { /** @type {string|null} */ let hash = null, /** @type {HTMLElement} */ item = event.target, /** @type {string|null} */ box_hash = null, /** @type {string|null} */ item_hash = null; while(item.classList){ if(item.classList.contains("wmd-dictionary-box")){ box_hash = hash = item.getAttribute("data-dictionary-box"); break; }else if(item.classList.contains("wmd-dictionary-item")) item_hash = hash = item.getAttribute("data-hash"); item = item.parentNode; }; if(hash){ /** @type {number} */ let i = dictionary_boxes.indexOf(hash); if(++ i){ /** @type {number} */ let j, /** @type {number} */ k; if(box_hash) [j, k] = ( !dictionary_boxes.includes(item_hash) ? [i, dictionary_boxes.length] : [i = dictionary_boxes.indexOf(item_hash) + 1, dictionary_boxes.length]); else{ [j, k] = [0, dictionary_boxes.length]; i = 0; }; dictionary_boxes.slice(j, k).forEach(hash => { document.querySelector(".wmd-dictionary-box[data-dictionary-box=" + hash + "]").remove(); dictionary_boxes.splice(i, 1); }); return; }; }; document.querySelectorAll(".wmd-dictionary-box").forEach(box => box.remove()); dictionary_boxes = []; }; /** * @param {!wmarkdown_preload_callback} callback * @returns {string} * @access public */ this.preload_hash = callback => { /** @type {string} */ const hash = self.get_hash(); WMarkDown.preload("[data-preload=" + hash + "]", item => { item && item.removeAttribute("data-preload"); hashes.splice(hashes.indexOf(hash), 1); callback(item); }); return hash; }; constructor(); }; /** * @param {!string} pattern * @returns {RegExp} * @access public * @static */ WMarkDown.format_pattern = pattern => { /** @type {RegExpMatchArray} */ const matches = pattern.match(/^\/(.+)\/([a-z]*)$/); matches || console.log([pattern, matches]); return new RegExp(matches[1], matches[2]); }; /** * @param {!string} url * @param {!wmarkdown_ajax_callback} callback * @returns {XMLHttpRequest} * @access public * @static */ WMarkDown.get = (url, callback) => { /** @type {boolean} */ let ended = false; /** @type {XMLHttpRequest} */ const ajax = new XMLHttpRequest(), /** @type {number} */ time = Date.now(), /** * @param {!string} message * @returns {void} */ end = message => !ended && (ended = true) && typeof callback == "function" && callback( ajax.responseText, ajax.status, ajax.readyState, message == "OK", message ); ajax.open("get", url, true); ajax.timeout = 2000; ajax.onreadystatechange = () => { if(ended) return; if(ajax.readyState == 4) end((ajax.status >= 200 && ajax.status < 300) || [301, 302, 304].includes(ajax.status) ? "OK" : "HTTP_ERROR"); else if(Date.now() - time > 2000) end("FORCED_TIMEOUT"); }; ajax.send(null); ajax.onerror = () => end("ERROR"); ajax.onabort = () => end("ABORTED"); ajax.ontimeout = () => end("TIMEOUT"); return ajax; }; /** * @param {!MouseEvent} event * @returns {void} * @access public * @static */ WMarkDown.deploy = event => { const item = event.target.hasAttribute("data-deployed") ? event.target : event.target.parentNode; item.setAttribute("data-deployed", item.getAttribute("data-deployed") == "false"); }; /** * @param {!HTMLImageElement} item * @param {!ErrorEvent} [event] * @returns {void} * @access public * @static */ WMarkDown.image_load = (item, event) => { /** @type {Array.} */ const images = JSON.parse(atob(item.getAttribute("data-sources"))), /** @type {number} */ i = Number(item.getAttribute("data-i")); if(i >= images.length){ item.parentNode.setAttribute("data-status", "error"); return; }; item.setAttribute("src", images[i]); item.setAttribute("data-i", i + 1); }; /** * @param {!HTMLImageElement} item * @param {!EventTarget} [event] * @returns {void} * @access public * @static */ WMarkDown.image_loaded = (item, event) => { /** @type {HTMLSpanElement|Null} */ const span_image = item.parentNode.querySelector(".image"); item.parentNode.setAttribute("data-status", "success"); span_image && (span_image.style.backgroundImage = "url('" + item.src + "')"); }; /** * @param {!EventTarget} [event] * @returns {void} * @access public * @static */ WMarkDown.show_menu = event => document.querySelector(".wmd-main-menu").setAttribute("data-visible", true); /** * @param {!EventTarget} event * @returns {void} * @access public * @static */ WMarkDown.hide_menu = event => { if( event.target.parentNode.parentNode.classList && event.target.parentNode.parentNode.classList.contains("wmd-main-menu-button") ) return; /** @type {HTMLElement|null} */ const main_menu = document.querySelector(".wmd-main-menu"); if(!main_menu || main_menu.getAttribute("data-visible") == "false") return; /** @type {HTMLElement} */ let item = event.target; while(item.tagName.toLowerCase() != "body" && item != main_menu) item = item.parentNode; item != main_menu && main_menu.setAttribute("data-visible", false); }; /** * @param {!HTMLElement} item * @param {!MouseEvent} [event] * @returns {void} * @access public * @static */ WMarkDown.view_switch = (item, event) => { /** @type {HTMLDivElement} */ const box = item.parentNode.parentNode.querySelector(".code"); if(!box) return; /** @type {HTMLDivElement} */ const view = box.querySelector(".view"), /** @type {Boolean} */ visible = view.getAttribute("data-visible") != "true"; view.setAttribute("data-visible", visible); ["lines", "content"].forEach(key => box.querySelector("." + key).setAttribute("data-visible", !visible)); }; /** * @param {!(string|HTMLElement)} selector * @param {!wmarkdown_preload_callback} callback * @returns {void} * @access public * @static */ WMarkDown.preload = (selector, callback) => { if(typeof callback == "function"){ if(!selector) callback(null, false, 1 << 1); else if(selector.tagName || selector.nodeName) callback(selector, false, 0); else if(typeof selector == "string"){ /** @type {HTMLElement|null} */ let item = null; try{ if(item = document.querySelector(selector)){ callback(item, false, 0); return; }; }catch(exception){ callback(null, false, 1 << 0); return; }; /** @type {number} */ const date = Date.now(), /** @type {number} */ interval = setInterval(() => { if(item = document.querySelector(selector)){ clearInterval(interval); callback(item, true, 0); }else if(Date.now() - date > 2000){ clearInterval(interval); callback(null, false, 1 << 2); }; }, 250); }else callback(null, false, 1 << 3); }; }; return WMarkDown; })();