您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows to see all words of a course / selected levels and to export them in CSV format
// ==UserScript== // @name Memrise Course Spreadsheet // @description Allows to see all words of a course / selected levels and to export them in CSV format // @match http://*.memrise.com/course/* // @match https://*.memrise.com/course/* // @match https://*.memrise.com/community/course/* // @run-at document-end // @version 1.4.3 // @grant none // @namespace https://gf.qytechs.cn/users/213706 // ==/UserScript== /* jshint esversion: 8 */ if(typeof unsafeWindow == "undefined") { unsafeWindow = window; } //+-------------------------------------------------------- //| //| ADD SPEADSHEET TAB //| //+-------------------------------------------------------- function onLoad() { if(typeof unsafeWindow.MEMRISE == "undefined") { return; } if(!document.getElementById('content') || document.body.classList.contains('course_edit')) { return; } // Get the current course canonical link and ID var navbar = document.querySelector('.course-tabs-wrap .nav'), linkCourse = navbar.children[0].querySelector('a').getAttribute('href'), getId = linkCourse.match(/\/course\/(\d+)\//); if(!getId || window.location.pathname != linkCourse) { return; } // Add tab Spreadsheet in navbar var link = linkCourse + '#spreadsheet'; idCourse = getId[1]; var li = document.createElement('li'), a = document.createElement('a'); a.setAttribute('href', link); a.innerHTML = 'Spreadsheet'; li.appendChild(a); navbar.appendChild(li); // Handle switching to Spreadsheet tab var courseTitle = document.querySelector('h1.course-name').innerText.trim(), docTitle = `Spreadsheet - ${courseTitle} - Memrise`, openSpreadsheetTab = openTab.bind(null, {docTitle, linkCourse, idCourse}, li); // ... via URL/click if(window.location.hash === "#spreadsheet") { openSpreadsheetTab(); } else { li.addEventListener("click", function(){ unsafeWindow.history.pushState({}, docTitle, link); openSpreadsheetTab(); }); } // ... via browser's back or forward button window.addEventListener('popstate', function(e){ if(window.location.hash === "#spreadsheet") { openSpreadsheetTab(); } else { window.location.reload(); } }); } //+-------------------------------------------------------- //| //| RENDER CONTENT OF TAB (form) //| //+-------------------------------------------------------- function getCookies() { let cookie = {}; document.cookie.split(';').forEach(function(el) { let [k,v] = el.split('='); cookie[k.trim()] = v; }) return cookie; } /** * @param object courseInfo - {docTitle, linkCourse, idCourse} * @param DOMElement li - li to set active in navbar */ function openTab(courseInfo, li) { if(li.classList.contains('active')) { return; } document.title = courseInfo.docTitle; // Set the class "active" on tab var tab = li; while(tab = tab.previousElementSibling) { tab.classList.remove("active"); } li.classList.add("active"); // Get the list of levels var container = document.getElementById('content').firstElementChild, levelElms = container.querySelectorAll('.level'), levels = [], selectbox = {0: "", 1: ""}; for(let i=0; i<levelElms.length; i++) { var elm = levelElms[i], ico = elm.querySelector('.level-ico').classList, level = { href : elm.getAttribute('href'), idx : elm.querySelector('.level-index').innerText.trim(), title: elm.querySelector('.level-title').innerText.trim(), media: (ico.contains('level-ico-multimedia-inactive') || ico.contains('level-ico-multimedia')) }; levels.push(level); selectbox[level.media ? 1 : 0] += `<option value="${i}" selected>${level.idx}. ${level.title}</option>`; } levelElms = null; // Empty the page container.innerHTML = ` <style> #spreadsheet_conf legend { font-weight: 600; } #spreadsheet_conf :disabled { opacity: 0.5; } #spreadsheet_conf td { padding: 5px; } #spreadsheet_conf .form-inline div { float: right; } #spreadsheet_conf .form-check-label, #spreadsheet_conf .form-check-input { display: inline-block; } #spreadsheet_conf .actions { margin-top: 10px; } #spreadsheet_conf button.icon { font-size: 2em; background: none; border: 0; box-shadow: none; padding: 0; opacity: 0.5; } #spreadsheet_conf button.icon:hover { opacity: 1; } #spreadsheet .loading { width: 100%; height: 32px; position: relative; top: -10px; background-image: url("https://static.memrise.com/img/icons/[email protected]"); background-position: center center; background-size: 32px 32px; background-repeat: no-repeat; } #spreadsheet { border-top: 1px solid #e5e5e5; padding-top: 20px; } #spreadsheet:empty { border-top-color: transparent; } #spreadsheet table { background: white; table-layout: fixed; width: 100%; } #spreadsheet td, #spreadsheet th { border: 1px solid #e4e4e4; padding: 2px 5px; vertical-align: top; } #spreadsheet .num { background: rgba(0,0,0,0.03); text-align: right; width: 5%; } #spreadsheet td.num { color: rgba(0,0,0,0.6); white-space: nowrap; } #spreadsheet td.num.ignored { color: rgba(0,0,0,0); } #spreadsheet .score.left { border-right-color: transparent; padding-right: 0; } #spreadsheet .score.right::before { content: "/"; color: rgba(0,0,0,0.2); } #spreadsheet .score.right.ignored::before { content: "-"; color: rgba(0,0,0,0.6); } #spreadsheet .score.right { text-align: left; padding-left: 0; } #spreadsheet .score.often-missed { color:#ff725b; } #spreadsheet .score.sometimes-missed { color:#f08700; } #spreadsheet .course-title { font-weight: normal; font-size: 22px; } #spreadsheet audio, #spreadsheet video { display: block; max-width: 100%; } #spreadsheet .alt span { color: rgba(0,0,0,0.4); } #spreadsheet .alt span::before { content: "- "; } #spreadsheet .more { margin: 5px 0; } #spreadsheet .more + .more { margin-top: 10px; } #spreadsheet .more span { padding: 0 5px; line-height: 1em; } #spreadsheet .highlight { display: block; border-bottom: 2px solid white; color: rgba(0,0,0,0.4); } </style>`.replace(/\s+/g, ' '); var tooltip = document.querySelector('.tooltip.in'); if(tooltip) { tooltip.parentNode.removeChild(tooltip); tooltip = null; } // Add the selectBox of levels / checkbox display alternatives container.innerHTML += `<form id="spreadsheet_conf"> <legend class="form-label">Spreadsheet should contain ...</legend> ${(!selectbox[0] && !selectbox[1]) && `<input type="hidden" id="export0" value="1">` || ""} <table> ${(selectbox[0] || selectbox[1]) && ((selectbox[0] && selectbox[1]) && ` <tr class="form-group"> <td class="form-inline form-ab"> <input class="form-input chooseExport" type="radio" id="chooseExport0" name="chooseExport" value="0" checked> <label class="form-label" for="chooseExport0">Classic levels</label> </td> <td class="form-inline form-ab"> <input class="form-input chooseExport" type="radio" id="chooseExport1" name="chooseExport" value="1"> <label class="form-label" for="chooseExport1">Multimedia levels</label> </td> </tr>`) + `<tr class="form-group"> ${selectbox[0] && ` <td class="form-inline form-ab"> <select id="export0" multiple>${selectbox[0]}</select> </td>`} ${selectbox[1] && ` <td class="form-inline form-ab"> <select id="export1" multiple ${selectbox[0] ? 'disabled' : ''}>${selectbox[1]}</select> </td>`} </tr>` } <tr class="form-group"> <td class="form-inline"> Display: <div> <input class="form-input" type="checkbox" id="exportAlt" name="exportAlt" value="1"> <label class="form-label" for="exportAlt">Alternative answers</label> <br> <input class="form-input" type="checkbox" id="exportMore" name="exportMore" value="1"> <label class="form-label" for="exportMore">Additional informations</label> </div> </td> </tr> </table> <div class="actions"> <button type="submit" name="render">Render</button> <button type="submit" name="export">Export CSV</button> <button type="button" id="exportInMemory" style="display: none" class="icon" title="Export CSV using data in memory (rendered below)">⤓</button> </div> </form> <div id="spreadsheet"></div>`; // Choose to export either multimedia or classic levels if(selectbox[0] && selectbox[1]) { document.getElementById('chooseExport0').addEventListener('click', chooseExport); document.getElementById('chooseExport1').addEventListener('click', chooseExport); function chooseExport(){ var val = this.value; document.getElementById(`export${val}`).disabled = false; document.getElementById(`export${1 - val}`).disabled = true; document.getElementById(`exportAlt`).disabled = (val == 1); document.getElementById(`exportMore`).disabled = (val == 1); } } // Export using in memory data document.getElementById('exportInMemory').addEventListener("click", function(){ new ExportInMemory(); }); // On render/export document.getElementById("spreadsheet_conf").addEventListener("submit", function(e){ e.preventDefault(); // Get the list of levels selected var levelsToExport = [], isMultimedia = (typeof this.elements.export0 == "undefined" || this.elements.export0.disabled) ? 1 : 0, item = this.elements[`export${isMultimedia}`], exportAlt = !isMultimedia && this.elements.exportAlt.checked, exportMore = !isMultimedia && this.elements.exportMore.checked; // Course with no level: retrieve level 1 (ex: /course/233943/livre-1001-phrases-pour-parler-allemand/) if(item.type && item.type == "hidden") { levelsToExport.push({ href : courseInfo.linkCourse, idx : 1, title: '', media: false }); // Retrieve selected levels } else { for(let i = 0; i < item.options.length; i++) { if(item.options[i].selected) { var rank = item.options[i].value; levelsToExport.push(levels[rank]); } } } // Render or export spreadsheet if(isMultimedia) { if(document.activeElement.name == "export"){ new ExportMultimedia(courseInfo.linkCourse, levelsToExport); } else { new SpreadSheetMultimedia(courseInfo.linkCourse, levelsToExport); } } else { if(document.activeElement.name == "export"){ new Export(courseInfo.idCourse, levelsToExport, exportAlt, exportMore); } else { new SpreadSheet(courseInfo.idCourse, levelsToExport, exportAlt, exportMore); } } }); } //+-------------------------------------------------------- //| //| RENDER SPREADSHEET (table) //| //+-------------------------------------------------------- class SpreadSheet { // DOMElement this.body // integer this.idCourse // array this.levels // boolean this.exportAlt /** * @param string idCourse * @param array levels * @param boolean exportAlt - Export alternatives answers * @param boolean exportMore - Export extra columns (visible_info, hidden_info, attributes) */ constructor(idCourse, levels, exportAlt, exportMore) { this.idCourse = idCourse; this.levels = levels; this.exportAlt = exportAlt; this.exportMore = exportMore; this.cookies = getCookies(); // Display a loader var container = document.getElementById("spreadsheet"), loading = this.createLoader(container); // Create the spreadsheet this.extraHeaders = {}; this.createBody(container); this.createContent(loading); } /** * Create the loader and it to the container * @param DOMElement container * @return DOMElement */ createLoader(container) { var loading = document.createElement("div"); loading.setAttribute("class", "loading"); container.innerHTML = ""; container.appendChild(loading); document.getElementById('exportInMemory').style.display = 'none'; return loading; } /** * Create a table * @return DOMElement */ createBody(container) { var table = document.createElement("table"); container.appendChild(table); table.innerHTML = `<thead><tr> <th class="lvl-idx num">Level</th> <th class="item-idx num">#</th> <th class="item-label">Label</th> <th class="item-definition">Definition</th> <th class="score num" colspan="2">Score</th> ${this.exportMore ? `<th class="item-more">More</th>` : ``} </tr></thead> <tbody></tbody>`; this.body = table.lastElementChild; } /** * Populate the body */ async createContent(loading) { var n = this.levels.length-1, hasErr = false; for(let i = 0; i <= n; i++) { let level = this.levels[i]; let options = { method: 'POST', credentials: 'include', referrer: `https://${window.location.host}/aprender/preview?course_id=${this.idCourse}&level_index=${level.idx}`, headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.cookies['csrftoken'] ?? '', }, body: JSON.stringify({ session_source_id: parseInt(this.idCourse), session_source_sub_index: parseInt(level.idx), session_source_type: "course_id_and_level_index", }), }; await fetch(`https://${window.location.host}/v1.21/learning_sessions/preview/`, options) .then((response) => { // Returns 400 if column b isn't defined return response.ok ? response.json() : null; }) .then((data) => { if(data) { var rows = data.learnables, scores = data.progress; // current user scores for(let j = 0; j < rows.length; j++) { var item = rows[j]; this.createRow( level, j, item.screens[1], // includes attributes as well scores && scores[j] ); } } if(i == n && loading){ loading.parentNode.removeChild(loading); loading = null; } }).catch((e) => { hasErr = true; unsafeWindow.console.error(e); loading.setAttribute('class', 'alert alert-danger'); loading.innerHTML = `Something went wrong. Please contact the developer of this script if the error persists.`; }); if(hasErr) { break; } } this.end(hasErr); } /** * Returns the URL to retrieve the words of a level * @param string|integer idLevel * @return string */ getUrl(idLevel) { return `https://${window.location.host}/ajax/session/?course_id=${this.idCourse}&level_index=${idLevel}&session_slug=preview`; } /** * Create a new row * @param object level - Data about current level * @param integer j - Current row number * @param object data - Row data * @param object score - User score for current word * @param object data */ createRow(level, j, data, score) { var tr = document.createElement('tr'), html = ""; html = `<td class="lvl-idx num"><a href="${level.href}">${level.idx}</a></td>`; html += `<td class="item-idx num">${j+1}</td>`; html += `<td class="item-label">${this.getValue(data.item)}</td>`; html += `<td class="item-definition">${this.getValue(data.definition)}</td>`; html += this.getScore(score); if(this.exportMore) { html += `<td class="item-more">`; html += data.visible_info.map(it => {this.addExtraHeader(it.label); return `<div class="more"><span class="highlight">${it.label}</span> ${this.getValue(it, false)}</div>`;}).join(''); html += data.hidden_info.map(it => {this.addExtraHeader(it.label); return `<div class="more"><span class="highlight">${it.label}</span> ${this.getValue(it, false)}</div>`;}).join(''); html += data.attributes.map(it => {this.addExtraHeader(it.label); return `<div class="more"><span class="highlight">${it.label}</span> <span>${escapeHTML(it.value)}</span></div>`;}).join(''); html += `</td>`; } tr.innerHTML = html; this.body.appendChild(tr); } /** * Keep in mind the extra columns in "More" * To be able to export the rendered table to CSV * @param string label */ addExtraHeader(label) { if(typeof this.extraHeaders[label] == 'undefined') { let k = Object.keys(this.extraHeaders).length; this.extraHeaders[label] = k; } } /** * Returns HTML: the content of the columnm (text, image, audio or video) * @param object item * @param boolean[optional] checkAlt - [true] Used to disable alternatives in additionnal informations * @return string */ getValue(item, checkAlt=true) { var txt = ""; switch(item.kind) { case "text" : txt = `<span>${escapeHTML(item.value)}</span>`; if(checkAlt && this.exportAlt) { for(let i=0; i<item.alternatives.length; i++) { txt += `<div class="alt">`; txt += `<span>${escapeHTML(item.alternatives[i])}</span>`; txt += `</div>`; } } break; case "image": txt = `<img src=${item.value[0]} class="text-image" />`; if(checkAlt && this.exportAlt) { for(let i=1; i<item.value.length; i++) { txt += `<div class="alt">`; txt += `<img src=${item.value[i]} class="text-image" />`; txt += `</div>`; } } break; case "audio": txt = `<audio src=${item.value[0].normal} controls></audio>`; if(checkAlt && this.exportAlt) { for(let i=1; i<item.value.length; i++) { txt += `<div class="alt">`; txt += `<audio src=${item.value[i].normal} controls></audio>`; txt += `</div>`; } } break; case "video": txt = `<video src=${item.value[0]} controls>Your browser does not support the video tag.</video>`; if(checkAlt && this.exportAlt) { for(let i=1; i<item.value.length; i++) { txt += `<div class="alt">`; txt += `<video src=${item.value[i]} controls>Your browser does not support the video tag.</video>`; txt += `</div>`; } } break; default: return ""; } return txt; } /** * Returns HTML: the user's score (correct/attemps) * @param object score * @return string */ getScore(score) { if(!score || !score.attempts) { return '<td class="score num" colspan="2">-</td>'; } var successRate, className; if(score.ignored) { successRate = 'Ignored'; className = 'ignored'; } else { successRate = parseInt(score.correct / score.attempts * 100) + '%'; className = (successRate == 100 ? "never-missed" : (successRate < 20 ? "often-missed" : (successRate > 80 ? "rarely-missed" : "sometimes-missed"))); } return `<td class="score left num ${className}" title="${successRate}">${this.truncateNum(""+score.correct)}</td> <td class="score right num ${className}" title="${successRate}">${this.truncateNum(""+score.attempts)}</td>`; } /** * Make sure the number isn't longer than length, or truncate the left (1012, 3 => 12) * @param string str * @param integer[optional] length - [3] * @return string */ truncateNum(str, length=3) { if(str <= length) { return str; } return str.substring(str.length-length).replace(/^0+/, ''); } /** * Return the filename of the generated CSV * @return string */ getFilename() { var filename = 'Memrise-' + idCourse; if(this.levels.length == 1) { filename += '-' + this.levels[0].idx; } return filename + '.csv'; } /** * Called when all levels have been fetched and rendered * We keep extra data needed to export the data in-memory * (rather than fetching all levels all over again) * * @param boolean hasErr Used by subclass */ end(hasErr) { this.body.dataset.file = this.getFilename(); // Keep extra headers labels to export current data if(Object.keys(this.extraHeaders).length) { let extra = []; for(let label in this.extraHeaders) { extra[this.extraHeaders[label]] = label; } this.body.dataset.extraHeaders = JSON.stringify(extra); } else { delete this.body.dataset.extraHeaders; } document.getElementById('exportInMemory').style.display = null; } } //+-------------------------------------------------------- //| //| RENDER SPREADSHEET - MULTIMEDIA (table) //| //+-------------------------------------------------------- class SpreadSheetMultimedia { // DOMElement this.body // string this.urlCourse // array this.levels /** * @param string urlCourse * @param array levels */ constructor(urlCourse, levels) { this.urlCourse = urlCourse; this.levels = levels; // Display a loader var container = document.getElementById("spreadsheet"), loading = this.createLoader(container); // Create the spreadsheet this.createBody(container); this.createContent(loading); } /** * Create the loader and it to the container * @param DOMElement container * @return DOMElement */ createLoader(container) { var loading = document.createElement("div"); loading.setAttribute("class", "loading"); container.innerHTML = ""; container.appendChild(loading); document.getElementById('exportInMemory').style.display = 'none'; return loading; } /** * Create a table * @return DOMElement */ createBody(container) { var table = document.createElement("table"); container.appendChild(table); table.innerHTML = `<thead><tr> <th class="lvl-idx num">Level</th> <th class="item-definition">Content</th> </tr></thead> <tbody></tbody>`; this.body = table.lastElementChild; } /** * Populate the body */ async createContent(loading) { var n = this.levels.length-1, hasErr = false; for(let i = 0; i <= n; i++) { let level = this.levels[i]; await fetch(this.getUrl(level.idx), { credentials: "same-origin" }) .then((response) => response.text()) .then((html) => { var data = html.match(/^ *var level_multimedia =(.*)/m); if(!data) { // Empty level (ex: /course/50121/flags-of-the-world/9/) return ""; } eval('data = ' + data[1].trim()); return data; }) .then((data) => { this.createRow(level, data); if(i == n){ loading.parentNode.removeChild(loading); loading = null; } }).catch((e) => { hasErr = true; unsafeWindow.console.error(e); loading.setAttribute('class', 'alert alert-danger'); loading.innerHTML = `Something went wrong. Please contact the developer of this script if the error persists.`; }); if(hasErr) { break; } } this.end(hasErr); } /** * Returns the URL to retrieve the words of a level * @param string|integer idLevel * @return string */ getUrl(idLevel) { return `https://${window.location.host}${this.urlCourse}${idLevel}/`; } /** * Create a new row * @param object data */ createRow(level, data) { var tr = document.createElement('tr'), html = ""; html = `<td class="lvl-idx num"><a href="${level.href}">${level.idx}</a></td>`; html += `<td class="item-label"> <h3 class="course-title">${escapeHTML(level.title)}</h3> <div class="multimedia-raw" style="display: none">${escapeHTML(data)}</div> <div class="multimedia-wrapper">${this.parseMarkdown(data)}</div> </td>`; tr.innerHTML = html; this.body.appendChild(tr); unsafeWindow.MEMRISE.renderer.do_embeds(unsafeWindow.$(tr)); } /** * Converts Markdown to HTML using Memrise's renderer * @param string txt * @return string */ parseMarkdown(txt) { return unsafeWindow.MEMRISE.renderer.rich_format(txt); } /** * Return the filename of the generated CSV * @return string */ getFilename() { var filename = 'Memrise-' + idCourse; if(this.levels.length == 1) { filename += '-' + this.levels[0].idx; } return filename + '-multimedia.csv'; } /** * Called when all levels have been fetched and rendered * We keep extra data needed to export the data in-memory * (rather than fetching all levels all over again) * * @param boolean hasErr Used by subclass */ end(hasErr) { this.body.dataset.file = this.getFilename(); delete this.body.dataset.extraHeaders; document.getElementById('exportInMemory').style.display = null; } } //+-------------------------------------------------------- //| //| EXPORT CSV //| //+-------------------------------------------------------- class Export extends SpreadSheet { /** * Create the loader and it to the container * @param DOMElement container * @return DOMElement */ createLoader(container) { var loading = document.createElement("div"); loading.setAttribute("class", "loading"); if(container.children.length) { container.insertBefore(loading, container.firstElementChild); } else { container.appendChild(loading); } return loading; } /** * Init the content of the CSV * @return DOMElement */ createBody(container) { this.body = ''; this.headers = {}; } /** * Create a new row * @param object level * @param integer j * @param object data * @param object score */ createRow(level, j, data, score) { this.body += level.idx + ','; this.body += (j+1) + ','; this.body += this.getValue(data.item) + ','; this.body += this.getValue(data.definition) + ','; if(score && score.attempts){ this.body += score.correct + ','; this.body += score.attempts + ','; this.body += parseInt(score.correct / score.attempts * 100); } else { this.body += ',,'; } // Retrieve additional columns if(this.exportMore) { var arr = []; this.getExtraColumns(arr, data.visible_info); this.getExtraColumns(arr, data.hidden_info); this.getExtraColumns(arr, data.attributes); // Add columns for(let i=0; i<arr.length; i++){ this.body += ','; this.body += arr[i] || ''; } } this.body += '\n'; } /** * Retrieves the additional content in data * And puts it in the right place in arr * @param array arr * @param object[pointer] data */ getExtraColumns(arr, data){ var k; for(let i=0; i<data.length; i++) { var it = data[i]; if(typeof this.headers[it.label] != 'undefined') { k = this.headers[it.label]; } else { k = Object.keys(this.headers).length; this.headers[it.label] = k; } if(typeof it.kind != "undefined") { arr[k] = this.getValue(it, false); } else { arr[k] = escapeCSV(it.value); } } } /** * Returns CSV-escaped text: the content of the column (text, image, audio or video) * @param object item * @param boolean[optional] checkAlt - [true] Used to disable alternatives in additionnal informations * @return string */ getValue(item, checkAlt=true) { var txt; switch(item.kind) { case "text" : txt = item.value; if(checkAlt && this.exportAlt) { for(let i=0; i<item.alternatives.length; i++) { txt += '\n' + item.alternatives[i]; } } break; case "image": txt = item.value[0]; if(checkAlt && this.exportAlt) { for(let i=1; i<item.value.length; i++) { txt += '\n' + item.value[i]; } } break; case "audio": txt = item.value[0].normal; if(checkAlt && this.exportAlt) { for(let i=1; i<item.value.length; i++) { txt += '\n' + item.value[i].normal; } } break; case "video": txt = item.value[0]; if(checkAlt && this.exportAlt) { for(let i=1; i<item.value.length; i++) { txt += '\n' + item.value[i]; } } break; default: return ""; } return escapeCSV(txt); } /** * Trigger download of the CSV (in-memory) * @param boolean hasErr */ end(hasErr) { if(hasErr) { return; } download(this.getFilename(), this.getHeaders() + '\n' + this.body); } /** * Retrieve all headers * Includes visible_info / hidden_info / attributes if that option was checked * @return string */ getHeaders() { var headers = 'Level,#,Label,Definition,Correct,Attempts,Score %'; if(!this.exportMore) { return headers; } var extra = []; for(var label in this.headers) { extra[this.headers[label]] = escapeCSV(label); } return headers + (extra.length ? ',' + extra.join(',') : ''); } } //+-------------------------------------------------------- //| //| EXPORT CSV - MULTIMEDIA //| //+-------------------------------------------------------- class ExportMultimedia extends SpreadSheetMultimedia { /** * Create the loader and it to the container * @param DOMElement container * @return DOMElement */ createLoader(container) { var loading = document.createElement("div"); loading.setAttribute("class", "loading"); if(container.children.length) { container.insertBefore(loading, container.firstElementChild); } else { container.appendChild(loading); } return loading; } /** * Init the content of the CSV * @return DOMElement */ createBody(container) { this.body = 'Level,Title,Content\n'; } /** * Create a new row * @param object data */ createRow(level, data) { this.body += level.idx + ','; this.body += escapeCSV(level.title) + ','; this.body += escapeCSV(data) + '\n'; } /** * Trigger download of the CSV * @param boolean hasErr */ end(hasErr) { if(!hasErr) { download(this.getFilename(), this.body); } } } //+-------------------------------------------------------- //| //| EXPORT IN-MEMORY //| //+-------------------------------------------------------- class ExportInMemory { /** * Entrypoint */ constructor() { var container = document.getElementById('spreadsheet'), body = container.querySelector('tbody'), filename = body.dataset.file; var extraHeaders = this.decodeExtraHeaders(body.dataset.extraHeaders), headers = Array.from(container.querySelector('thead tr').children) .map(node => node.innerText); var csv = this.getHeaders(headers, extraHeaders) + '\n' + this.getData(body, headers, extraHeaders); download(filename || ('Memrise-' + idCourse + '.csv'), csv); } /** * @param array headers * @param array extraHeaders * @return string */ getHeaders(_headers, extraHeaders) { var headers = [..._headers]; var k = headers.indexOf('Score'); if(k != -1) { headers.splice(k, 1, ...['Correct', 'Attempts', 'Score %']); } k = headers.indexOf('More'); if(k != -1) { headers.splice(k, 1, ...extraHeaders); } k = headers.indexOf('Content'); if(k != -1) { headers.splice(k, 1, ...['Title', 'Content']); } return headers.map(label => escapeCSV(label)).join(','); } /** * Retrieve the JSON-decoded list of extra headers * Or an empty list * * @param string|undefined data * @return array */ decodeExtraHeaders(data) { return typeof data == 'undefined' ? [] : JSON.parse(data); } /** * Retrieve the rendered table as a CSV string * @param DOMElement body * @return string */ getData(body, headers, extraHeaders) { var csv = ''; for(let i=0; i<body.children.length; i++) { let tr = body.children[i], data = []; let k = 0; for(let j=0; j<headers.length; j++) { let label = headers[j], td = tr.children[k]; switch(label) { case 'Level': case '#': csv += td.innerText + ','; break; case 'Score': if(!td.hasAttribute('colspan')) { let correct = parseInt(td.innerText, 10), attempt = parseInt(tr.children[k+1].innerText, 10); k++; csv += correct + ','; csv += attempt + ','; csv += parseInt(correct/attempt * 100); } else { csv += ',,'; } break; case 'More': let more = td.querySelectorAll('.more'), extra = {}; // Retrieve all additionnal that have been defined for(let j2=0; j2<more.length; j2++) { let label = more[j2].firstElementChild.innerText, content = this.getValue(more[j2].lastElementChild); extra[label] = escapeCSV(content); } // Put them in order for(let j2=0; j2<extraHeaders.length; j2++) { let label = extraHeaders[j2]; csv += ',' + (typeof extra[label] == 'undefined' ? '' : extra[label]); } break; // Multimedia case 'Content': csv += escapeCSV(td.children[0].innerText.trim()) + ','; csv += escapeCSV(td.children[1].innerText.trim()); break; default: csv += escapeCSV(this.getValue(td.firstElementChild, true)) + ','; break; } k++; } csv += '\n'; } return csv; } /** * Retrieve the text of a DOMElement * @param DOMElement node * @param boolean siblings - [false] Return the content of siblings too * @return string */ getValue(node, siblings=false) { if(["IMG", "AUDIO", "VIDEO"].indexOf(node.nodeName) != -1) { var links = Array.from(node.parentNode.querySelectorAll(node.nodeName)) .map(node => node.getAttribute('src')); return links.join('\n'); } else { return siblings ? node.parentNode.innerText : node.innerText; } } } //+-------------------------------------------------------- //| //| COMMON FONCTIONS (in-memory) //| //+-------------------------------------------------------- /** * Escape HTML * @param string txt * @return txt */ function escapeHTML(txt) { if(typeof txt != "string") { return ""; } return txt.replace(/</g, '<').replace(/>/g, '>'); } /** * Escape text for CSV * Surround with quotes and escape quotes inside text */ function escapeCSV(txt) { if(typeof txt != "string") { return ""; } return '"' + txt.replace(/"/g, '""') + '"'; } /** * Trigger download of a file * @param string filename * @param string txt */ function download(filename, txt) { var blob = new Blob([txt], {type: 'text/csv'}); if(window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(blob, filename); } else { var link = window.document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } window.addEventListener('load', onLoad, false);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址