/** * @namespace aria */ /* exported sourceCode */ 'use strict'; var aria = aria || {}; /* ---------------------------------------------------------------- */ /* ARIA Widget Namespace */ /* ---------------------------------------------------------------- */ aria.widget = aria.widget || {}; /* ---------------------------------------------------------------- */ /* Source code generators */ /* ---------------------------------------------------------------- */ // Default indentation according to the ARIA Practices Code Guide: // https://github.com/w3c/aria-practices/wiki/Code-Guide. var DEFAULT_INDENT = '  '; // Void elements according to “HTML 5.3: The HTML Syntax”: // https://w3c.github.io/html/syntax.html#void-elements. var VOID_ELEMENTS = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr', ]; /** * Creates a slider widget using ARIA * * @property {Array} location Object containing the keyCodes used by the slider widget * @property {Array} code JQuery node object * @class SourceCode * @memberof aria.widget */ aria.widget.SourceCode = function () { this.location = new Array(); this.code = new Array(); this.exampleHeader = new Array(); this.resources = new Array(); this.HTMLDescription = new Array(); }; /** * Adds source code * * @param {string} locationId - ID of `code` element that will display the example html * @param {string} codeId - ID of element containing only and all of the html used to render the example widget * @param {string} exampleHeaderId - ID of header element under which the "Open in Codepen" button belongs * @param {string} cssJsFilesId - ID of element containing links to all the relevant js and css files used for the example widget * @param {string} HTMLDescriptionId - ID of the separator element under the HTML Source Code which the "Open in Codepen" button belongs * @function add * @memberof aria.widget.SourceCode */ aria.widget.SourceCode.prototype.add = function ( locationId, codeId, exampleHeaderId, cssJsFilesId, HTMLDescriptionId ) { this.location[this.location.length] = locationId; this.code[this.code.length] = codeId; this.exampleHeader[this.exampleHeader.length] = exampleHeaderId; this.HTMLDescription[this.HTMLDescription.length] = HTMLDescriptionId; this.resources[this.resources.length] = cssJsFilesId; }; /** * Generates HTML content for source code * * @function make * @memberof aria.widget.SourceCode */ aria.widget.SourceCode.prototype.make = function () { for (var i = 0; i < this.location.length; i++) { var sourceCodeNode = document.getElementById(this.location[i]); var nodeCode = document.getElementById(this.code[i]); var exampleAmount = document.querySelectorAll('[id^="ex"]'); sourceCodeNode.className = 'sourcecode'; this.createCode(sourceCodeNode, nodeCode, 0, true); if (this.HTMLDescription[i]) { addOpenInCodePenForm( i + exampleAmount, this.HTMLDescription[i], this.code[i], this.resources[i] ); } // Remove unnecessary `
` element. if (sourceCodeNode.innerHTML.startsWith('
')) { sourceCodeNode.innerHTML = sourceCodeNode.innerHTML.replace('
', ''); } // Adds the "Open In CodePen" button by the example header if (this.exampleHeader[i]) { addOpenInCodePenForm( i, this.exampleHeader[i], this.code[i], this.resources[i] ); } } }; /** * Creates the HTML for one node * * @param {HTMLElement} sourceCodeNode element to put the source code in * @param {object} node DOM Element node to use to generate the source code * @param {number} indentLevel Level of indentation * @param {boolean} skipFirst Whether to skip the first node * @function createCode * @memberof aria.widget.SourceCode */ aria.widget.SourceCode.prototype.createCode = function ( sourceCodeNode, node, indentLevel, skipFirst ) { if (!skipFirst) { var openTag = ''; var nodeNameStr = node.nodeName.toLowerCase(); openTag += '\n' + indentation(indentLevel) + '<' + nodeNameStr; var wrapAttributes = node.attributes.length > 2; for (var attrPos = 0; attrPos < node.attributes.length; attrPos++) { if (!wrapAttributes || attrPos === 0) { openTag += ' '; } var attributeValue = sanitizeNodeValue(node.attributes[attrPos].value); openTag += node.attributes[attrPos].nodeName + '="' + attributeValue + '"'; if (wrapAttributes && attrPos !== node.attributes.length - 1) { openTag += '\n' + indentation(indentLevel); openTag += ' '.repeat(nodeNameStr.length + 2); } } openTag += '>'; sourceCodeNode.innerHTML += openTag; indentLevel++; } for (var i = 0; i < node.childNodes.length; i++) { var childNode = node.childNodes[i]; switch (childNode.nodeType) { case Node.ELEMENT_NODE: // To remove the SVG definitions from the example's source code, add the id `svg_definitions` if (childNode.id !== 'svg_definitions') { this.createCode(sourceCodeNode, childNode, indentLevel, false); } break; case Node.TEXT_NODE: if (hasText(childNode.nodeValue)) { var textNodeContent = sanitizeNodeValue(childNode.nodeValue).trim(); textNodeContent = indentLines( textNodeContent, indentation(indentLevel) ); sourceCodeNode.innerHTML += '\n' + textNodeContent; } break; case Node.COMMENT_NODE: if (hasText(childNode.nodeValue)) { var commentNodeContent = sanitizeNodeValue(childNode.nodeValue); commentNodeContent = '<!--' + commentNodeContent + '-->'; commentNodeContent = indentLines( commentNodeContent, indentation(indentLevel) ); sourceCodeNode.innerHTML += '\n' + commentNodeContent; } break; } } if (!skipFirst && !VOID_ELEMENTS.includes(node.tagName.toLowerCase())) { indentLevel--; var closeTag = '</' + node.nodeName.toLowerCase() + '>'; if (node.childNodes.length > 0) { sourceCodeNode.innerHTML += '\n' + indentation(indentLevel); } sourceCodeNode.innerHTML += closeTag; } }; /** * Returns the indentation string based on a given indent level. * * @param {number} indentLevel * @returns {string} */ function indentation(indentLevel) { return DEFAULT_INDENT.repeat(indentLevel); } /** * Tests whether the input string contains any non-whitespace character. * * `\S`: Any non-whitespace character * * Examples: * * ``` * hasText('\t\n\r '); * //> false * * hasText('\tcontent\n\r '); * //> true * ``` * * @param {string} nodeContent content of a node to test for whitespace characters * @returns {boolean} */ function hasText(nodeContent) { if (typeof nodeContent !== 'string') { return false; } return /\S/.test(nodeContent); } /** * Replaces the characters `<` and `>` with their respective HTML entities. * * @param {string} textContent * @returns {string} */ function sanitizeNodeValue(textContent) { if (typeof textContent !== 'string') { return ''; } var output = stripIndentation(textContent); return output .replace(new RegExp('<', 'g'), '<') .replace(new RegExp('>', 'g'), '>'); } /** * Strips any leading indentation from the text content of a node. * * @param {string} textContent * @returns {string} */ function stripIndentation(textContent) { var textIndentation = detectIndentation(textContent); if (textIndentation === 0) { return textContent; } var firstLine = textContent.substring(0, textContent.indexOf('\n')); var lines = textContent.split('\n').slice(1); lines = lines.map(function (line) { if (line === '') { return line; } return line.substring(textIndentation); }); return firstLine + '\n' + lines.join('\n'); } /** * Determines the indentation level of text content. * * Algorithm: * * Case 1: `textContent` is on the same line as opening and closing tag**: * * In other words, the text doesn’t contain any newline. Return 0. * * Case 2: `textContent` is on the same line as opening tag**: * * We already know there is at least one newline, because case 1 didn’t return. * We can now find the first indented line that contains any non-whitespace * character in order to determine present indentation. * * 1. Starting from the position after the first newline, iterate over each * character in `textContent`. * 2. If the character is a non-whitespace (i.e. `/\S/`), `break`. * 3. If the character is a newline, reset the indentation level and * `continue`. * 4. Increment the indentation level. * * Return the indentation level. * * @param {string} textContent * @returns {number} The level of indentation */ function detectIndentation(textContent) { // Case 1 if (!textContent.includes('\n')) { return 0; } // Case 2 var indentationLevel = 0; for (var i = textContent.indexOf('\n') + 1; i < textContent.length; i++) { if (/\S/.test(textContent[i])) { break; } if (textContent[i] === '\n') { indentationLevel = 0; continue; } indentationLevel++; } return indentationLevel; } /** * Prepends each line of a string with the provided indentation string. * * @param {string} input * @param {string} indentation * @returns {string} */ function indentLines(input, indentation) { var lines = input.split('\n'); lines = lines.map(function (line) { return indentation + line; }); return lines.join('\n'); } /** * Creates and adds an "Open in CodePen" button * * @param {string} exampleIndex - the example number, if there are multiple examples * @param {string} exampleHeaderId - the example header to place the button next to * @param {string} exampleCodeId - the example html code * @param {string} exampleFilesId - the element containing all relevant CSS and JS file */ function addOpenInCodePenForm( exampleIndex, exampleHeaderId, exampleCodeId, exampleFilesId ) { var jsonInputId = 'codepen-data-ex-' + exampleIndex; var buttonId = exampleHeaderId + '-codepenbutton'; var form = document.createElement('form'); form.setAttribute('action', 'https://codepen.io/pen/define'); form.setAttribute('method', 'POST'); form.setAttribute('target', '_blank'); var input = document.createElement('input'); input.setAttribute('id', jsonInputId); input.setAttribute('type', 'hidden'); input.setAttribute('name', 'data'); var button = document.createElement('button'); button.innerText = 'Open In CodePen'; button.id = buttonId; button.style.display = 'none'; form.appendChild(input); form.appendChild(button); var exampleHeader = document.getElementById(exampleHeaderId); exampleHeader.parentNode.insertBefore(form, exampleHeader.nextSibling); // Correct the indentation for the example html var indentedExampleHtml = document.getElementById(exampleCodeId).innerHTML; indentedExampleHtml = indentedExampleHtml.replace(/^\n+/, ''); var indentation = indentedExampleHtml.match(/^\s+/)[0]; var exampleHtml = indentedExampleHtml.replace( new RegExp('^' + indentation, 'gm'), '' ); var path = location.pathname.split('/'); var filename = path.length > 0 ? path[path.length - 1].replace('.html', '') : ''; var postJson = { title: filename, html: exampleHtml, css: '', js: '', head: '', css_external: location.origin + '/content/shared/css/core.css;' + 'https://www.w3.org/StyleSheets/TR/2016/base.css;' + 'https://use.fontawesome.com/releases/v5.1.0/css/all.css', js_external: location.origin + 'content/shared/js/utils.js', }; var totalFetchedFiles = 0; var fileLinks = document.querySelectorAll('#' + exampleFilesId + ' a'); for (let fileLink of fileLinks) { var request = new XMLHttpRequest(); request.open('GET', fileLink.href, true); request.onload = function () { var href = this.responseURL; if (this.status >= 200 && this.status < 400) { if (href.indexOf('css') !== -1) { postJson.css = postJson.css.concat(this.response); } if (href.indexOf('js') !== -1) { postJson.js = postJson.js.concat(this.response); } totalFetchedFiles++; } else { // eslint-disable-next-line no-console console.warn( "Not showing 'Open in Codepen' button. Could not load resource: " + href ); } }; request.onerror = function () { // eslint-disable-next-line no-console console.warn( "Not showing 'Open in Codepen' button. Could not load resource: " + fileLink.href ); }; request.send(); } var timerId = setInterval(() => { if (totalFetchedFiles === fileLinks.length) { clearInterval(timerId); document.getElementById(jsonInputId).value = JSON.stringify(postJson); var button = document.getElementById(buttonId); button.style.display = ''; } }, 500); setTimeout(() => { clearInterval(timerId); // eslint-disable-next-line no-console console.warn( "Not showing 'Open in Codepen' button. Timeout when loading resource." ); }, 10000); } var sourceCode = new aria.widget.SourceCode();