diff --git a/langchain-rag-23ai/workshops/desktop/index.html b/langchain-rag-23ai/workshops/desktop/index.html index 1f0cffa8..6f90dfd4 100644 --- a/langchain-rag-23ai/workshops/desktop/index.html +++ b/langchain-rag-23ai/workshops/desktop/index.html @@ -10,7 +10,7 @@ - + diff --git a/langchain-rag-23ai/workshops/desktop/main.26.3.js b/langchain-rag-23ai/workshops/desktop/main.26.3.js new file mode 100644 index 00000000..cca886dd --- /dev/null +++ b/langchain-rag-23ai/workshops/desktop/main.26.3.js @@ -0,0 +1,2720 @@ +/* + * ============================================ + * Oracle LiveLabs Workshop Framework + * Version: 26.3 + * ============================================ + * + * Version Date Author Summary + * --------------------------------------------------------- + * 21.9 Feb-14-22 Kevin Lazarz Added fix for LLAPEX-403 (accessible html tables) + * 22.0 Feb-14-22 Kevin Lazarz Added alt-text fix - add alt attribute to all images which do not have alt + * 22.1 Feb-15-22 Kevin Lazarz Added fix for landmark issue (LLAPEX-401) and list issue (LLAPEX-400) + * 22.2 Feb-17-22 Kevin Lazarz Role back LLAPEX-400 due issues in some workshops + * 22.3 Mar-08-22 Kevin Lazarz Temp fix for list issues LLAPEX-400, added QA check for images missing alt-text, changed numbering for table header + * 22.4 Mar-30-22 Ashwin Agarwal Added alt-text for modal images (LLAPEX-431) + * 22.5 Apr-1-22 Ashwin Agarwal Created global main.js (merge main.js * main.sprint.js) - LLAPEX-440 + * 22.6 Apr-18-22 Ashwin Agarwal Accessibility bugs in JavaScript - anchor not in
  • - LLAPEX-400 + * 22.7 Apr-20-22 Ashwin Agarwal Add a static header for sprints - LLAPEX-448 + * 22.8 May-09-22 Ashwin Agarwal Single sourcing does not work for included files - LLAPEX-477 + * 22.9 Jun-01-22 Ashwin Agarwal Remove header, custom table caption (LLAPEX-418), hide expand/collapse button (LLAPEX-465), variables (LLAPEX-487), object storage URL changes (LLAPEX-488) + * 22.10 Jun-01-22 Ashwin Agarwal Remove feature where the expand/collapse button disappears when there are less than or equal to 2 h2 sections + * 22.11 Jun-15-22 Ashwin Agarwal Relative path incorrect for included files (LLAPEX-480) + * 22.12 Jul-15-22 Kevin Lazarz Replace object storage links with github.io link + * 23.0 Aug-24-22 Kevin Lazarz Added code to allow embedding videos from Oracle Video Hub (LLAPEX-559) + * 23.1 Sep-21-22 Kevin Lazarz Fix for LLAPEX-595 + * 23.2 Nov-10-22 Kevin Lazarz Added LLAPEX-637 & LLAPEX-642 + * 23.3 Mar-13-23 Dan Williams Provided an example of imperative text (eg.'Start' not 'Starting) (LLAPEX-699) + * 23.4 Mar-17-23 Dan Williams Updated imperative text ( eg. 'Start' not 'Starting') to include where issue is within Lab (LLAPEX-701) + * 23.4.1 Oct-24-24 Kevin Lazarz Fixed Lintchecker + * 23.5 Oct-24-24 Kaylien Phan Fixing "includes" functionality to accommodate for CDN + * 23.6 Mar-20-25 Brianna Ambler Adding support for LiveSQL integration with LiveLabs sprints + * 23.7 Jan-06-26 Brianna Ambler Renaming LiveSQL to FreeSQL + * 23.8 Jan-22-26 Kevin Lazarz Modernization: Clipboard API, error handling, code organization, JSDoc + * 24.0 Jan-22-26 Kevin Lazarz Added lazy loading images + * 24.1 Jan-22-26 Kevin Lazarz Added interactive quiz feature + * 24.2 Jan-22-26 Kevin Lazarz Added quiz scoring with badge download + * 24.3 Jan-22-26 Kevin Lazarz Enhanced badge UI with preview and disclaimer + * 24.4 Jan-23-26 Kevin Lazarz Auto-calculate estimated reading time for "Estimated Time: X" placeholder + * 24.5 Jan-23-26 Kevin Lazarz Added direct video file embedding support [](video:URL) + * 25.0 Jan-28-26 Kevin Lazarz SQL copy-to-clipboard: added trailing newline for ```sql blocks so last statement executes + * 26.2 Feb-06-26 Kevin Lazarz QA parity: removed gerund checks, synced script behavior + * 26.3 Mar-13-26 Kevin Lazarz FreeSQL iframe loader split into external module + loader wiring + */ + +/* + * ============================================ + * SECTION 1: CONFIGURATION & GLOBALS + * ============================================ + */ +"use strict"; + +var showdown = "https://oracle-livelabs.github.io/common/redwood-hol/js/showdown.min.js"; +var highlight = "https://oracle-livelabs.github.io/common/redwood-hol/js/highlight.min.js"; +var freesqlEmbedLoader = "https://oracle-livelabs.github.io/common/redwood-hol/js/freesql-embed-loader.min.js"; + +/** + * Resolve the currently loaded main script URL. + * Supports both versioned and minified file names. + * @returns {string} + */ +function resolveCurrentMainScriptSource() { + if (document.currentScript && document.currentScript.src) { + return document.currentScript.src; + } + + let scripts = document.getElementsByTagName("script"); + let mainPattern = /\/main(?:\.[0-9.]+)?(?:\.min)?\.js(?:[?#].*)?$/; + + for (let index = scripts.length - 1; index >= 0; index -= 1) { + let src = scripts[index].src || ""; + if (mainPattern.test(src)) { + return src; + } + } + + return ""; +} + + +/** @constant {string} Base path for related workshops content */ +const related_path = "https://oracle-livelabs.github.io/common/related/"; + +let main = function () { + // Internal configuration + let manifestFileName = "manifest.json"; + let expandText = "Expand All Tasks"; + let collapseText = "Collapse All Tasks"; + + /** @constant {string} Current domain origin */ + const currentDomain = window.location.origin; + console.log("Current domain:", currentDomain); + + /** @constant {string} Text displayed on copy buttons */ + const copyButtonText = "Copy"; + + /** @constant {string} Query parameter name for lab selection */ + const queryParam = "lab"; + + /** @constant {Array} UTM tracking parameter configurations */ + const utmParams = [ + { + "url": "https://signup.cloud.oracle.com", + "inParam": "customTrackingParam", + "outParam": "sourceType" + }, + { + "url": "https://myservices.us.oraclecloud.com/mycloud/signup", + "inParam": "customTrackingParam", + "outParam": "sourceType" + }, + { + "url": "https://myservices.oraclecloud.com/mycloud/signup", + "inParam": "customTrackingParam", + "outParam": "sourceType" + }, + { + "url": "https://cloud.oracle.com", + "inParam": "customTrackingParam", + "outParam": "sourceType" + } + ]; + + /** @constant {string} Query parameter name for navigation state */ + const nav_param_name = 'nav'; + + /** @constant {string} Query parameter name for header visibility */ + const header_param_name = 'header'; + + /** @constant {Object} Extended navigation hash mappings */ + const extendedNav = { '#last': 2, '#next': 1, '#prev': -1, "#first": -2 }; + + $.ajaxSetup({ cache: true }); + + /** @type {Object|null} Global manifest file reference */ + let manifest_global; + + /* + * ============================================ + * SECTION 2: INITIALIZATION + * ============================================ + */ + $(document).ready(function () { + let manifestFileContent; + if (getParam("manifest")) { + manifestFileName = getParam("manifest"); + } + + let freeSqlLoaderReady = $.Deferred(); + $.getScript(freesqlEmbedLoader, function () { + console.log("FreeSQL embed loader loaded!"); + freeSqlLoaderReady.resolve(); + }).fail(function (_jqXHR, _settings, exception) { + console.warn("FreeSQL embed loader failed to load from " + freesqlEmbedLoader + ".", exception); + freeSqlLoaderReady.resolve(); + }); + + $.when( + $.getScript(showdown, function () { + console.log("Showdown library loaded!"); + }), + $.getJSON(manifestFileName, function (manifestFile) { + if (manifestFile.workshoptitle !== undefined) { // if manifest file contains a field for workshop title + document.getElementsByClassName("hol-Header-logo")[0].innerText = manifestFile.workshoptitle; // set title in the HTML output (DBDOC-2392) + } + console.log("Manifest file loaded!"); + + + if (getParam("manifest")) { + $(manifestFile.tutorials).each(function () { + if ($(this)[0].filename.indexOf("http") == -1 && $(this)[0].filename[0] !== "/") { + $(this)[0].filename = manifestFileName.substring(0, manifestFileName.lastIndexOf("/") + 1) + $(this)[0].filename; + } + }); + } + + // const currentDomain = window.location.origin; // e.g., "https://livelabs.oracle.com" + // console.log("Current domain:", currentDomain); + + // Added for include feature: [DBDOC-2434] Include any file inside of Markdown before rendering + for (let short_name in manifestFile.include) { + let include_fname = manifestFile.include[short_name]; + + if (include_fname.indexOf("http") === -1 && include_fname[0] !== "/") { // If the link is relative + include_fname = manifestFileName.substring(0, manifestFileName.lastIndexOf("/") + 1) + include_fname; + } + + // Modify include_fname based on the current domain + if (include_fname.startsWith("/") && currentDomain.includes("livelabs.oracle.com")) { + include_fname = "/cdn/" + include_fname.replace(/^\/+/, ""); // Ensure correct path + } else if (include_fname.startsWith("/") && currentDomain.includes("apexapps-stage.oracle.com")) { + include_fname = "/livelabs/cdn/" + include_fname.replace(/^\/+/, ""); // Ensure correct path + } + + console.log("Fetching:", include_fname); + + $.get(include_fname, function (included_file_content) { + manifestFile.include[short_name] = { + 'path': include_fname, + 'content': included_file_content + }; + }).fail(function () { + console.error("Failed to load:", include_fname); + }); + } + + if (manifestFile.variables) { + if (!Array.isArray(manifestFile.variables)) { + manifestFile['variables'] = Array(manifestFile.variables); + } + $(manifestFile.variables).each(function (_, i) { + let include_fname = i; + // console.log("Variables:" , include_fname); + + // Modify include_fname based on the current domain + if (include_fname.startsWith("/") && currentDomain.includes("livelabs.oracle.com")) { + include_fname = "/cdn/" + include_fname.replace(/^\/+/, ""); // Ensure correct path + } else if (include_fname.startsWith("/") && currentDomain.includes("apexapps-stage.oracle.com")) { + include_fname = "/livelabs/cdn/" + include_fname.replace(/^\/+/, ""); // Ensure correct path + } + console.log("Variables:" , include_fname); + + $.getJSON(include_fname, function (variables) { + if (!manifestFile['variable_values']) { + manifestFile['variable_values'] = {}; + } + $.extend(manifestFile['variable_values'], variables); + }); + }) + } + + manifest_global = manifestFileContent = manifestFile; //reading the manifest file and storing content in manifestFileContent variable + }), + $.getScript(highlight, function () { + console.log("Highlight.js loaded!"); + }), + freeSqlLoaderReady.promise() + ).done(function () { + init(); + let selectedTutorial = setupTutorialNav(manifestFileContent); //populate side navigation based on content in the manifestFile + let articleElement = document.createElement('article'); //creating an article that would contain MD to HTML converted content + + loadTutorial(articleElement, selectedTutorial, manifestFileContent, toggleTutorialNav); + + prepareToc(manifestFileContent); + setupRelatedSection(manifestFileContent); + + setTimeout(function () { + if (location.hash.slice(1)) + expandSectionBasedOnHash($("li[data-unique='" + location.hash.slice(1) + "']")); + + if ($('#leftNav-toc').hasClass('scroll')) + $('.selected')[0].scrollIntoView(true); + }, 1000); + }); + }); + + // specifies when to do when window is scrolled + $(window).scroll(function () { + // if ($('#contentBox').height() > $('#leftNav-toc').height() || ($('#leftNav-toc').height() + $('header').height()) > $(window).height()) { + if (($('#contentBox').outerHeight() + $('header').outerHeight() + $('footer').outerHeight()) > $(window).outerHeight()) { + $('#leftNav-toc').addClass("scroll"); + + if (($(window).scrollTop() + $(window).height()) > $('footer').offset().top) { //if footer is seen + $('#leftNav-toc').css('max-height', $('footer').offset().top - $('#leftNav-toc').offset().top); + } else { + $('#leftNav-toc').css('max-height', $(window).height() - $('header').height()); + } + } else { + $('#leftNav-toc').removeClass("scroll"); + } + + try { + if ((document.querySelector('.selected .active').getBoundingClientRect().y + document.querySelector('.selected .active').clientHeight) > $(window).height() && $('#leftNav-toc').hasClass("scroll")) + $('.selected .active')[0].scrollIntoView(false); + } catch (e) { console.debug('TOC scroll error:', e); } + + let active = $('#contentBox').find('[data-unique]').first(); + $('#contentBox').find('[data-unique]').each(function () { + if (($(this).offset().top - $(window).scrollTop() - $('header').height()) < Math.abs($(active).offset().top - $(window).scrollTop())) { + active = $(this); + } + }); + $('.selected .toc .toc-item').removeClass('active'); + $('.selected .toc').find('[data-unique="' + $(active).attr('data-unique') + '"]').addClass('active'); + }); + + $(window).on('hashchange load', function (e) { + try { // if next or previous is not available then it raises exception + let position = extendedNav[e.target.location.hash] + if (position !== undefined) + changeTutorial(getMDFileName(selectTutorial(manifest_global, position).filename)); + + setTimeout(function () { + // Cause a subtle change in the parent page to trigger Google Translate + // if (window.parent && window.parent.document) { + let body = window.parent.document.body; + + // Find or create a subtle trigger element + let triggerElement = window.parent.document.getElementById("translation-trigger"); + if (!triggerElement) { + triggerElement = window.parent.document.createElement("span"); + triggerElement.id = "translation-trigger"; + triggerElement.style.display = "none"; // Keep it invisible + body.appendChild(triggerElement); + } + + // Toggle text content to force translation detection + triggerElement.textContent = triggerElement.textContent === "." ? " " : "."; + console.log("Translation trigger updated:", triggerElement); + // } + }, 500); // Adjust delay as needed + } catch (e) { console.debug('Hash change error:', e); } + }); + + let init = function () { + // hide header if the url contains header=hide + let header_param = getParam(header_param_name); + if (header_param == 'hide') { + $('header').hide(); + $('body').css("padding-top", "0px"); + } + // $('.hol-Header-actions').prependTo('.hol-Header-wrap').show(); + $('.hol-Header-actions').prependTo('.hol-Header-wrap'); + $('
    ').appendTo(".hol-Header-logo")[0]; + + $('#openNav').click(function () { + let nav_param = getParam(nav_param_name); + if (!nav_param || nav_param === 'open') { + window.history.pushState('', '', setParam(window.location.href, nav_param_name, 'close')); + } else if (nav_param === 'close') { + window.history.pushState('', '', setParam(window.location.href, nav_param_name, 'open')); + } + toggleTutorialNav(); + }); + + $('.hol-Footer-topLink').after($(document.createElement('a')).addClass('hol-Footer-rightLink hide')); + $('.hol-Footer-topLink').before($(document.createElement('a')).addClass('hol-Footer-leftLink hide')); + $('#contentBox').css('min-height', $(window).height() - $('header').outerHeight() - $('footer').outerHeight()); + $('.hol-Header-actions').show('slide'); + } + + /* + * ============================================ + * SECTION 3: MARKDOWN PROCESSING + * ============================================ + */ + + /** + * Loads and renders a tutorial from markdown + * @param {HTMLElement} articleElement - Container for rendered content + * @param {Object} selectedTutorial - Tutorial object from manifest + * @param {Object} manifestFileContent - Full manifest configuration + * @param {Function} [callbackFunc=null] - Optional callback after load + */ + const loadTutorial = function (articleElement, selectedTutorial, manifestFileContent, callbackFunc = null) { + let tut_fname; + + // const currentDomain = window.location.origin; // e.g., "https://livelabs.oracle.com" + // console.log("Current domain:", currentDomain); + + // Modify tut_fname based on the current domain + if (selectedTutorial.filename.startsWith("/") && currentDomain.includes("livelabs.oracle.com")) { + tut_fname = "/cdn/" + selectedTutorial.filename.replace(/^\/+/, ""); // Ensure correct path + } else if (selectedTutorial.filename.startsWith("/") && currentDomain.includes("apexapps-stage.oracle.com")) { + tut_fname = "/livelabs/cdn/" + selectedTutorial.filename.replace(/^\/+/, ""); // Ensure correct path + } else { + tut_fname = selectedTutorial.filename; + } + + $.get(tut_fname, function (markdownContent) { //reading MD file in the manifest and storing content in markdownContent variable + console.log(tut_fname + " loaded!"); + + if (selectedTutorial.filename == 'preview' && markdownContent == "None") { + markdownContent = window.localStorage.getItem("mdValue"); + } + + markdownContent = include(markdownContent, manifestFileContent.include); // added for include feature: [DBDOC-2434] Include any file inside of Markdown before rendering + markdownContent = substituteVariables(markdownContent, manifestFileContent.variable_values); // added for variable feature + markdownContent = calculateEstimatedTime(markdownContent); // calculates and replaces "Estimated Time: X" placeholder + markdownContent = singlesource(markdownContent, selectedTutorial.type); // implement show/hide feature based on the if tag (DBDOC-2430) + markdownContent = convertFreeSQLButtonTags(markdownContent); // converts tags to actual FreeSQL buttons + markdownContent = convertBracketInsideCopyCode(markdownContent); // converts <> tags inside copy tag to < and > (DBDOC-2404) + markdownContent = addPathToImageSrc(markdownContent, tut_fname); //adding the path for the image based on the filename in manifest + markdownContent = addPathToTypeHrefs(markdownContent); // if type is specified in the markdown, then add absolute path for it. + markdownContent = convertSingleLineCode(markdownContent); + markdownContent = convertQuizBlocks(markdownContent); // converts ```quiz blocks to interactive quiz HTML + markdownContent = convertCodeBlocks(markdownContent); // codeblock with multiple breaks don't render correctly, so I convert to codeblock here itself + + $(articleElement).html(new showdown.Converter({ + tables: true, //allows tables to rendered + parseImgDimensions: true, //allows image dimension to be specified in the markdown + metadata: true, // allows metadata to be added between --- and --- tags at the top of the markdown + simplifiedAutoLink: true, //transform http addresses automatically in to clickable URLs in HTML + strikethrough: true //allow strikethrough formatting + }).makeHtml(markdownContent)); //converting markdownContent to HTML by using showdown plugin + + articleElement = updateOpenCloseButtonText(articleElement, manifestFileContent); // in the manifest file, you can specify task_type to specify different text + articleElement = showRightAndLeftArrow(articleElement, manifestFileContent); + articleElement = renderVideoHubVideos(articleElement); //adds iframe to Oracle Video Hub videos + articleElement = renderYouTubeVideos(articleElement); //adds iframe to YouTube videos + articleElement = renderDirectVideos(articleElement); //adds HTML5 video element for direct video URLs + articleElement = updateH1Title(articleElement); //adding the h1 title in the Tutorial before the container div and removing it from the articleElement + articleElement = wrapSectionTag(articleElement); //adding each section within section tag + articleElement = wrapImgWithFigure(articleElement); //Wrapping images with figure, adding figcaption to all those images that have title in the MD + articleElement = addPathToAllRelativeHref(articleElement, tut_fname); //adding the path for all HREFs based on the filename in manifest + articleElement = setH2Name(articleElement); + articleElement = makeAnchorLinksWork(articleElement); //if there are links to anchors (for example: #hash-name), this function will enable it work + articleElement = addTargetBlank(articleElement); //setting target for all ahrefs to _blank + articleElement = allowCodeCopy(articleElement); //adds functionality to copy code from codeblocks + articleElement = enableForceDownload(articleElement); // enables the force download feature (?download=1 must be mentioned at the end of the URL) + articleElement = injectUtmParams(articleElement); + articleElement = showTabs(articleElement, selectedTutorial.type); //show type options as tabs (DBDOC-2455) + articleElement = highlightCodeBlock(articleElement); // highlights the code in the codeblock (DBDOC-2494) + articleElement = addModalWindow(articleElement); // add modal window so that images open in full screen when clicked (DBDOC-2575) + updateHeadContent(selectedTutorial, manifestFileContent.workshoptitle); //changing document head based on the manifest + + // adding link to the support forum URL in the footer if the manifest file contains it (DBDOC-2459 and DBDOC-2496) + addGoToForumLink(manifestFileContent.support); + // adding social media link to the header + // addSocialMediaLink(manifestFileContent.help, manifestFileContent.workshoptitle); + // adding link to the Neep Help URL in the header if the manifest file contains it (DBDOC-2496) + + // KP Translate + $(document).ready(function () { + addTranslateIcon(manifestFileContent.help); + + $(document).on('click', function (e) { + if (!$(e.target).closest('#translate_icon, #translate_popup').length) { + $('#translate_popup').hide(); + } + }); + }); + addNeedHelpLink(manifestFileContent.help); + + if (getParam("qa") == "true") { + articleElement = performQA(articleElement, markdownContent, manifestFileContent); + } + }).done(function () { + $("main").html(articleElement); //placing the article element inside the main tag of the Tutorial template + setTimeout(function () { + setupContentNav(manifestFileContent, articleElement); + }, 0); //sets up the collapse/expand button and open/close section feature + + + + //FOllowing code will make sure that landmarks have a unique title (LLAPEX-401) + document.getElementsByTagName("header")[0].setAttribute("title", "livelabs header"); + document.getElementsByTagName("main")[0].setAttribute("title", "livelabs main"); + document.getElementsByTagName("footer")[0].setAttribute("title", "livelabs footer"); + //END of fix for landmarks + + // Following code makes tables accessible (see LLAPEX-403) + $("table").attr("role", "presentation"); //add role to table + + var i = 0; + var tables = document.getElementsByTagName("table"); + let title = document.querySelector('title').innerText; + let caption_start = '{: title="'; + + for (i; i < tables.length; i++) { + var table = tables[i]; + var capt = table.createCaption(); + let given_title = null; + let next_element = $($(tables)[i]).find('tr').last(); + if (next_element.text().trim().startsWith(caption_start)) { + given_title = next_element.text().trim().replace(caption_start, ""); + given_title = given_title.replace('"}', ""); + $(next_element).remove(); + } + var tit = capt.textContent = 'Table ' + (i + 1) + ': ' + (given_title || title); + table.setAttribute("role", "presentation"); + }; + // END OF TABLE ACCESSIBILITY ENHANCEMENT + + if (selectedTutorial.filename == 'preview') { + let uploaded_images = JSON.parse(window.localStorage.getItem("imagesValue")); + + // added for showing images in preview + if (uploaded_images !== null) { + $('main').find('img').each(function (i, imageFile) { + for (let i = 0; i < uploaded_images.length; i++) { + if ($(imageFile).attr('src').indexOf(uploaded_images[i].filename) >= 0) { + $(imageFile).attr('src', uploaded_images[i].src); + } + } + }); + } + } + + if (getParam("qa") == "true") { + dragElement(document.getElementById("qa-report")); + } else { + collapseSection($("#module-content h2:not(:eq(0))"), "none"); //collapses all sections by default + } + + if (callbackFunc) + callbackFunc(); + + }).fail(function () { + console.log(selectedTutorial.filename + ' not found! Please check that the file is available in the location provided in the manifest file.'); + }); + } + + let convertSingleLineCode = function (markdown) { + let regex_type = new RegExp(/`{3,4}(.*?)`{3,4}/g); + let contentToReplace = []; + + let matches; + do { + matches = regex_type.exec(markdown); + if (matches === null) { + $(contentToReplace).each(function (index, value) { + markdown = markdown.replace(value.replace, value.with); + }); + return markdown; + } + + contentToReplace.push({ + "replace": matches[0], + "with": '`' + matches[1] + '`' + }); + + } while (matches); + } + + //DBDOC-2591: Code blocks break when line breaks (empty lines) are added + let convertCodeBlocks = function (markdown) { + let regex_type = new RegExp(/`{3,}(.*?)\n([\s\S|\n]*?)`{3,}/g); + let matches, remove, remove_space_regex; + let contentToReplace = []; + + do { + let pre_tag = "
    ";
    +            matches = regex_type.exec(markdown);
    +            if (matches === null) {
    +                $(contentToReplace).each(function (index, value) {
    +                    // replace using split because the string has regex
    +                    markdown = markdown.split(value.replace).join(value.with);
    +                });
    +                return markdown;
    +            }
    +            // else
    +            remove = matches[2].substring(0, matches[2].indexOf(matches[2].trim())).replace(/\t/g, '    ');
    +            // remove_space_regex = new RegExp("^" + remove, "gm");
    +
    +            if (matches[1].trim().length !== 0) {
    +                pre_tag = '
    ';
    +            }
    +
    +            let replace_with = matches[2].replace(/\t/g, '    ').split('\n');
    +
    +            for (let i = 0; i < replace_with.length; i++) {
    +                replace_with[i] = replace_with[i].replace(remove, '');
    +            }
    +            replace_with = replace_with.join('\n');
    +
    +            contentToReplace.push({
    +                "replace": matches[0],
    +                "with": pre_tag + '' + replace_with.trim() + '
    ' + // "with": pre_tag + '' + matches[2].replace(/(?=[\r\n])\r?\n?/g,"\n") + '
    ' + // "with": pre_tag + '' + matches[2].replace(remove_space_regex, '').trim().replace(/\t/g, '') + '' + }); + } while (matches); + } + + /** + * Converts ```quiz-config blocks to store quiz scoring configuration + * Syntax: + * passing: 80 + * badge: images/badge.png + */ + let convertQuizConfig = function (markdown) { + let configRegex = /`{3,}quiz-config\s*\n([\s\S]*?)`{3,}/g; + + return markdown.replace(configRegex, function (match, content) { + let config = { + passing: 80, + badge: null + }; + + let lines = content.trim().split('\n'); + lines.forEach(function (line) { + // Allow optional whitespace at start of line + let passingMatch = line.match(/^\s*passing:\s*(\d+)/i); + let badgeMatch = line.match(/^\s*badge:\s*(.+)/i); + + if (passingMatch) { + config.passing = parseInt(passingMatch[1], 10); + } + if (badgeMatch) { + config.badge = badgeMatch[1].trim(); + } + }); + + // Return a hidden div with config data + return ''; + }); + } + + /** + * Converts ```quiz code blocks to interactive quiz HTML + * Syntax: + * Q: Question text here + * * Correct answer (asterisk marks correct) + * - Wrong answer (dash marks incorrect) + * > Optional explanation shown after answering + * + * Add 'score' after quiz to include in scoring: ```quiz score + * Multiple correct answers = checkboxes, single correct = radio buttons + */ + let convertQuizBlocks = function (markdown) { + // First process quiz-config blocks + markdown = convertQuizConfig(markdown); + + let quizRegex = /`{3,}quiz(\s+score)?\s*\n([\s\S]*?)`{3,}/g; + let quizId = 0; + let scoredQuizCount = 0; + + let result = markdown.replace(quizRegex, function (match, scoreFlag, content) { + let html = ''; + let isScored = scoreFlag && scoreFlag.trim() === 'score'; + let lines = content.trim().split('\n'); + let currentQuestion = null; + let questions = []; + + // Parse quiz content + for (let i = 0; i < lines.length; i++) { + let line = lines[i].trim(); + + if (line.match(/^Q:\s*/i)) { + // New question + if (currentQuestion) { + questions.push(currentQuestion); + } + currentQuestion = { + text: line.replace(/^Q:\s*/i, ''), + options: [], + explanation: null + }; + } else if (line.match(/^\*\s+/)) { + // Correct answer + if (currentQuestion) { + currentQuestion.options.push({ + text: line.replace(/^\*\s+/, ''), + correct: true + }); + } + } else if (line.match(/^-\s+/)) { + // Incorrect answer + if (currentQuestion) { + currentQuestion.options.push({ + text: line.replace(/^-\s+/, ''), + correct: false + }); + } + } else if (line.match(/^>\s*/)) { + // Explanation + if (currentQuestion) { + currentQuestion.explanation = line.replace(/^>\s*/, ''); + } + } + } + + // Don't forget the last question + if (currentQuestion) { + questions.push(currentQuestion); + } + + // Generate HTML for each question + questions.forEach(function (q, qIndex) { + let qId = 'quiz-' + quizId + '-q' + qIndex; + let correctCount = q.options.filter(o => o.correct).length; + let inputType = correctCount > 1 ? 'checkbox' : 'radio'; + let inputName = qId + '-options'; + + if (isScored) { + scoredQuizCount++; + } + + html += '
    '; + + // Add header with score display for scored quizzes + if (isScored) { + html += '
    Scored Quiz
    '; + } + + html += '
    ' + q.text + '
    '; + html += '
    '; + + q.options.forEach(function (opt, optIndex) { + let optId = qId + '-opt' + optIndex; + html += ''; + }); + + html += '
    '; + + if (q.explanation) { + html += ''; + } + + html += ''; + html += ''; + html += '
    '; + html += '
    '; + + quizId++; + }); + + return html; + }); + + // If there are scored quizzes, add a hidden tracker for state and badge container + if (scoredQuizCount > 0) { + result += '
    '; + result += ''; + result += '
    '; + } + + return result; + } + + // DBDOC-2575: Add ability to expand images to full screen + let addModalWindow = function (articleElement) { + let modalClose = $(document.createElement('span')).attr('id', 'modalClose').html("×"); + let modalImg = $(document.createElement('img')).attr('id', 'modalImg'); + let modalCaption = $(document.createElement('div')).attr('id', 'modalCaption'); + let modalWindow = $(document.createElement('div')).attr('id', 'modalWindow'); + + $(modalWindow).append([modalCaption, modalClose, modalImg]); + $(articleElement).append(modalWindow); + + $(articleElement).find('img').click(function () { + $(modalImg).attr({ src: this.src, alt: this.alt }); + $(modalWindow).addClass('show'); + $(modalCaption).text(this.alt); + }) + $(modalWindow).click(function () { + $(modalWindow).removeClass('show'); + }) + + return articleElement; + } + + // DBDOC-2455: Support for content selectable via tabs + let showTabs = function (articleElement, type) { + if ($.type(type) == "object") { // if true, it means select tab needs to be added + let div = $(document.createElement('div')).addClass('selection_tabs'); + let tab = $(document.createElement('ul')).addClass('tab'); + + if (getParam("type") == false) { + window.history.pushState('', '', setParam(window.location.href, 'type', Object.keys(type)[0])); + } + + $(Object.keys(type)).each(function (_, type_key) { + let li = $(document.createElement('li')).addClass('btn_if_' + type_key); + // $(li).html('' + type[type_key] + ''); + $(li).html('' + type[type_key] + ''); + $(li).find('a').click(function () { + $(this).attr('href', setParam(window.location.href, 'type', type_key)); + }); + $(tab).append(li); + + if (type_key == getParam("type")) { + $(li).find('a').addClass('active'); + } + }); + + $(div).append(tab); + $(articleElement).find('h2:not(:eq(0))').after(div); + $(articleElement).find('h1').after(div); + $(articleElement).find('.selection_tabs:not(:eq(0))').addClass('stick'); + } + + return articleElement; + } + // DBDOC-2494: added for syntax highlight feature. The syntax highlight feature uses the highlight.js plugin. + let highlightCodeBlock = function (articleElement) { + $(articleElement).find('pre:not(.nohighlighting) code').each(function (_, block) { + hljs.highlightBlock(block); + }); + return articleElement; + } + + // DBDOC-2449: added for force download feature. To force download a file referenced in the link, append '?download=1' to the link. + let enableForceDownload = function (articleElement) { + $(articleElement).find('a[href$="?download=1"]').each(function () { // loop through each link that ends with ?download=1 + $(this).attr('download', ''); // set download attribute to the link + $(this)[0].href = $(this)[0].href.replace('?download=1', ''); // removes ?download=1 from the link + }); + return articleElement; + } + + // added for include feature: [DBDOC-2434] Include any file inside of Markdown before rendering + let include = function (markdown, include) { + for (let short_name in include) { + if (typeof include[short_name] !== 'object') + continue; + include[short_name]['content'] = addPathToImageSrc(include[short_name]['content'], include[short_name]['path']); + // console.log("include function: " ,include[short_name]['path'] ); + markdown = markdown.split("[](include:" + short_name + ")").join(include[short_name]['content']); + } + return markdown; + } + + // added for variable substitute feature + let substituteVariables = function (markdown, all_variables) { + for (let variable in all_variables) { + markdown = markdown.split("[](var:" + variable + ")").join(all_variables[variable]); + } + return markdown; + } + + /** + * Calculates estimated reading time and replaces "Estimated Time: X" or "Estimated Time: x" + * Only replaces if the pattern contains exactly X or x as placeholder + * @param {string} markdown - The markdown content + * @returns {string} - Markdown with calculated time replacing placeholder + */ + let calculateEstimatedTime = function (markdown) { + // Only match exactly "Estimated Time: X" or "Estimated Time: x" + // Use [ \t]* instead of \s* to avoid consuming newlines + const placeholderPattern = /Estimated Time:[ \t]*([Xx])[ \t]*(?:minutes?)?/; + const match = markdown.match(placeholderPattern); + + // If no placeholder found, return unchanged + if (!match) { + return markdown; + } + + // Extract code blocks first (to count separately) + const codeBlockPattern = /```[\s\S]*?```/g; + const codeBlocks = markdown.match(codeBlockPattern) || []; + + // Remove code blocks from markdown for regular word count + let textContent = markdown.replace(codeBlockPattern, ''); + + // Count images + const imagePattern = /!\[.*?\]\(.*?\)/g; + const imageCount = (markdown.match(imagePattern) || []).length; + + // Remove markdown syntax for cleaner word count + textContent = textContent + .replace(/!\[.*?\]\(.*?\)/g, '') // Remove images + .replace(/\[.*?\]\(.*?\)/g, '') // Remove links + .replace(/#{1,6}\s*/g, '') // Remove headers + .replace(/[*_`~]/g, '') // Remove formatting + .replace(/<[^>]+>/g, '') // Remove HTML tags + .replace(/\|/g, ' ') // Replace table pipes + .replace(/[-=]{3,}/g, ''); // Remove horizontal rules + + // Count words in regular text + const textWords = textContent.split(/\s+/).filter(word => word.length > 0).length; + + // Count words in code blocks + let codeWords = 0; + codeBlocks.forEach(block => { + const codeContent = block.replace(/```\w*\n?/g, '').replace(/```/g, ''); + codeWords += codeContent.split(/\s+/).filter(word => word.length > 0).length; + }); + + // Calculate reading time + // Text: 225 words per minute (middle of 200-250) + // Code: 200 words per minute (10% slower) + // Images: 12 seconds each + const textMinutes = textWords / 225; + const codeMinutes = codeWords / 200; + const imageMinutes = (imageCount * 12) / 60; + + // Total time with 10% added for hands-on instructions + let totalMinutes = (textMinutes + codeMinutes + imageMinutes) * 1.10; + + // Round up to next 5-minute increment + totalMinutes = Math.ceil(totalMinutes / 5) * 5; + + // Ensure minimum of 5 minutes + if (totalMinutes < 5) { + totalMinutes = 5; + } + + // Replace the placeholder with calculated time + markdown = markdown.replace(placeholderPattern, 'Estimated Time: ' + totalMinutes + ' minutes'); + + console.log('Estimated Time calculated:', totalMinutes, 'minutes (text:', textWords, 'words, code:', codeWords, 'words, images:', imageCount + ')'); + + return markdown; + } + + let addPathToTypeHrefs = function (markdown) { + let regex_type = new RegExp(/\[(?:.+?)\]\((&type=(\S*?))\)/g); + let matches; + + do { + matches = regex_type.exec(markdown); + if (matches !== null) { + markdown = markdown.replace(matches[1], setParam(window.location.href, "type", matches[2])); + } + } while (matches); + + return markdown; + } + + let arrowClick = function () { + if ($(this).text() === '-') { + $(this).next().next().fadeOut('fast', function () { + $(window).scroll(); + }); + $(this).text('+'); + } else { + $(this).next().next().fadeIn('fast', function () { + $(window).scroll(); + }); + $(this).text('-'); + } + } + + let setupRelatedSection = function (manifestFileContent) { + // this part has been added for LLAPEX-448 + const max_related = 5; + let related_li = []; + if ('show_related' in manifestFileContent) { + let related_content; + let tut_titles = []; + + for (let i = 0; i < manifestFileContent.tutorials.length; i++) { + tut_titles[i] = manifestFileContent.tutorials[i].title.toLowerCase(); + } + + for (let i = 0; i < manifestFileContent.show_related.length; i++) { + if (!('filename' in manifestFileContent.show_related[i]) || !('tags' in manifestFileContent.show_related[i]) || !('title' in manifestFileContent.show_related[i])) { + continue; + } + $.getJSON(related_path + manifestFileContent.show_related[i]['filename'], function (content) { + related_content = content; + }).done(function () { + related_li[i] = $(document.createElement('li')).attr('id', 'related-content-' + i).css({ 'border-bottom': '0px', 'padding-left': '36px', 'cursor': 'default', 'background-color': 'rgb(0,0,0,0.06)' }); + + let div_main = $(document.createElement('div')); + let a = $(document.createElement('a')).css('cursor', 'pointer'); + let arrow, div; + + $(a).click(function () { + $(this).prev().click(); + }); + $(a).append($(document.createElement('div')).text(manifestFileContent.show_related[i]['title']).css({ 'font-weight': '600' })); + $(div_main).append(a); + $(related_li[i]).append(div_main); + div = $(document.createElement('div')).attr('id', 'toc-related-' + i).addClass('toc'); + $(div_main).append(div); + + if ('state' in manifestFileContent.show_related[i] && manifestFileContent.show_related[i]['state'] === "collapsed") { + $(div).hide(); + arrow = $(document.createElement('div')).addClass('arrow').text('+'); + } else { + arrow = $(document.createElement('div')).addClass('arrow').text('-'); + } + + $(arrow).css('cursor', 'pointer').click(arrowClick); + $(div_main).prepend(arrow); + $("#leftNav-toc ul.hol-Nav-list:first-of-type").append(related_li[i]); + + // for each related workshop + let related_workshops = {}; + let tags = manifestFileContent.show_related[i]['tags']; + $(tags).each(function (_, tag) { + related_workshops = { ...related_workshops, ...related_content[tag] }; + }); + + let filtered_workshops = {}; + + for (let j = 0; j < Object.keys(related_workshops).length; j++) { + if (manifestFileContent.workshoptitle.toLowerCase() === Object.keys(related_workshops)[j].toLowerCase()) continue; + if ($.inArray(Object.keys(related_workshops)[j].toLowerCase(), tut_titles) != -1) continue; + + filtered_workshops[Object.keys(related_workshops)[j]] = related_workshops[Object.keys(related_workshops)[j]]; + } + + let filter = Object.keys(filtered_workshops).sort(() => Math.random() - Math.random()).slice(0, max_related); + + $(filter).each(function (_, f) { + let ul = document.createElement('ul'); + let li = $(document.createElement('li')).addClass('toc-item').text(f); + $(li).wrapInner(''); + $(ul).append(li); + $(ul).appendTo(div); + }); + }); + } + } + } + let prepareToc = function (manifestFileContent) { + let h2_regex = new RegExp(/^##\s(.+)*/gm); + let h2s_list = []; + let matches; + let tut_fname; + + // const currentDomain = window.location.origin; // e.g., "https://livelabs.oracle.com" + + $(manifestFileContent.tutorials).each(function (i, tutorial) { + let ul; + let div = document.createElement('div'); + $(div).attr('id', 'toc' + i).addClass('toc'); + + // Modify tut_fname based on the current domain + if (tutorial.filename.startsWith("/") && currentDomain.includes("livelabs.oracle.com")) { + tut_fname = "/cdn/" + tutorial.filename.replace(/^\/+/, ""); // Ensure correct path + } else if (tutorial.filename.startsWith("/") && currentDomain.includes("apexapps-stage.oracle.com")) { + tut_fname = "/livelabs/cdn/" + tutorial.filename.replace(/^\/+/, ""); // Ensure correct path + } else { + tut_fname = tutorial.filename; + } + + $.get(tut_fname, function (markdownContent) { //reading MD file in the manifest and storing content in markdownContent variable + if (tutorial.filename == 'preview' && markdownContent == "None") { + markdownContent = window.localStorage.getItem("mdValue"); + } + markdownContent = include(markdownContent, manifestFileContent.include); + markdownContent = singlesource(markdownContent, tutorial.type); + + do { + matches = h2_regex.exec(markdownContent); + + if (matches !== null) { + ul = document.createElement('ul'); + $(ul).append($(document.createElement('li')).addClass('toc-item').text(matches[1].replace(/\**/g, '').replace(/\##/g, '')).attr('data-unique', alphaNumOnly(matches[1]))); + $(ul).click(function () { + if ($(this).parent().parent().parent().hasClass('selected')) { + location.hash = alphaNumOnly($(this).text()); + expandSectionBasedOnHash($(this).find('li').attr('data-unique')); + } else { + changeTutorial(getMDFileName(tutorial.filename), alphaNumOnly($(this).text())); + } + + }); + + // fix added for LLAPEX-400 + $(ul).each(function () { + if (tutorial !== selectTutorial(manifestFileContent)) { + let li = $(this).find('li')[0]; + $(li).wrapInner(''); + } + }); + $(ul).appendTo(div); + } + } while (matches); + + }); + + $('.hol-Nav-list li')[i].append(div); + }); + + setTimeout(function () { + let anchorItem = $('.selected li[data-unique="' + location.hash.slice(1) + '"]'); + if (anchorItem.length !== 0) + $(anchorItem)[0].click(); + }, 1000); + $(".hol-Nav-list>li").wrapInner("
    ") + + $(".hol-Nav-list>li>div").prepend($(document.createElement('div')).addClass('arrow').text('+')); + + $('.hol-Nav-list > li > div .arrow').click(arrowClick); + + $('.selected div.arrow').text('-'); + $('.hol-Nav-list > li:not(.selected) .toc').hide(); + + } + + let toggleTutorialNav = function () { + let nav_param = getParam(nav_param_name); + + if (!nav_param || nav_param === 'open') { + $('.hol-Nav-list > li:not(.selected)').attr('tabindex', '0'); + $('#leftNav-toc, #leftNav, #contentBox').addClass('open').removeClass('close'); + } else if (nav_param === 'close') { + $('.hol-Nav-list > li:not(.selected)').attr('tabindex', '-1'); + $('#leftNav-toc, #leftNav, #contentBox').addClass('close').removeClass('open'); + } + setTimeout(function () { + $(window).scroll(); + }, 100); + } + + /* + * ============================================ + * SECTION 4: NAVIGATION + * ============================================ + */ + + /** + * Creates and populates the tutorial navigation sidebar + * @param {Object} manifestFileContent - The manifest file content + * @returns {Object} The selected tutorial object + */ + const setupTutorialNav = function (manifestFileContent) { + let div = $(document.createElement('div')).attr('id', 'leftNav-toc'); + let ul = $(document.createElement('ul')).addClass('hol-Nav-list'); + + $(manifestFileContent.tutorials).each(function (i, tutorial) { + let file_name = getMDFileName(tutorial.filename); + + $(document.createElement('li')).each(function () { + $(this).click(function (e) { + if (!$(e.target).hasClass('arrow') && !$(e.target).hasClass('toc-item') && !$(e.target).hasClass('toc-item active')) { + if ($(e.target).parent().parent().hasClass('selected') || $(e.target).hasClass('selected')) { + try { + $('.selected .arrow').click(); + } catch (err) { console.debug('Nav click error:', err); } + } else { + changeTutorial(file_name); + } + } + }); + $(this).attr('id', getLabNavID(file_name)); + //The title specified in the manifest appears in the side nav as navigation + // $(this).text(tutorial.title).wrapInner(""); + $(this).text(tutorial.title).wrapInner("
    "); + $(this).appendTo(ul); + + /* for accessibility */ + $(this).keydown(function (e) { + if (e.keyCode === 13 || e.keyCode === 32) { //means enter and space + e.preventDefault(); + changeTutorial(file_name); + } + }); + /* accessibility code ends here */ + }); + }); + + $(ul).appendTo(div); + $(div).appendTo('#leftNav'); + return selectTutorial(manifestFileContent); + } + + let getMDFileName = function (file_name) { + return file_name.split('/')[file_name.split('/').length - 1].replace('.md', ''); + } + + let getLabNavID = function (file_name, prefix = 'tut-') { + return prefix + getMDFileName(file_name.toString()).replace(/[\(\)]+?/g, '').replace('.md', ''); + } + + let selectTutorial = function (manifestFileContent, position = 0) { + $('#' + getLabNavID(getParam(queryParam))).addClass('selected'); //add class selected to the tutorial that is selected by using the ID + $('.selected').find('a').contents().unwrap(); // remove hyperlink from "selected" lab + $('.selected').unbind('keydown'); + + if (position === -2) return manifestFileContent.tutorials[0]; + if (position === 2) return manifestFileContent.tutorials[manifestFileContent.tutorials.length - 1]; + + //find which tutorial in the manifest file is selected + for (var i = 0; i < manifestFileContent.tutorials.length; i++) { + if (getParam(queryParam) === getMDFileName(manifestFileContent.tutorials[i].filename)) + return manifestFileContent.tutorials[i + position]; + } + + // if old link style URL is used (for example: ?labs=short-tutorial-title) + // remove this condition after old style link is removed + for (var i = 0; i < manifestFileContent.tutorials.length; i++) { + if (getParam(queryParam) === createShortNameFromTitle(manifestFileContent.tutorials[i].title)) { + changeTutorial(getMDFileName(manifestFileContent.tutorials[i].filename), window.location.hash.substr(1)); + return; + } + } + // until here + + //if no title has selected class, selected class is added to the first class + $('.hol-Nav-list').find('li:eq(0)').addClass("selected"); + return manifestFileContent.tutorials[0 + position]; //return the first tutorial is no tutorial is matches + } + + /* Setup toc navigation and tocify */ + let setupTocNav = function () { + $(".hol-Nav-list .selected").wrapInner("
    ") + $(".hol-Nav-list .selected div").prepend($(document.createElement('div')).addClass('arrow').text('+')); + $(".hol-Nav-list .selected").unbind('click'); + + $(".hol-Nav-list .selected > div").click(function (e) { + if ($('.selected div.arrow').text() === '-') { + $('#toc').fadeOut('fast'); + $('.selected div.arrow').text('+'); + } else { + $('#toc').fadeIn('fast'); + $('.selected div.arrow').text('-'); + } + }); + + /* for accessibility */ + $(".hol-Nav-list .selected > div").keydown(function (e) { + if (e.keyCode === 13 || e.keyCode === 32) { //means enter and space + e.preventDefault(); + $(this).click() + } + }); + /* accessibility code ends here */ + + $(window).scroll(); + $('#toc').appendTo(".hol-Nav-list .selected"); + $('.selected div.arrow').click(); + } + + /* The following function performs the event that must happen when the lab links in the navigation is clicked */ + let changeTutorial = function (file_name, anchor = "") { + + if (anchor !== "") anchor = '#' + anchor; + location.href = unescape(setParam(window.location.href, queryParam, file_name) + anchor); + } + + /*the following function changes the path of images as per the path of the MD file. + This ensures that the images are picked up from the same location as the MD file. + The manifest file can be in any location.*/ + let addPathToImageSrc = function (markdownContent, myUrl) { + let imagesRegExp = new RegExp(/!\[.*?\]\((.*?)\)/g); + let contentToReplace = []; // content that needs to be replaced + let matches; + + myUrl = myUrl.substring(0, myUrl.lastIndexOf('/') + 1); //removing filename from the url + + do { + matches = imagesRegExp.exec(markdownContent); + // console.log(matches); + if (matches === null) { + $(contentToReplace).each(function (index, value) { + markdownContent = markdownContent.replace(value.replace, value.with); + }); + return markdownContent; + } + + // if (myUrl.indexOf("/") !== 1) { + matches[1] = matches[1].split(' ')[0]; + let origImg = matches[1].trim(); + if (matches[1].indexOf("http") === -1 && matches[1][0] !== "/") { + contentToReplace.push({ + "replace": '(' + matches[1], + /* "with": '(' + myUrl + matches[1] TMM: changed 10/6/20*/ + "with": '(' + myUrl + matches[1].trim() + }); + } + + if (["livelabs.oracle.com", "apexapps-stage.oracle.com"].some(domain => currentDomain.includes(domain)) + && !origImg.startsWith("/cdn/") && !origImg.startsWith("/livelabs/cdn/") && origImg.startsWith("/")) { + let replaceImg = origImg; // Default to the original path + + if (currentDomain.includes("livelabs.oracle.com")) { + replaceImg = "/cdn" + origImg; + } else if (currentDomain.includes("apexapps-stage.oracle.com")) { + replaceImg = "/livelabs/cdn" + origImg; + } + + contentToReplace.push({ + replace: `(${origImg}`, + with: `(${replaceImg}` + }); + + } + + } while (matches); + } + /* The following function adds the h1 title before the container div. It picks up the h1 value from the MD file. */ + let updateH1Title = function (articleElement) { + $('#tutorial-title').text("\t\t›\t\t" + $(articleElement).find('h1').text()); + // $(articleElement).find('h1').remove(); //Removing h1 from the articleElement as it has been added to the HTML file already + return articleElement; + } + /* This function picks up the entire converted content in HTML, and break them into sections. */ + let wrapSectionTag = function (articleElement) { + $(articleElement).find('h2').each(function () { + $(this).nextUntil('h2').andSelf().wrapAll('
    '); + }); + return articleElement; + } + /* Wrapping all images in the article element with Title in the MD, with figure tags, and adding figcaption dynamically. + The figcaption is in the format Description of illustration [filename]. + The image description files must be added inside the files folder in the same location as the MD file.*/ + let wrapImgWithFigure = function (articleElement) { + // Add lazy loading attribute to images + // First image loads eagerly (above the fold), rest load lazily + let isFirstImage = true; + $(articleElement).find("img").each(function () { + if (isFirstImage) { + isFirstImage = false; + $(this).attr('loading', 'eager'); + } else { + $(this).attr('loading', 'lazy'); + } + }); + + $(articleElement).find("img").on('load', function () { + if ($(this)[0].width > 100 || $(this)[0].height > 100 || $(this).attr("title") !== undefined) { // only images with title or width or height > 100 get wrapped (DBDOC-2397) + $(this).wrap("
    "); //wrapping image tags with figure tags + + } + + //Add role attribute to all images that do not have an alt attribute + if ($(this).attr("alt").length < 1 || (!$(this).attr("alt")) || $(this).attr("alt") == '' || $(this).attr("alt") == undefined || $(this).attr("alt") == 0) { + // $(this).attr("role","presentation"); ALternative solution + $(this).attr("alt", "The content is described above."); + } + + }); + return articleElement; + } + /*the following function changes the path of the HREFs based on the absolute path of the MD file. + This ensures that the files are linked correctly from the same location as the MD file. + The manifest file can be in any location.*/ + let addPathToAllRelativeHref = function (articleElement, myUrl) { + if (myUrl.indexOf("/") !== -1) { + myUrl = myUrl.replace(/\/[^\/]+$/, "/"); //removing filename from the url + $(articleElement).find('a').each(function () { + if ($(this).attr("href").indexOf("http") === -1 && $(this).attr("href")[0] !== "/" && $(this).attr("href").indexOf("?") !== 0 && $(this).attr("href").indexOf("#") !== 0) { + $(this).attr("href", myUrl + $(this).attr("href")); + } + }); + } + return articleElement; + } + /* the following function makes anchor links work by adding an event to all href="#...." */ + let makeAnchorLinksWork = function (articleElement) { + $(articleElement).find('a[href^="#"]').each(function () { + let href = $(this).attr('href'); + if (href !== "#") { //eliminating all plain # links + $(this).click(function () { + expandSectionBasedOnHash(href.split('#')[1]); + }); + } + }); + return articleElement; + } + /*the following function sets target for all HREFs to _blank */ + let addTargetBlank = function (articleElement) { + $(articleElement).find('a').each(function () { + if ($(this).attr('href').indexOf("http") === 0 && $(this).attr('href').indexOf("&type=") == -1) //ignoring # hrefs + $(this).attr('target', '_blank'); //setting target for ahrefs to _blank + }); + return articleElement; + } + /* Sets the title, contentid, description, partnumber, and publisheddate attributes in the HTML page. + The content is picked up from the manifest file entry*/ + let updateHeadContent = function (tutorialEntryInManifest, workshoptitle) { + (workshoptitle !== undefined) ? + document.title = workshoptitle + " | " + tutorialEntryInManifest.title : + document.title = tutorialEntryInManifest.title; + + const metaProperties = [{ + name: "contentid", + content: tutorialEntryInManifest.contentid + }, { + name: "description", + content: tutorialEntryInManifest.description + }, { + name: "partnumber", + content: tutorialEntryInManifest.partnumber + }, { + name: "publisheddate", + content: tutorialEntryInManifest.publisheddate + }]; + $(metaProperties).each(function (i, metaProp) { + if (metaProp.content) { + let metaTag = document.createElement('meta'); + $(metaTag).attr(metaProp).prependTo('head'); + } + }); + } + + /* Add the Go to forum link in the footer (DBDOC-2459 and DBDOC-2496) */ + let addGoToForumLink = function (support) { + const support_text = "Go to forum"; + if (support !== undefined) { + // the Need Help? URL is taken from the manifest file (key is support) + let need_help = $(document.createElement('li')).append($(document.createElement('a')).attr({ 'href': support, 'target': '_blank' }).text(support_text)); + $('.footer-links').append(need_help); + } + } + + /* Add the Need Help link in the header (DBDOC-2459 and DBDOC-2496) */ + let addNeedHelpLink = function (help, wtitle) { + const subject = "Question about workshop: " + wtitle; + const help_text = "Need help? Send us an email."; + if (help !== undefined) { + // the Need Help? URL is taken from the manifest file (key is help) + let need_help = $(document.createElement('a')).attr({ 'href': 'mailto:' + help + '?subject=' + subject, 'title': help_text, 'id': 'need_help', 'tabindex': '0' }).text('?').addClass('header-icon'); + $('header .hol-Header-wrap').append(need_help); + + // let need_help_div = $(document.createElement('div')).attr({ 'href': 'mailto:' + help + '?subject=' + subject, 'title': help_text, 'id': 'need_help', 'tabindex': '0' }).text('?'); + // $('div#container').append(need_help_div); + } + } + let addTranslateIcon = function (help) { + const help_text = "Need another language? Learn how to translate this page?"; + + if (help !== undefined) { + let translate_icon = $('', { + href: '#', + title: help_text, + id: 'translate_icon', + tabindex: '0' + }).html(` + + `) + .addClass('header-icon'); + + translate_icon.on('click', function (e) { + e.preventDefault(); + $('#translate_popup').toggle(); + }); + + // Detect browser + let userAgent = navigator.userAgent.toLowerCase(); + let defaultTab = 'chrome'; // fallback + if (userAgent.includes('edg')) { + defaultTab = 'edge'; + } else if (userAgent.includes('firefox')) { + defaultTab = 'firefox'; + } else if (userAgent.includes('safari') && !userAgent.includes('chrome') && !userAgent.includes('edg')) { + defaultTab = 'safari'; + } else if (userAgent.includes('chrome') && !userAgent.includes('edg')) { + defaultTab = 'chrome'; + } + + let popupContent = `
    +

    How to Translate This Page

    +

    You must be on the livelabs.oracle.com domain to use translations.
    + They are not available on apexapps.oracle.com.

    +

    For the best translation experience, we recommend Google Chrome.

    + + +
    + + + + +
    + + +
    +
      +
    1. Right-click anywhere on the page and choose “Translate to
      [Your Language]”
      .
    2. +
    3. If that option doesn’t appear, click the ⋮ three-dot menu in the
      top-right corner of Chrome.
    4. +
    5. Select “Translate” from the dropdown.
    6. +
    7. + Then, click the translate icon + Translate icon in the address bar. +
    8. +
    9. If needed, click the ⋮ three-dot menu within the Google
      Translate popup and choose your preferred language.
    10. +
    +
    + + + + + +
    + + + +
    + `; + + + + // + // + + let popup = $('
    ', { + id: 'translate_popup' + }).html(popupContent); + + $('header .hol-Header-wrap').append(translate_icon); + $('body').append(popup); + + // After popup is appended, activate the default tab + $(document).ready(function () { + // Set default active tab + $('.translation-tabs .tab').removeClass('active'); + $('.translation-tabs .tab[data-tab="' + defaultTab + '"]').addClass('active'); + + $('.tab-content').hide(); + $('#' + defaultTab).show(); + }); + + // Tab click behavior + $(document).on('click', '.translation-tabs .tab', function () { + const selectedTab = $(this).data('tab'); + $('.translation-tabs .tab').removeClass('active'); + $(this).addClass('active'); + $('.tab-content').hide(); + $('#' + selectedTab).show(); + }); + } + }; + + + + + + /* Add the Social Media link in the header */ + // let addSocialMediaLink = function(help, wtitle) { + // let url_to_share = (window.location != window.parent.location) ? document.referrer: document.location.href; + // console.log(url_to_share); + // console.log(window.parent.location); + // // Share Workshop on Facebook + // let fb = $(document.createElement('a')).attr({ + // 'href': 'https://facebook.com', + // 'title': "Share on Facebook", + // 'target': '_blank', + // 'id': 'need_help', + // 'tabindex': '1' + // }).text('F'); + // $('header .hol-Header-wrap').append(fb); + + // let linkedin = $(document.createElement('a')).attr({ + // 'href': 'https://linkedin.com', + // 'title': "Share on LinkedIn", + // 'target': '_blank', + // 'id': 'need_help', + // 'tabindex': '2' + // }).text('I'); + // $('header .hol-Header-wrap').append(linkedin); + + // let twitter = $(document.createElement('a')).attr({ + // 'href': 'https://twitter.com', + // 'title': "Share on Twitter", + // 'target': '_blank', + // 'id': 'need_help', + // 'tabindex': '2' + // }).text('T'); + // $('header .hol-Header-wrap').append(twitter); + // } + + /* + * ============================================ + * SECTION 5: UI COMPONENTS + * ============================================ + */ + + /** + * Enables collapse/expand feature for the steps + * @param {Object} manifestFileContent - The manifest file content + * @param {HTMLElement} articleElement - The article element + */ + const setupContentNav = function (manifestFileContent, articleElement) { + //adds the expand collapse button before the second h2 element + $("#module-content h2:eq(1)") + .before('') + .prev().on('click', function (e) { + ($(this).text() === expandText) ? expandSection($("#module-content h2:not(:eq(0))"), "show") : collapseSection($("#module-content h2:not(:eq(0))"), "hide"); + changeButtonState(); //enables the expand all parts and collapse all parts button + + }); + //enables the feature that allows expand collapse of sections + $("#module-content h2:not(:eq(0))").click(function (e) { + ($(this).hasClass('plus')) ? expandSection(this, "fade") : collapseSection(this, "fade"); + changeButtonState(); + }); + /* for accessibility */ + $("#module-content h2:not(:eq(0))").attr('tabindex', '0'); + $('#module-content h2:not(:eq(0))').keydown(function (e) { + if (e.keyCode === 13 || e.keyCode === 32) { //means enter and space + e.preventDefault(); + if ($(this).hasClass('plus')) + expandSection($(this), "fade"); + else + collapseSection($(this), "fade"); + } + }); + /* accessibility code ends here */ + + // code to hide expand/collapse button + let hide_expand_button = selectTutorial(manifestFileContent).hide_button || manifestFileContent.hide_button; + if (hide_expand_button == "true" || hide_expand_button == "yes") { + $('#btn_toggle').hide(); + } + + window.scrollTo(0, 0); + } + + /** + * Expands a collapsible section + * @param {HTMLElement|jQuery} anchorElement - The anchor element (h2) to expand + * @param {string} effect - Animation effect: "show", "fade", or "none" + */ + const expandSection = function (anchorElement, effect) { + if (effect === "show") { + $(anchorElement).nextUntil("#module-content h1, #module-content h2").show('fast', function () { + $(window).scroll(); + }); + } else if (effect === "fade") { + $(anchorElement).nextUntil("#module-content h1, #module-content h2").fadeIn('fast', function () { + $(window).scroll(); + }); + } + $(anchorElement).addClass("minus"); + $(anchorElement).removeClass("plus"); + } + + /** + * Collapses a collapsible section + * @param {HTMLElement|jQuery} anchorElement - The anchor element (h2) to collapse + * @param {string} effect - Animation effect: "hide", "fade", or "none" + */ + const collapseSection = function (anchorElement, effect) { + if (effect === "hide") { + $(anchorElement).nextUntil("#module-content h1, #module-content h2").hide('fast', function () { + $(window).scroll(); + }); + } else if (effect === "fade") { + $(anchorElement).nextUntil("#module-content h1, #module-content h2").fadeOut('fast', function () { + $(window).scroll(); + }); + } else if (effect === "none") { + $(anchorElement).nextUntil("#module-content h1, #module-content h2").attr('style', 'display:none;'); + } + $(anchorElement).addClass('plus'); + $(anchorElement).removeClass('minus'); + } + + /** + * Detects and updates the state of the collapse/expand button + */ + const changeButtonState = function () { + if ($("#module-content h2.minus").length <= $("#module-content h2.plus").length) { + $('#btn_toggle').text(expandText); + $("#btn_toggle").addClass('plus'); + $("#btn_toggle").removeClass('minus'); + } else { + $('#btn_toggle').text(collapseText); + $("#btn_toggle").addClass('minus'); + $("#btn_toggle").removeClass('plus'); + } + } + /* Expands section on page load based on the hash. Expands section when the leftnav item is clicked */ + let expandSectionBasedOnHash = function (itemName) { + let anchorElement = $('div[name="' + itemName + '"]').next(); //anchor element is always the next of div (eg. h2 or h3) + if ($(anchorElement).hasClass('hol-ToggleRegions')) //if the next element is the collpase/expand button + anchorElement = $(anchorElement).next(); + try { + if (anchorElement[0].tagName !== 'H2') { + anchorElement = $(anchorElement).siblings('h2'); + } + + if ($(anchorElement).hasClass('minus') || $(anchorElement).hasClass('plus')) + expandSection(anchorElement, "fade"); + $(anchorElement)[0].scrollIntoView(); + window.scrollTo(0, window.scrollY - $('.hol-Header').height()); + changeButtonState(); + } catch (e) { console.debug('Section expand error:', e); } + } + + // this function higlights the text when the copy button is clicked + // let selectElement = function(elements) { + // let sel, range, el = elements; + // if (window.getSelection && document.createRange) { //Browser compatibility + // sel = window.getSelection(); + // window.setTimeout(function(){ + // range = document.createRange(); //range object + // range.selectNodeContents(el); //sets Range + // sel.removeAllRanges(); //remove all ranges from selection + // sel.addRange(range); //add Range to a Selection. + // }, 1); + + // window.setTimeout(function() { + // sel.removeAllRanges(); + // }, 4000); + // } + // } + + /** + * Highlights code elements when the copy button is clicked + * @param {jQuery} elements - The elements to highlight + */ + const selectElement = function (elements) { + $(elements).addClass('code-highlight'); + + window.setTimeout(function () { + $(elements).removeClass('code-highlight'); + }, 2000); + } + + /** + * Copies text to clipboard using modern Clipboard API with fallback + * @param {string} text - The text to copy to clipboard + * @param {jQuery} buttonElement - The button element for animation feedback + * @private + */ + const copyToClipboard = function (text, buttonElement) { + const animateSuccess = () => { + $(buttonElement).parent().animate({ + opacity: 0.2 + }).animate({ + opacity: 1 + }); + }; + + // Modern Clipboard API (preferred) + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text) + .then(animateSuccess) + .catch((err) => { + console.debug('Clipboard API failed, using fallback:', err); + // Fallback to execCommand for older browsers + fallbackCopyToClipboard(text, buttonElement, animateSuccess); + }); + } else { + // Fallback for browsers without Clipboard API + fallbackCopyToClipboard(text, buttonElement, animateSuccess); + } + }; + + /** + * Fallback clipboard copy using deprecated execCommand (for older browsers) + * @param {string} text - The text to copy + * @param {jQuery} buttonElement - The button element + * @param {Function} onSuccess - Callback on successful copy + * @private + */ + const fallbackCopyToClipboard = function (text, buttonElement, onSuccess) { + const dummy = $('