');
+ $(this).before('');
+ }
+ });
+
+ $(articleElement).find('.copy-button').click(function () {
+ selectElement($(this).next().find('.copy-code'));
+
+ const codeElement = $(this).next();
+ const preElement = $(this).parent(); // The element has the language class
+ // Check if code block is SQL (supports sql, plsql, and language-* variants)
+ const isSql = preElement.hasClass('sql') ||
+ preElement.hasClass('language-sql') ||
+ preElement.hasClass('plsql') ||
+ preElement.hasClass('language-plsql');
+
+ let copyText = codeElement.find('.copy-code').map(function () {
+ return $(this).text().trim();
+ }).get().join('\n');
+
+ // Add trailing newline only for SQL code blocks so last statement executes when pasted
+ if (isSql) {
+ copyText += '\n';
+ }
+
+ copyToClipboard(copyText, this);
+ });
+
+ return articleElement;
+ }
+
+ /* adds iframe to YouTube videos so that it renders in the same page.
+ The MD code should be in the format [](youtube:) for it to render as iframe. */
+ let renderYouTubeVideos = function (articleElement) {
+ $(articleElement).find('a[href^="youtube:"]').each(function () {
+ $(this).after('');
+ $(this).remove();
+ });
+ return articleElement;
+ }
+
+ /* adds iframe to Oracle Video Hub videos so that it renders in the same page.
+ The MD code should be in the format [](videohub:) for it to render as iframe. */
+ let renderVideoHubVideos = function (articleElement) {
+ $(articleElement).find('a[href^="videohub:"]').each(function () {
+ $(this).after('');
+ $(this).remove();
+ });
+ return articleElement;
+ }
+
+ /* adds HTML5 video element for direct video file URLs.
+ The MD code should be in the format [](video:) or [](video::size) for it to render as video.
+ Supported sizes: small, medium, large (default: small)
+ Supported formats: mp4, webm, ogg/ogv */
+ let renderDirectVideos = function (articleElement) {
+ $(articleElement).find('a[href^="video:"]').each(function () {
+ let href = $(this).attr('href');
+ // Remove the 'video:' prefix
+ let videoPath = href.substring(6);
+ let size = 'small'; // default size
+
+ // Check if size is specified at the end (e.g., :small, :medium, :large)
+ let sizeMatch = videoPath.match(/:(small|medium|large)$/);
+ if (sizeMatch) {
+ size = sizeMatch[1];
+ videoPath = videoPath.replace(/:(small|medium|large)$/, '');
+ }
+
+ // Determine video type from extension
+ let videoType = 'video/mp4'; // default
+ if (videoPath.endsWith('.webm')) {
+ videoType = 'video/webm';
+ } else if (videoPath.endsWith('.ogg') || videoPath.endsWith('.ogv')) {
+ videoType = 'video/ogg';
+ }
+
+ $(this).after('');
+ $(this).remove();
+ });
+ return articleElement;
+ }
+
+ /* remove all content that is not of type specified in the manifest file. Then remove all if tags.*/
+ let singlesource = function (markdownContent, type) {
+ let ifTagRegExp = new RegExp(/<\s*if type="([^>]*)">([\s\S|\n]*?)<\/\s*if>/gm);
+ let contentToReplace = []; // content that needs to be replaced
+
+ if (getParam("type") !== false) {
+ type = getParam("type");
+ } else if ($.type(type) == 'object') {
+ type = Object.keys(type)[0];
+ }
+
+ if ($.type(type) !== 'array')
+ type = Array(type);
+
+ let matches;
+ do {
+ matches = ifTagRegExp.exec(markdownContent);
+ if (matches === null) {
+ $(contentToReplace).each(function (index, value) {
+ markdownContent = markdownContent.replace(value.replace, value.with);
+ });
+ return markdownContent;
+ }
+ // convert if type to array
+ let all_types = matches[1].split(' '),
+ matchFound = false;
+
+ for (let i = 0; i < all_types.length && !matchFound; i++) {
+ if ($.inArray(all_types[i], type) >= 0) { // check if type specified matches content
+ matchFound = true;
+ }
+ }
+
+ // replace with blank if type doesn't match
+ // replace with text without if tag (if any if type matches)
+ (!matchFound) ?
+ contentToReplace.push({ "replace": matches[0], "with": '' }) :
+ contentToReplace.push({ "replace": matches[0], "with": matches[2] });
+
+ } while (matches);
+ }
+ /* converts < > symbols inside the copy tag to < and > */
+ let convertBracketInsideCopyCode = function (markdownContent) {
+ let copyRegExp = new RegExp(/([\s\S|\n]*?)<\/copy>/gm);
+
+ markdownContent = markdownContent.replace(copyRegExp, function (code) {
+ code = code.replace('', '');
+ code = code.replace('', '');
+ code = code.replace(//g, '>');
+ return '' + code.trim() + '';
+ });
+
+ return markdownContent;
+ }
+ // Defines the FreeSQL Buttons for Sprints
+ let convertFreeSQLButtonTags = function (markdownContent) {
+ let sqlCode = "";
+ let link = "";
+
+ // If the markdown includes a FreeSQL button...
+ if (markdownContent.includes(' tag detected. Now replacing it with the real button.');
+
+ // and the author is using a tutorial...
+ if (markdownContent.includes('/gm), function (code) {
+ link = code;
+ link = link.replace('',"");
+ return code;});
+ console.log("Tutorial Link: " + link);
+
+ // and the author is using a worksheet...
+ } else if (markdownContent.includes('')) {
+ console.log(" tag does not include a source. Building a FreeSQL worksheet link.");
+ let worksheetRegExp = new RegExp(/([\s\S|\n]*?)<\/freesql>/gm); // Finds all content between the freesql tag.
+
+ // find all code wrapped in , concatenate it and...
+ markdownContent = markdownContent.replace(worksheetRegExp, function (code){
+ code = code.replace('', '');
+ code = code.replace('', '');
+ sqlCode += code;
+ return code;
+ });
+
+ // create the worksheet's link using the encoded SQL.
+ link = 'https://freesql.com/next/worksheet?code=' + encodeURIComponent(sqlCode);
+ console.log('Worksheet Link: ' + link);
+ } else {console.log('FreeSQL button is not properly formatted.')};
+
+
+ // Replace with the actual button.
+ markdownContent = markdownContent.replace(new RegExp(//gm), function (code) {
+ code = code.replace(code, ' ');
+ console.log("The Free SQL button is now added.")
+ return code;
+ });
+ } else {console.log('No tag detected');}
+
+ return markdownContent;
+ }
+
+ /* injects tracking code into links specified in the utmParams variable */
+ let injectUtmParams = function (articleElement) {
+ let currentUrl = window.location.href;
+ $(utmParams).each(function (index, item) {
+ let inParamValue = getParam(item.inParam);
+ if (inParamValue) {
+ $(articleElement).find('a[href*="' + item.url + '"]').each(function () {
+ let targetUrl = $(this).attr('href');
+ $(this).attr('href', unescape(setParam(targetUrl, item.outParam, inParamValue)));
+ });
+ }
+ });
+
+ /* hack for manual links like this ?lab=xx. Should be removed later. */
+ $(utmParams).each(function (index, item) {
+ let inParamValue = getParam(item.inParam);
+ if (inParamValue) {
+ $(articleElement).find('a[href*="?' + queryParam + '="]').each(function () {
+ let targetUrl = $(this).attr('href') + '&' + item.inParam + '=' + inParamValue;
+ $(this).attr('href', unescape(targetUrl));
+ });
+ }
+ });
+ /* remove till here */
+ return articleElement;
+ }
+
+ /*
+ * ============================================
+ * SECTION 8: UTILITIES
+ * ============================================
+ */
+
+ /**
+ * Sets a query parameter value in a URL
+ * @param {string} url - The URL to modify
+ * @param {string} paramName - The parameter name
+ * @param {string} paramValue - The parameter value
+ * @returns {string} The modified URL
+ */
+ const setParam = function (url, paramName, paramValue) {
+ let onlyUrl = (url.split('?')[0]).split('#')[0];
+ let params = url.replace(onlyUrl, '').split('#')[0];
+ let hashAnchors = url.replace(onlyUrl + params, '');
+ hashAnchors = "";
+
+ let existingParamValue = getParam(paramName);
+ if (existingParamValue) {
+ return onlyUrl + params.replace(paramName + '=' + existingParamValue, paramName + '=' + paramValue) + hashAnchors;
+ } else {
+ if (params.length === 0 || params.length === 1) {
+ return onlyUrl + '?' + paramName + '=' + paramValue + hashAnchors;
+ }
+ return onlyUrl + params + '&' + paramName + '=' + paramValue + hashAnchors;
+ }
+ }
+ /**
+ * Gets a query parameter value from the current URL
+ * @param {string} paramName - The parameter name to retrieve
+ * @returns {string|boolean} The parameter value or false if not found
+ */
+ const getParam = function (paramName) {
+ const params = window.location.search.substring(1).split('&');
+ for (let i = 0; i < params.length; i++) {
+ if (params[i].split('=')[0] == paramName) {
+ // Fix for LLAPEX-595 to remove characters only before the first '='
+ return params[i].split(/=(.*)/s)[1];
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a short name from a title for URL-friendly identifiers
+ * @param {string} title - The title to convert
+ * @returns {string} The short name
+ */
+ const createShortNameFromTitle = function (title) {
+ if (!title) {
+ console.log("The title in the manifest file cannot be blank!");
+ return "ErrorTitle";
+ }
+ const removeFromTitle = ["-a-", "-in-", "-of-", "-the-", "-to-", "-an-", "-is-", "-your-", "-you-", "-and-", "-from-", "-with-"];
+ const folderNameRestriction = ["<", ">", ":", "\"", "/", "\\\\", "|", "\\?", "\\*", "&", "\\.", ","];
+ let shortname = title.toLowerCase().replace(/ /g, '-').trim().substr(0, 50);
+ $.each(folderNameRestriction, function (i, value) {
+ shortname = shortname.replace(new RegExp(value, 'g'), '');
+ });
+ $.each(removeFromTitle, function (i, value) {
+ shortname = shortname.replace(new RegExp(value, 'g'), '-');
+ });
+ if (shortname.length > 40) {
+ shortname = shortname.substr(0, shortname.lastIndexOf('-'));
+ }
+ return shortname;
+ }
+
+
+ let updateOpenCloseButtonText = function (articleElement, manifestFileContent) {
+ let task_type = selectTutorial(manifestFileContent).task_type || manifestFileContent.task_type;
+ if (task_type) {
+ const default_task_type = "Tasks";
+ task_type = task_type.trim();
+ collapseText = collapseText.replace(default_task_type, task_type);
+ expandText = expandText.replace(default_task_type, task_type);
+ }
+ return articleElement;
+ }
+
+ let showRightAndLeftArrow = function (articleElement, manifestFileContent) {
+ let next_page = selectTutorial(manifestFileContent, extendedNav['#next']);
+ let prev_page = selectTutorial(manifestFileContent, extendedNav['#prev']);
+
+
+ if (next_page !== undefined) {
+ $('.hol-Footer-rightLink').removeClass('hide').addClass('show').attr({ 'href': unescape(setParam(window.location.href, queryParam, getMDFileName(next_page.filename))), 'title': 'Next' }).text('Next');
+ }
+ if (prev_page !== undefined) {
+ $('.hol-Footer-leftLink').removeClass('hide').addClass('show').attr({ 'href': unescape(setParam(window.location.href, queryParam, getMDFileName(prev_page.filename))), 'title': 'Previous' }).text('Previous');
+ }
+ return articleElement;
+ }
+
+ let setH2Name = function (articleElement) {
+
+ $(articleElement).find('h2').each(function () {
+ $(this).before($(document.createElement('div')).attr({
+ 'name': alphaNumOnly($(this).text()),
+ 'data-unique': alphaNumOnly($(this).text())
+ }));
+ });
+ return articleElement;
+ }
+
+ /**
+ * Returns only alphanumeric characters from text
+ * @param {string} text - The input text
+ * @returns {string} Text with only alphanumeric characters
+ */
+ const alphaNumOnly = function (text) { return text.replace(/[^[A-Za-z0-9:?\(\)]+?/g, ''); }
+
+ /*
+ * ============================================
+ * SECTION 7: QA VALIDATION
+ * ============================================
+ */
+
+ /**
+ * Performs QA validation on the article content
+ * @param {HTMLElement} articleElement - The article element to validate
+ * @param {string} markdownContent - The raw markdown content
+ * @param {Object} manifestFileContent - The manifest file content
+ * @returns {jQuery} The article element with QA report prepended
+ */
+ const performQA = function (articleElement, markdownContent, manifestFileContent) {
+ let error_div = $(document.createElement('div')).attr('id', 'qa-report').html("");
+ const more_info = "Please see using the LiveLabs template for more information.";
+
+ let urlExists = function (url, callback) {
+ $.ajax({
+ type: 'HEAD',
+ url: url,
+ success: function () {
+ callback(true);
+ },
+ error: function () {
+ callback(false);
+ }
+ });
+ }
+
+ let add_issue = function (error_msg, error_type = "", follow_id = false) {
+ if (follow_id) {
+ $(error_div).find('ol').append("" + error_msg + " (show)");
+ } else {
+ $(error_div).find('ol').append("" + error_msg + "");
+ }
+
+ }
+
+ let checkH1 = function (article) {
+ if ($(article).find('h1').length !== 1) {
+ add_issue("Only a single title is allowed, please edit your Markdown file and remove or recast other content tagged with a single #.", "major-error");
+ $(article).find('h1').addClass('error');
+ }
+ }
+
+ let checkForGerundInTitle = function (manifest) {
+ // removed in 26.2
+ return;
+ if (manifest.workshoptitle.indexOf("ing ") !== -1) {
+ //updated to specifiy what Imperative means
+ add_issue("Your workshop title uses a gerund. Consider using an imperative verb workshop title, for example, 'Start' instead of 'Starting'.", "major-error")
+ // add_issue("Please use an imperative workshop title instead of a gerund,(e.g 'Start' not 'Starting').", "major-error")
+ }
+ }
+
+ let checkForGerundInLabTitle = function (manifest) {
+ // removed in 26.2
+ return;
+ var i = 0;
+ while (i < manifest.tutorials.length) {
+ if (manifest.tutorials[i].title.indexOf("ing ") !== -1) {
+ //specifies where imperative issue location(s) within the Lab
+ add_issue("Your lab: '" + manifest.tutorials[i].title + "', uses a gerund. Consider using an imperative verb lab title instead of a gerund, for example, 'Start' instead of 'Starting'.", "major-error")
+ }
+ i++;
+ }
+ }
+
+
+ let checkForHtmlTags = function (markdown) {
+ let count = (markdown.match(new RegExp(" 1)
+ add_issue("There are " + count + " occurrences of HTML (for example: <a href=...>) in your Markdown. Please do not embed HTML in Markdown.");
+ }
+
+ let checkSecondH2Tag = function (article) {
+ if ($(article).find('h2:eq(1)').text().substr(0, 4).indexOf("Task") !== 0) {
+ $(article).find('h2:eq(1)').addClass(getFollowId());
+ add_issue("The second H2 tag (##) of your Markdown file should be labeled with \"Task\".", "", getFollowId());
+ }
+ }
+
+ let checkImages = function (article) {
+ $(article).find('img').each(function () {
+ // skip the modalImg img frame from QA check
+ if ($(this).attr("id") === "modalImg") {
+ return;
+ }
+ try {
+ // if ($(this).attr('src').split('/')[$(this).attr('src').split('/').length - 2].indexOf("images") !== 0) {
+ if ($(this).attr('src').indexOf("/images/") <= 0) {
+ add_issue("Your images must be in an images folder. Please rename the folder and update your Markdown.");
+ return false; // to break the each loop
+ }
+ } catch (e) {
+ add_issue("Your images must be in an images folder. Please rename the folder and update your Markdown.");
+ return false;
+ };
+ });
+ }
+
+ let checkImagesAltText = function (article) {
+ $(article).find('img').each(function () {
+ // if ($(this).attr("alt").length <1 || (!$(this).attr("alt")) || $(this).attr("alt") == '' || $(this).attr("alt") == undefined || $(this).attr("alt") == 0) {
+ try {
+ if ($(this).attr('alt').length < 1 || (!$(this).attr("alt")) || $(this).attr("alt") == '' || $(this).attr("alt") == undefined || $(this).attr("alt") == 0) {
+ add_issue("Please make sure that all images contain alternate text.");
+ return false;
+ }
+
+ } catch (e) {
+ return false;
+ };
+ })
+ }
+
+
+ let checkCodeBlockFormat = function (markdown) {
+ let count = (markdown.match(/\````/g) || []).length;
+ if (count == 1) {
+ add_issue("Your Markdown file has " + count + " codeblock with 4 (````). This should be changed to 3 (```). Please review your Markdown and make the necessary changes.")
+ } else if (count > 1) {
+ add_issue("Your Markdown file has " + count + " codeblocks with 4 (````). This should be changed to 3 (```). Please review your Markdown and make the necessary changes.")
+ }
+ }
+
+ let updateCount = function (article) {
+ $(error_div).find('#qa-reportheader').html('Total Issues: ' + $(error_div).find('li').length);
+ if (!$(error_div).find('li').length) {
+ $(error_div).find('#qa-reportbody').hide();
+ } else {
+ $(error_div).find('#qa-reportbody').show();
+ if ($(error_div).find('#qa-reportbody p').length === 0)
+ $(error_div).find('#qa-reportbody').append('' + more_info + '
');
+ }
+ }
+
+ let checkLinkExists = function (article) {
+ $(article).find('a').each(function () {
+ let url = $(this).attr('href');
+ let url_text = $(this).text();
+ urlExists(url, function (exists) {
+ if (!exists) {
+ $('a[href$="' + url + '"]').addClass('error ' + getFollowId());
+ add_issue("This URL may be broken: " + url_text + "", "major-error", getFollowId());
+ updateCount(article);
+ }
+ });
+ });
+ }
+
+ let checkImageExists = function (article) {
+ $(article).find('img').each(function () {
+ // skip the modalImg img frame from QA check
+ if ($(this).attr("id") === "modalImg") {
+ return;
+ }
+ let url = $(this).attr('src');
+ let url_text = $(this).attr('src').split('/')[$(this).attr('src').split('/').length - 1];
+ urlExists(url, function (exists) {
+ if (!exists) {
+ ;
+ $('img[src$="' + url + '"]').addClass('error ' + getFollowId());
+ add_issue("The link to image " + url_text + " is broken.", "major-error", getFollowId())
+ updateCount(article);
+ }
+ });
+ });
+ }
+
+ let checkIfSectionExists = function (article, section_name) {
+ if ($(article).find('div[name="' + alphaNumOnly(section_name) + '"]').length === 0)
+ add_issue("You are missing " + section_name + " section.");
+ }
+
+ let checkIndentation = function (article) {
+ $(article).find('section:not(:first-of-type)').each(function () {
+ let tag_list = [];
+ if ($(this).find('h2').text().toUpperCase().trim().indexOf("Task") == 0) {
+ $(this).children().each(function () {
+ tag_list.push($(this).prop('tagName'));
+ });
+
+ if ($.inArray("UL", tag_list) !== -1 & $.inArray("OL", tag_list) == -1) {
+ add_issue("In section " + $(this).find('h2').text() + ", your steps are not numbered. Numbered steps should follow your STEP element.", "minor-error");
+ $(this).find('h2').addClass('format-error');
+ }
+
+ if ($.inArray("PRE", tag_list) > $.inArray("OL", tag_list)) {
+ $(this).children('pre').addClass('format-error ' + getFollowId());
+ add_issue("Your codeblock is not indented correctly. Add spaces to indent your codeblock. Use one tab stop (4 spaces).", "minor-error", getFollowId());
+ }
+
+ $(this).find('img').each(function () {
+ if ($(this).parent().parent().prop('tagName').indexOf("LI") == -1 && $(this).parent().parent().prop('tagName').indexOf("OL") == -1 && $(this).parent().parent().prop('tagName').indexOf("UL") == -1) {
+ // $(this).parents('section').children('h2').addClass('format-error');
+ $(this).addClass('format-error ' + getFollowId());
+ add_issue("The image " + $(this).attr('src').split('/')[$(this).attr('src').split('/').length - 1] + " is not aligned with your text blocks. Add spaces to indent your image.", "minor-error", getFollowId());
+ }
+ });
+ }
+ });
+ }
+
+ let getFollowId = function () { return 'error_' + $(error_div).find('li').length; }
+
+ checkH1(articleElement);
+ checkForGerundInTitle(manifestFileContent);
+ checkForGerundInLabTitle(manifestFileContent);
+ checkForHtmlTags(markdownContent);
+ checkImages(articleElement);
+ checkImagesAltText(articleElement);
+ checkCodeBlockFormat(markdownContent);
+ checkSecondH2Tag(articleElement);
+ if (!window.location.href.indexOf("localhost") && window.location.href.indexOf("127.0.0.1")) {
+ checkLinkExists(articleElement);
+ }
+ checkImageExists(articleElement);
+ checkIfSectionExists(articleElement, "Acknowledgements");
+ // checkIfSectionExists(articleElement, "See an issue?");
+ checkIndentation(articleElement);
+ updateCount(articleElement);
+
+ return $(articleElement).prepend(error_div);
+ }
+
+ // picked up as it is from: https://www.w3schools.com/howto/howto_js_draggable.asp
+ function dragElement(elmnt) {
+ var pos1 = 0,
+ pos2 = 0,
+ pos3 = 0,
+ pos4 = 0;
+ if (document.getElementById(elmnt.id + "header")) {
+ // if present, the header is where you move the DIV from:
+ document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
+
+ $('#qa-reportheader').dblclick(function () { // this line has been added to collapse qa report body
+ $('#qa-reportbody').fadeToggle();
+ });
+
+ } else {
+ // otherwise, move the DIV from anywhere inside the DIV:
+ elmnt.onmousedown = dragMouseDown;
+ }
+
+ function dragMouseDown(e) {
+ e = e || window.event;
+ e.preventDefault();
+ // get the mouse cursor position at startup:
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ document.onmouseup = closeDragElement;
+ // call a function whenever the cursor moves:
+ document.onmousemove = elementDrag;
+ }
+
+ function elementDrag(e) {
+ e = e || window.event;
+ e.preventDefault();
+ // calculate the new cursor position:
+ pos1 = pos3 - e.clientX;
+ pos2 = pos4 - e.clientY;
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ // set the element's new position:
+ elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
+ elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
+ }
+
+ function closeDragElement() {
+ // stop moving when mouse button is released:
+ document.onmouseup = null;
+ document.onmousemove = null;
+ }
+ }
+
+}();
+
+/**
+ * Global function to check quiz answers
+ * Called by quiz check buttons via onclick
+ * @param {string} quizId - The quiz element's data-quiz-id
+ * @param {boolean} isMultiple - Whether this is a multiple-answer quiz (checkboxes)
+ */
+function checkQuizAnswer(quizId, isMultiple) {
+ let quiz = document.querySelector('[data-quiz-id="' + quizId + '"]');
+ if (!quiz) return;
+
+ let options = quiz.querySelectorAll('.ll-quiz-option');
+ let resultDiv = quiz.querySelector('.ll-quiz-result');
+ let explanationDiv = quiz.querySelector('.ll-quiz-explanation');
+ let checkBtn = quiz.querySelector('.ll-quiz-check');
+ let retryBtn = quiz.querySelector('.ll-quiz-retry');
+ let allCorrect = true;
+ let anySelected = false;
+ let isScored = quiz.getAttribute('data-scored') === 'true';
+ let wasAnswered = quiz.getAttribute('data-answered') === 'true';
+ let wasCorrect = quiz.getAttribute('data-correct') === 'true';
+
+ options.forEach(function (option) {
+ let input = option.querySelector('input');
+ let feedback = option.querySelector('.ll-quiz-feedback');
+ let isCorrect = option.getAttribute('data-correct') === 'true';
+ let isSelected = input.checked;
+
+ if (isSelected) anySelected = true;
+
+ // Reset previous state
+ option.classList.remove('correct', 'incorrect', 'missed');
+ feedback.textContent = '';
+
+ if (isSelected && isCorrect) {
+ option.classList.add('correct');
+ feedback.textContent = '✓';
+ } else if (isSelected && !isCorrect) {
+ option.classList.add('incorrect');
+ feedback.textContent = '✗';
+ allCorrect = false;
+ } else if (!isSelected && isCorrect) {
+ option.classList.add('missed');
+ allCorrect = false;
+ }
+
+ // Disable input after checking
+ input.disabled = true;
+ });
+
+ if (!anySelected) {
+ resultDiv.textContent = 'Please select an answer.';
+ resultDiv.className = 'll-quiz-result warning';
+ // Re-enable inputs
+ options.forEach(function (option) {
+ option.querySelector('input').disabled = false;
+ });
+ return;
+ }
+
+ // Show result
+ if (allCorrect) {
+ resultDiv.textContent = 'Correct!';
+ resultDiv.className = 'll-quiz-result success';
+ } else {
+ resultDiv.textContent = 'Not quite. The correct answer' + (isMultiple ? 's are' : ' is') + ' highlighted.';
+ resultDiv.className = 'll-quiz-result error';
+ }
+
+ // Show explanation if present
+ if (explanationDiv) {
+ explanationDiv.style.display = 'block';
+ }
+
+ // Hide check button, show retry button
+ checkBtn.style.display = 'none';
+ if (retryBtn) {
+ retryBtn.style.display = 'inline-block';
+ }
+
+ // Update quiz state
+ quiz.setAttribute('data-answered', 'true');
+ quiz.setAttribute('data-correct', allCorrect.toString());
+
+ // Update score tracker if this is a scored quiz
+ if (isScored) {
+ updateQuizScore(wasAnswered, wasCorrect, allCorrect);
+ }
+}
+
+/**
+ * Reset a quiz to allow retry
+ * @param {string} quizId - The quiz element's data-quiz-id
+ */
+function retryQuiz(quizId) {
+ let quiz = document.querySelector('[data-quiz-id="' + quizId + '"]');
+ if (!quiz) return;
+
+ let options = quiz.querySelectorAll('.ll-quiz-option');
+ let resultDiv = quiz.querySelector('.ll-quiz-result');
+ let explanationDiv = quiz.querySelector('.ll-quiz-explanation');
+ let checkBtn = quiz.querySelector('.ll-quiz-check');
+ let retryBtn = quiz.querySelector('.ll-quiz-retry');
+ let isScored = quiz.getAttribute('data-scored') === 'true';
+ let wasCorrect = quiz.getAttribute('data-correct') === 'true';
+
+ // Reset all options
+ options.forEach(function (option) {
+ let input = option.querySelector('input');
+ let feedback = option.querySelector('.ll-quiz-feedback');
+
+ option.classList.remove('correct', 'incorrect', 'missed');
+ feedback.textContent = '';
+ input.checked = false;
+ input.disabled = false;
+ });
+
+ // Reset result and explanation
+ resultDiv.textContent = '';
+ resultDiv.className = 'll-quiz-result';
+ if (explanationDiv) {
+ explanationDiv.style.display = 'none';
+ }
+
+ // Show check button, hide retry button
+ checkBtn.style.display = 'inline-block';
+ if (retryBtn) {
+ retryBtn.style.display = 'none';
+ }
+
+ // Update quiz state - mark as not answered for retry
+ quiz.setAttribute('data-answered', 'false');
+ quiz.setAttribute('data-correct', 'false');
+
+ // Update score tracker if this is a scored quiz (remove from answered count)
+ if (isScored) {
+ updateQuizScore(true, wasCorrect, false, true);
+ }
+}
+
+/**
+ * Update the quiz score tracker and inline score displays
+ * @param {boolean} wasAnswered - Whether quiz was previously answered
+ * @param {boolean} wasCorrect - Whether quiz was previously correct
+ * @param {boolean} isCorrect - Whether quiz is now correct
+ * @param {boolean} isRetry - Whether this is a retry (removing answer)
+ */
+function updateQuizScore(wasAnswered, wasCorrect, isCorrect, isRetry) {
+ let tracker = document.getElementById('ll-quiz-score-tracker');
+ if (!tracker) return;
+
+ let total = parseInt(tracker.getAttribute('data-total'), 10);
+ let correct = parseInt(tracker.getAttribute('data-correct'), 10);
+ let answered = parseInt(tracker.getAttribute('data-answered'), 10);
+
+ if (isRetry) {
+ // Removing an answer (retry)
+ answered--;
+ if (wasCorrect) correct--;
+ } else if (wasAnswered) {
+ // Updating an existing answer
+ if (wasCorrect && !isCorrect) correct--;
+ else if (!wasCorrect && isCorrect) correct++;
+ } else {
+ // New answer
+ answered++;
+ if (isCorrect) correct++;
+ }
+
+ tracker.setAttribute('data-correct', correct);
+ tracker.setAttribute('data-answered', answered);
+
+ // Calculate percentage
+ let percentage = answered > 0 ? Math.round((correct / total) * 100) : 0;
+
+ // Update all inline score displays next to "Scored Quiz" labels
+ let scoreDisplays = document.querySelectorAll('.ll-quiz-score-display');
+ let scoreText = '';
+
+ if (answered < total) {
+ scoreText = correct + '/' + total + ' correct (' + percentage + '%) - ' + (total - answered) + ' remaining';
+ } else {
+ scoreText = correct + '/' + total + ' correct (' + percentage + '%)';
+ }
+
+ scoreDisplays.forEach(function (display) {
+ display.textContent = scoreText;
+
+ // Update styling based on current performance
+ display.classList.remove('passing', 'failing');
+ if (answered === total) {
+ let config = document.getElementById('ll-quiz-config');
+ let passingScore = config ? parseInt(config.getAttribute('data-passing'), 10) : 80;
+ if (percentage >= passingScore) {
+ display.classList.add('passing');
+ } else {
+ display.classList.add('failing');
+ }
+ }
+ });
+
+ // Handle badge container when all answered
+ let badgeContainer = tracker.querySelector('.ll-quiz-badge-container');
+
+ if (answered >= total) {
+ let config = document.getElementById('ll-quiz-config');
+ let passingScore = config ? parseInt(config.getAttribute('data-passing'), 10) : 80;
+ let badgePath = config ? config.getAttribute('data-badge') : null;
+
+ if (percentage >= passingScore && badgePath) {
+ // Get the base path from an existing image in the article, or construct from URL
+ let fullBadgePath = badgePath;
+ if (!badgePath.startsWith('http') && !badgePath.startsWith('/')) {
+ // Relative path - need to prepend base path
+ let existingImg = document.querySelector('#module-content img');
+ if (existingImg && existingImg.src) {
+ // Extract base path from existing image src
+ let imgSrc = existingImg.src;
+ let basePath = imgSrc.substring(0, imgSrc.lastIndexOf('/') + 1);
+ // Remove 'images/' from base path if badge path starts with 'images/'
+ if (badgePath.startsWith('images/') && basePath.endsWith('images/')) {
+ basePath = basePath.slice(0, -7); // remove trailing 'images/'
+ }
+ fullBadgePath = basePath + badgePath;
+ } else {
+ // Fallback: try to construct path from current lab URL
+ // Look for lab path in selected nav item or URL hash
+ let selectedNav = document.querySelector('.selected a');
+ if (selectedNav && selectedNav.href) {
+ let labPath = selectedNav.href;
+ // Extract directory from lab path
+ let baseDir = labPath.substring(0, labPath.lastIndexOf('/') + 1);
+ fullBadgePath = baseDir + badgePath;
+ }
+ }
+ }
+ badgeContainer.innerHTML = 'Congratulations! You passed with ' + percentage + '%!
' +
+ '' +
+ '

' +
+ '
Download Your Badge' +
+ '
' +
+ 'Disclaimer: This badge is not an official Oracle Certification. We do not track or store any user data.
';
+ badgeContainer.style.display = 'block';
+ // Move badge container to appear right after the last scored quiz
+ let scoredQuizzes = document.querySelectorAll('.ll-quiz-scored');
+ if (scoredQuizzes.length > 0) {
+ let lastScoredQuiz = scoredQuizzes[scoredQuizzes.length - 1];
+ lastScoredQuiz.parentNode.insertBefore(badgeContainer, lastScoredQuiz.nextSibling);
+ }
+ // Scroll badge into view with smooth animation
+ setTimeout(function() {
+ badgeContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 300);
+ } else if (percentage < passingScore) {
+ badgeContainer.innerHTML = 'Score: ' + percentage + '%. You need ' + passingScore + '% to pass. Click "Try Again" on any quiz to retry.
';
+ badgeContainer.style.display = 'block';
+ // Move badge container to appear right after the last scored quiz
+ let scoredQuizzes = document.querySelectorAll('.ll-quiz-scored');
+ if (scoredQuizzes.length > 0) {
+ let lastScoredQuiz = scoredQuizzes[scoredQuizzes.length - 1];
+ lastScoredQuiz.parentNode.insertBefore(badgeContainer, lastScoredQuiz.nextSibling);
+ }
+ // Scroll into view so user sees they didn't pass
+ setTimeout(function() {
+ badgeContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 300);
+ } else {
+ badgeContainer.style.display = 'none';
+ }
+ } else {
+ badgeContainer.style.display = 'none';
+ }
+}
+
+let download = function () {
+
+ //enables download of files
+ let download_file = function (filename, text) {
+ let pom = document.createElement('a');
+ pom.setAttribute('href', 'data:html/plain;charset=utf-8,' + encodeURIComponent(text));
+ pom.setAttribute('download', filename);
+ if (document.createEvent) {
+ let event = document.createEvent('MouseEvents');
+ event.initEvent('click', true, true);
+ pom.dispatchEvent(event);
+ } else {
+ pom.click();
+ }
+ }
+
+ $.when($('img').each(function () {
+ $(this).css('max-width', '75%');
+ if ($(this).attr('src').indexOf('http') == -1)
+ $(this).attr('src', location.protocol + '//' + location.host + location.pathname + $(this).attr('src'));
+ }),
+ $('pre button').remove(),
+ $('pre').attr('style', 'white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word; max-width: 80%;'),
+ $("#module-content h2:not(:eq(0))").nextAll().show('fast'),
+ $('h2').removeClass('plus minus'),
+ $('#btn_toggle').remove()).done(function () {
+ download_file($('.selected span').text().replace(/[^[A-Za-z0-9:?]+?/g, '') + '.html', '' + $('#contentBox')[0].outerHTML + '');
+ });
+}
+
+/*!
+######################################################
+# ORA_APEX.JS
+######################################################
+*/
+if (location.hostname.includes("livelabs.oracle.com")) {
+ var script = document.createElement("script");
+ script.type = "text/javascript";
+ script.src = "https://www.oracle.com/us/assets/metrics/ora_apex.js";
+ document.head.appendChild(script);
+ }
+