From 685e48b2dec0c01e670c78901e4389d17d3b565f Mon Sep 17 00:00:00 2001 From: st143971 Date: Mon, 12 Aug 2024 15:22:20 +0200 Subject: [PATCH 1/2] Split Search and Display Template and Javascript Module - Moved some html creation from javascript into new template --- .gitignore | 3 +- amd/build/display.min.js | 11 +++ amd/build/display.min.js.map | 1 + ...earch_and_display.min.js => search.min.js} | 8 +- amd/build/search.min.js.map | 1 + amd/build/search_and_display.min.js.map | 1 - amd/src/display.js | 77 ++++++++++++++++++ amd/src/{search_and_display.js => search.js} | 41 +--------- block_booksearch.php | 2 +- templates/display.mustache | 78 +++++++++++++++++++ ...h_and_display.mustache => search.mustache} | 4 +- version.php | 2 +- 12 files changed, 182 insertions(+), 47 deletions(-) create mode 100644 amd/build/display.min.js create mode 100644 amd/build/display.min.js.map rename amd/build/{search_and_display.min.js => search.min.js} (70%) create mode 100644 amd/build/search.min.js.map delete mode 100644 amd/build/search_and_display.min.js.map create mode 100644 amd/src/display.js rename amd/src/{search_and_display.js => search.js} (89%) create mode 100644 templates/display.mustache rename templates/{search_and_display.mustache => search.mustache} (92%) diff --git a/.gitignore b/.gitignore index 1c9497b..30df0b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store .git/ /amd/Makefile -/lang/de \ No newline at end of file +/lang/de +Changelog.md \ No newline at end of file diff --git a/amd/build/display.min.js b/amd/build/display.min.js new file mode 100644 index 0000000..ef5a4bc --- /dev/null +++ b/amd/build/display.min.js @@ -0,0 +1,11 @@ +define("block_booksearch/display",["exports","core/templates"],(function(_exports,_templates){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayResults= +/** + * Block core and UI + * + * @module block_booksearch/display + * @copyright 2024 University of Stuttgart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +function(element,searchResults,chapterLabel){const data=function(searchResults,chapterLabel){const data=[];for(const pdfName in searchResults){if(!searchResults.hasOwnProperty(pdfName))continue;const chapters=[];for(const chapter in searchResults[pdfName])searchResults[pdfName].hasOwnProperty(chapter)&&chapters.push({chapter:chapter,bookurl:searchResults[pdfName][chapter].bookurl,context:searchResults[pdfName][chapter].context,chapterLabel:chapterLabel});data.push({filename:pdfName,chapters:chapters})}return{data:data}}(searchResults,chapterLabel);(0,_templates.render)("block_booksearch/display",data).then((html=>{element.innerHTML=html})).catch((ex=>{window.console.error("Template rendering failed: ",ex)}))}})); + +//# sourceMappingURL=display.min.js.map \ No newline at end of file diff --git a/amd/build/display.min.js.map b/amd/build/display.min.js.map new file mode 100644 index 0000000..7370a48 --- /dev/null +++ b/amd/build/display.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"display.min.js","sources":["../src/display.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/display\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport { render } from 'core/templates';\n\n/**\n * Processes and Displays the search results inside the given element.\n * @param {HTMLElement} element The elemtent to display the results inside.\n * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]].\n * @param {String} chapterLabel The label meaning \"Chapter\" in the active language.\n */\nexport function displayResults(element, searchResults, chapterLabel) {\n const data = preprocessSearchResults(searchResults, chapterLabel);\n render('block_booksearch/display', data).then((html) => {\n element.innerHTML = html;\n }).catch(ex => {\n window.console.error('Template rendering failed: ', ex);\n });\n}\n\n\n/**\n * This function processes the searchResults, so they are usable for the template.\n * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]].\n * @param {String} chapterLabel The label meaning \"Chapter\" in the active language.\n * @returns Array of search results processed to be perfectly used by the template.\n */\nfunction preprocessSearchResults(searchResults, chapterLabel) {\n const data = [];\n\n for (const pdfName in searchResults) {\n if (!searchResults.hasOwnProperty(pdfName)) {\n continue;\n }\n const chapters = [];\n\n for (const chapter in searchResults[pdfName]) {\n if (!searchResults[pdfName].hasOwnProperty(chapter)) {\n continue;\n }\n chapters.push({\n chapter: chapter,\n bookurl: searchResults[pdfName][chapter].bookurl,\n context: searchResults[pdfName][chapter].context,\n chapterLabel: chapterLabel\n });\n\n }\n\n data.push({\n filename: pdfName,\n chapters: chapters\n });\n\n }\n\n return { data: data };\n}"],"names":["element","searchResults","chapterLabel","data","pdfName","hasOwnProperty","chapters","chapter","push","bookurl","context","filename","preprocessSearchResults","then","html","innerHTML","catch","ex","window","console","error"],"mappings":";;;;;;;;SA8B+BA,QAASC,cAAeC,oBAC7CC,cAeuBF,cAAeC,oBACtCC,KAAO,OAER,MAAMC,WAAWH,cAAe,KAC5BA,cAAcI,eAAeD,wBAG5BE,SAAW,OAEZ,MAAMC,WAAWN,cAAcG,SAC3BH,cAAcG,SAASC,eAAeE,UAG3CD,SAASE,KAAK,CACVD,QAASA,QACTE,QAASR,cAAcG,SAASG,SAASE,QACzCC,QAAST,cAAcG,SAASG,SAASG,QACzCR,aAAcA,eAKtBC,KAAKK,KAAK,CACNG,SAAUP,QACVE,SAAUA,iBAKX,CAAEH,KAAMA,MA5CFS,CAAwBX,cAAeC,oCAC7C,2BAA4BC,MAAMU,MAAMC,OAC3Cd,QAAQe,UAAYD,QACrBE,OAAMC,KACLC,OAAOC,QAAQC,MAAM,8BAA+BH"} \ No newline at end of file diff --git a/amd/build/search_and_display.min.js b/amd/build/search.min.js similarity index 70% rename from amd/build/search_and_display.min.js rename to amd/build/search.min.js index 14ca28e..61cf982 100644 --- a/amd/build/search_and_display.min.js +++ b/amd/build/search.min.js @@ -1,11 +1,11 @@ -define("block_booksearch/search_and_display",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(label){chapterLabel=label;document.getElementById("bs-search-input").addEventListener("input",handleSearchInputChange)}; +define("block_booksearch/search",["exports","block_booksearch/display"],(function(_exports,_display){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(label){chapterLabel=label;document.getElementById("bs-search-input").addEventListener("input",handleSearchInputChange)}; /** * Block core and UI * - * @module block_booksearch/search_and_display + * @module block_booksearch/search * @copyright 2024 University of Stuttgart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -let chapterLabel="Chapter";function handleSearchInputChange(event){const searchTerm=event.target.value,searchTermLabel=document.getElementById("bs-search-term-label").value,courseContent=JSON.parse(atob(document.getElementById("bs-json-course-content").value));let searchResults=[];searchTerm&&(searchResults=function(courseContent,searchTerm,contextLength){const results={};return courseContent.forEach((section=>{const context=function(content,searchTerm,contextLength){const searchContent=content.toLowerCase();if(searchTerm=searchTerm.toLowerCase(),-1===searchContent.indexOf(searchTerm))return"";const occurrenceIndexes=function(text,term){const occurrences=[],termLength=term.length;let index,offset=0;for(;-1!==(index=text.indexOf(term,offset));){const occurrence={start:index,end:index+termLength};occurrences.push(occurrence),offset=index+1}return occurrences}(searchContent,searchTerm),[words,wordIndexes]=function(text){const words=[],wordIndexes=[],regex=/\S+/g;let match;for(;null!==(match=regex.exec(text));)words.push(match[0]),wordIndexes.push(match.index);return[words,wordIndexes]}(content),contextPositions=function(occurrences,wordIndexes,contextLength){let occurrenceContextPositions=function(occurrences,wordIndexes,contextLength){const results=[];let currentOccurrenceIndex=0;for(let wordNumber=0;wordNumber=occurrences.length);wordNumber++){if(wordNumber+1>=wordIndexes.length){const start=Math.max(0,wordNumber-contextLength),length=null;results.push({start:start,end:length});continue}const currentOccurrence=occurrences[currentOccurrenceIndex];if(wordIndexes[wordNumber+1]<=currentOccurrence.start)continue;const position={start:Math.max(0,wordNumber-contextLength),end:getContextEnd(wordIndexes,wordNumber,currentOccurrence.end,contextLength)};results.push(position),currentOccurrenceIndex++}return results}(occurrences,wordIndexes,contextLength);return occurrenceContextPositions=function(contextPositions){const results=[];for(let i=0;i=contextPositions[i+1].start;)end=contextPositions[i+1].end,i++;const mergedPosition={start:start,end:null!==end?end+1:void 0};if(results.push(mergedPosition),!end)break}return results}(occurrenceContextPositions),occurrenceContextPositions}(occurrenceIndexes,wordIndexes,contextLength);return function(words,contextPositions){let context="... ";return contextPositions.forEach((position=>{const subcontextWords=words.slice(position.start,position.end);context+=subcontextWords.join(" "),context+=" ... "})),context}(words,contextPositions)}(section.content,searchTerm,contextLength);section.context=context,context.length<1||(results.hasOwnProperty(section.filename)||(results[section.filename]={}),results[section.filename].hasOwnProperty(section.page)?results[section.filename][section.page].context+=section.context:results[section.filename][section.page]={filename:section.filename,url:section.url,bookUrl:section.bookUrl,context:section.context})})),results}(courseContent,searchTerm,5)),document.getElementById("bs-search-term").innerHTML=searchTermLabel+searchTerm,document.getElementById("bs-content").innerHTML=function(searchResults){let display="";for(var pdfName in searchResults){for(var chapter in display+="

"+pdfName+"

",display+='"}return display}(searchResults)}function getContextEnd(wordIndexes,startNumber,endIndex,contextLength){for(let i=startNumber;i=wordIndexes.length)return null;if(wordIndexes[i+1]<=endIndex)continue;return i+contextLength}return null}})); +let chapterLabel="Chapter";function handleSearchInputChange(event){const searchTerm=event.target.value,searchTermLabel=document.getElementById("bs-search-term-label").value,courseContent=JSON.parse(atob(document.getElementById("bs-json-course-content").value));let searchResults=[];searchTerm&&(searchResults=function(courseContent,searchTerm,contextLength){const results={};return courseContent.forEach((section=>{const context=function(content,searchTerm,contextLength){const searchContent=content.toLowerCase();if(searchTerm=searchTerm.toLowerCase(),-1===searchContent.indexOf(searchTerm))return"";const occurrenceIndexes=function(text,term){const occurrences=[],termLength=term.length;let index,offset=0;for(;-1!==(index=text.indexOf(term,offset));){const occurrence={start:index,end:index+termLength};occurrences.push(occurrence),offset=index+1}return occurrences}(searchContent,searchTerm),[words,wordIndexes]=function(text){const words=[],wordIndexes=[],regex=/\S+/g;let match;for(;null!==(match=regex.exec(text));)words.push(match[0]),wordIndexes.push(match.index);return[words,wordIndexes]}(content),contextPositions=function(occurrences,wordIndexes,contextLength){let occurrenceContextPositions=function(occurrences,wordIndexes,contextLength){const results=[];let currentOccurrenceIndex=0;for(let wordNumber=0;wordNumber=occurrences.length);wordNumber++){if(wordNumber+1>=wordIndexes.length){const start=Math.max(0,wordNumber-contextLength),length=null;results.push({start:start,end:length});continue}const currentOccurrence=occurrences[currentOccurrenceIndex];if(wordIndexes[wordNumber+1]<=currentOccurrence.start)continue;const position={start:Math.max(0,wordNumber-contextLength),end:getContextEnd(wordIndexes,wordNumber,currentOccurrence.end,contextLength)};results.push(position),currentOccurrenceIndex++}return results}(occurrences,wordIndexes,contextLength);return occurrenceContextPositions=function(contextPositions){const results=[];for(let i=0;i=contextPositions[i+1].start;)end=contextPositions[i+1].end,i++;const mergedPosition={start:start,end:null!==end?end+1:void 0};if(results.push(mergedPosition),!end)break}return results}(occurrenceContextPositions),occurrenceContextPositions}(occurrenceIndexes,wordIndexes,contextLength);return function(words,contextPositions){let context="... ";return contextPositions.forEach((position=>{const subcontextWords=words.slice(position.start,position.end);context+=subcontextWords.join(" "),context+=" ... "})),context}(words,contextPositions)}(section.content,searchTerm,contextLength);section.context=context,context.length<1||(results.hasOwnProperty(section.filename)||(results[section.filename]={}),results[section.filename].hasOwnProperty(section.page)?results[section.filename][section.page].context+=section.context:results[section.filename][section.page]={bookurl:section.bookurl,context:section.context})})),results}(courseContent,searchTerm,5)),document.getElementById("bs-search-term").innerHTML=searchTermLabel+searchTerm,(0,_display.displayResults)(document.getElementById("bs-content"),searchResults,chapterLabel)}function getContextEnd(wordIndexes,startNumber,endIndex,contextLength){for(let i=startNumber;i=wordIndexes.length)return null;if(wordIndexes[i+1]<=endIndex)continue;return i+contextLength}return null}})); -//# sourceMappingURL=search_and_display.min.js.map \ No newline at end of file +//# sourceMappingURL=search.min.js.map \ No newline at end of file diff --git a/amd/build/search.min.js.map b/amd/build/search.min.js.map new file mode 100644 index 0000000..50feb53 --- /dev/null +++ b/amd/build/search.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"search.min.js","sources":["../src/search.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/search\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport { displayResults } from 'block_booksearch/display';\n\n/**\n * String label 'Chapter' in the current language.\n */\nlet chapterLabel = 'Chapter';\n\n/**\n * Sets up the event listener for search input changes.\n * @param {string} label String label 'Chapter' in the current language.\n */\nexport function init(label) {\n chapterLabel = label;\n const inputElement = document.getElementById('bs-search-input');\n inputElement.addEventListener('input', handleSearchInputChange);\n}\n\n/**\n * Function to handle search term input event\n * @param {*} event The input event catched by the listener.\n */\nfunction handleSearchInputChange(event) {\n const searchTerm = event.target.value;\n const searchTermLabel = document.getElementById('bs-search-term-label').value;\n const courseContent = JSON.parse(atob(document.getElementById('bs-json-course-content').value));\n const contextLength = 5;\n\n let searchResults = [];\n // We search the content if we have a search term.\n if (searchTerm) {\n searchResults = getSearchResults(courseContent, searchTerm, contextLength);\n }\n\n // Update the inner HTML of the element with ID 'bs-search-term' to display the current search Term.\n document.getElementById(\"bs-search-term\").innerHTML = searchTermLabel + searchTerm;\n\n // Update the inner HTML of the element with ID 'bs-content' to display the results.\n displayResults(document.getElementById(\"bs-content\"), searchResults, chapterLabel);\n}\n\n\n/**\n * Processes an array of content sections to extract and format search results based on a search term and context length.\n * @param {Array} courseContent Array of content sections, where each section is an object\n * with keys 'content', 'filename', and 'page'.\n * @param {string} searchTerm The term to search for within the content sections.\n * @param {number} contextLength The number of words to include before and after each occurrence of the search term.\n * @return {Object} The search results, with filenames as keys and sections as values.\n * [filename: {page: {section}}]\n */\nfunction getSearchResults(courseContent, searchTerm, contextLength) {\n const results = {};\n\n courseContent.forEach(section => {\n // Get any search results with context from the section.\n const context = getSectionSearchResultContext(section.content, searchTerm, contextLength);\n\n // Add result to the section object.\n section.context = context;\n\n // Skip this section if there's no context (no result).\n if (context.length < 1) {\n return;\n }\n\n // Create new file entry in results if it does not exist.\n if (!results.hasOwnProperty(section.filename)) {\n results[section.filename] = {};\n }\n\n // Set chapter entry as section or add section context to existing chapter entry.\n if (!results[section.filename].hasOwnProperty(section.page)) {\n results[section.filename][section.page] = {\n bookurl: section.bookurl,\n context: section.context\n };\n } else {\n // Append to existing context if the section already exists.\n results[section.filename][section.page].context += section.context;\n }\n });\n\n return results;\n}\n\n\n/**\n * Get a combined string of any found search term occurrences in the content with the surrounding words as context.\n * @param {string} content The text content to search in.\n * @param {string} searchTerm The term to search for in this content.\n * @param {number} contextLength The number of words on each side surrounding the found occurrence to be returned as context.\n * @return {string} Text snippets for each term occurrence with their context, combined as one.\n */\nfunction getSectionSearchResultContext(content, searchTerm, contextLength) {\n const searchContent = content.toLowerCase();\n searchTerm = searchTerm.toLowerCase();\n\n // Check if the search term is present in the content.\n if (searchContent.indexOf(searchTerm) === -1) {\n return \"\";\n }\n\n // Get the text indexes of the term occurrences. Array of objects with 'start' and 'end' properties.\n const occurrenceIndexes = findOccurrences(searchContent, searchTerm);\n\n // Get the text as words and word starting indexes.\n const [words, wordIndexes] = splitTextIntoWords(content);\n\n // Get the word number positions of the context we want to return. Objects with 'start' and 'end' properties.\n const contextPositions = getContextPositions(occurrenceIndexes, wordIndexes, contextLength);\n\n // Get the combined string context.\n const context = getContext(words, contextPositions);\n\n return context;\n}\n\n\n/**\n * Searches for occurrences of a term in a given text and returns an array of occurrence objects.\n * Each occurrence object contains the start index (position in text) and the end index.\n * @param {string} text The text in which to search for the term.\n * @param {string} term The term to search for within the text.\n * @return {Array} An array of objects, each with 'start' and 'end' properties.\n */\nfunction findOccurrences(text, term) {\n const occurrences = [];\n const termLength = term.length;\n\n // Use indexOf to find the occurrences of the term in the text.\n let offset = 0;\n let index;\n\n while ((index = text.indexOf(term, offset)) !== -1) {\n const occurrence = {\n start: index,\n end: index + termLength\n };\n occurrences.push(occurrence);\n // Update the offset to search for the next occurrence.\n offset = index + 1;\n }\n\n return occurrences;\n}\n\n\n/**\n * Splits the text into words and returns an array of word strings and an array of word starting indexes.\n * @param {string} text The original text.\n * @return {Array} A pair of arrays [array of string words, array of word starting indexes].\n */\nfunction splitTextIntoWords(text) {\n const words = [];\n const wordIndexes = [];\n\n const regex = /\\S+/g; // Matches any non-whitespace sequence.\n\n let match;\n while ((match = regex.exec(text)) !== null) {\n words.push(match[0]); // Gather the word string.\n wordIndexes.push(match.index); // Gather the word starting index.\n }\n\n return [words, wordIndexes];\n}\n\n\n/**\n * Returns an array of positional data for each occurrence, including starting word number and ending word number.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (last word number of context or null if at end of text) properties.\n */\nfunction getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength) {\n const results = [];\n let currentOccurrenceIndex = 0;\n\n // Iterate through each word index\n for (let wordNumber = 0; wordNumber < wordIndexes.length; wordNumber++) {\n // If there are no more occurrences to check, exit the loop\n if (currentOccurrenceIndex >= occurrences.length) {\n break;\n }\n\n // Check if this is the last word\n if (wordNumber + 1 >= wordIndexes.length) {\n const start = Math.max(0, wordNumber - contextLength);\n const length = null;\n results.push({\n start: start,\n end: length\n });\n continue;\n }\n\n // The current occurrence to check against\n const currentOccurrence = occurrences[currentOccurrenceIndex];\n\n // If this word is not (yet) part of the context\n if (wordIndexes[wordNumber + 1] <= currentOccurrence.start) {\n continue;\n }\n\n // This word begins an occurrence\n const start = Math.max(0, wordNumber - contextLength);\n const end = getContextEnd(wordIndexes, wordNumber, currentOccurrence.end, contextLength);\n\n const position = {\n start: start,\n end: end\n };\n\n results.push(position);\n\n currentOccurrenceIndex++;\n }\n\n return results;\n}\n\n\n/**\n * Returns the number of the last word still in the context. Returns null if the context is the rest of all words.\n * @param {Array} wordIndexes An array that has the text starting index for each word.\n * @param {number} startNumber The word number where the occurrence starts.\n * @param {number} endIndex The text index where the occurrence ends.\n * @param {number} contextLength The amount of words that get returned on each side of the occurrence as context.\n * @return {?number} The word number of the last word in the context or null if it ends with the text.\n */\nfunction getContextEnd(wordIndexes, startNumber, endIndex, contextLength) {\n for (let i = startNumber; i < wordIndexes.length; i++) {\n // Check if the context reaches the last word\n if (i + contextLength + 1 >= wordIndexes.length) {\n return null;\n }\n\n // Check if the occurrence is part of the next word\n if (wordIndexes[i + 1] <= endIndex) {\n continue;\n }\n\n // Calculate the last word number in context\n const lastWordInContext = i + contextLength;\n\n return lastWordInContext;\n }\n return null;\n}\n\n\n/**\n * Returns an array of positional data for each occurrence set, including starting word number, ending word number, and word count.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction getContextPositions(occurrences, wordIndexes, contextLength) {\n let occurrenceContextPositions = getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength);\n occurrenceContextPositions = mergeOccurrenceContextPositions(occurrenceContextPositions);\n return occurrenceContextPositions;\n}\n\n\n/**\n * Merge overlapping occurrence positions together.\n * @param {Array} contextPositions Array of position objects with 'start' (first word in context)\n * and 'end' (last word in context or null if at end of text) properties.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction mergeOccurrenceContextPositions(contextPositions) {\n const results = [];\n\n for (let i = 0; i < contextPositions.length; i++) {\n // First position.\n let position = contextPositions[i];\n let start = position.start;\n let end = position.end;\n\n // Further positions.\n while (\n i + 1 < contextPositions.length && // Check if there is a next position.\n contextPositions[i].end && // Check if 'end' is null. We can then ignore all upcoming positions.\n contextPositions[i].end >= contextPositions[i + 1].start // Check if this and the next positions overlap.\n ) {\n end = contextPositions[i + 1].end; // The positions overlap so the end gets set to ht next positions end.\n i++;\n }\n\n const mergedPosition = {\n start: start,\n end: end !== null ? end + 1 : undefined // If end is not null we set end to the next element.\n };\n\n results.push(mergedPosition);\n\n if (!end) { // We can ignore all later positions as we already are at the end of possible context.\n break;\n }\n }\n\n return results;\n}\n\n\n/**\n * Based on given context positions, return a combined string from a list of words.\n * @param {Array} words List of all words.\n * @param {Array} contextPositions An array of occurrence position objects, each with 'start' (first word number of context),\n * and 'end' (first word number outside the context or undefinded if at end of text)\n * @return {string} A combined string of all given positions.\n */\nfunction getContext(words, contextPositions) {\n let context = \"... \";\n\n contextPositions.forEach(position => {\n const subcontextWords = words.slice(position.start, position.end);\n context += subcontextWords.join(\" \");\n context += \" ... \";\n });\n\n return context;\n}"],"names":["label","chapterLabel","document","getElementById","addEventListener","handleSearchInputChange","event","searchTerm","target","value","searchTermLabel","courseContent","JSON","parse","atob","searchResults","contextLength","results","forEach","section","context","content","searchContent","toLowerCase","indexOf","occurrenceIndexes","text","term","occurrences","termLength","length","index","offset","occurrence","start","end","push","findOccurrences","words","wordIndexes","regex","match","exec","splitTextIntoWords","contextPositions","occurrenceContextPositions","currentOccurrenceIndex","wordNumber","Math","max","currentOccurrence","position","getContextEnd","getEachOccurrenceContextPosition","i","mergedPosition","undefined","mergeOccurrenceContextPositions","getContextPositions","subcontextWords","slice","join","getContext","getSectionSearchResultContext","hasOwnProperty","filename","page","bookurl","getSearchResults","innerHTML","startNumber","endIndex"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/search_and_display.min.js.map b/amd/build/search_and_display.min.js.map deleted file mode 100644 index 3bcc50f..0000000 --- a/amd/build/search_and_display.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"search_and_display.min.js","sources":["../src/search_and_display.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/search_and_display\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * String label 'Chapter' in the current language.\n */\nlet chapterLabel = 'Chapter';\n\n/**\n * Sets up the event listener for search input changes.\n * @param {string} label String label 'Chapter' in the current language.\n */\nexport function init(label) {\n chapterLabel = label;\n const inputElement = document.getElementById('bs-search-input');\n inputElement.addEventListener('input', handleSearchInputChange);\n}\n\n/**\n * Function to handle search term input event\n * @param {*} event The input event catched by the listener.\n */\nfunction handleSearchInputChange(event) {\n const searchTerm = event.target.value;\n const searchTermLabel = document.getElementById('bs-search-term-label').value;\n const courseContent = JSON.parse(atob(document.getElementById('bs-json-course-content').value));\n const contextLength = 5;\n\n let searchResults = [];\n // We search the content if we have a search term.\n if (searchTerm) {\n searchResults = getSearchResults(courseContent, searchTerm, contextLength);\n }\n\n // Update the inner HTML of the element with ID 'bs-search-term' to display the current search Term.\n document.getElementById(\"bs-search-term\").innerHTML = searchTermLabel + searchTerm;\n\n // Update the inner HTML of the element with ID 'bs-content' to display the results.\n document.getElementById(\"bs-content\").innerHTML = getResultsUI(searchResults);\n}\n\n\n/**\n * Generates an HTML string to display search results for PDFs and their chapters.\n * @param {Object} searchResults - An object where keys are PDF names and values are objects of chapters.\n * @returns {string} An HTML string with headings for each PDF name and an unordered list of chapters, each with link and context.\n */\nfunction getResultsUI(searchResults) {\n // Initialize an empty string to build the HTML display\n let display = '';\n\n // Iterate over each PDF name in the search results\n for (var pdfName in searchResults) {\n // Add the PDF name as a heading\n display += '

' + pdfName + '

';\n // Start an unordered list for the chapters\n display += '
    ';\n // Iterate over each chapter in the current PDF\n for (var chapter in searchResults[pdfName]) {\n // Add each chapter as a list item with a link and context\n display += '
  • ' +\n '' +\n chapterLabel + '-' + chapter +\n ': ' + searchResults[pdfName][chapter].context +\n '
  • ';\n }\n // Close the unordered list\n display += '
';\n }\n\n return display;\n}\n\n\n/**\n * Processes an array of content sections to extract and format search results based on a search term and context length.\n * @param {Array} courseContent Array of content sections, where each section is an object\n * with keys 'content', 'filename', and 'page'.\n * @param {string} searchTerm The term to search for within the content sections.\n * @param {number} contextLength The number of words to include before and after each occurrence of the search term.\n * @return {Object} The search results, with filenames as keys and sections as values.\n * [filename: {page: {section}}]\n */\nfunction getSearchResults(courseContent, searchTerm, contextLength) {\n const results = {};\n\n courseContent.forEach(section => {\n // Get any search results with context from the section.\n const context = getSectionSearchResultContext(section.content, searchTerm, contextLength);\n\n // Add result to the section object.\n section.context = context;\n\n // Skip this section if there's no context (no result).\n if (context.length < 1) {\n return;\n }\n\n // Create new file entry in results if it does not exist.\n if (!results.hasOwnProperty(section.filename)) {\n results[section.filename] = {};\n }\n\n // Set chapter entry as section or add section context to existing chapter entry.\n if (!results[section.filename].hasOwnProperty(section.page)) {\n results[section.filename][section.page] = {\n filename: section.filename,\n url: section.url,\n bookUrl: section.bookUrl,\n context: section.context\n };\n } else {\n // Append to existing context if the section already exists.\n results[section.filename][section.page].context += section.context;\n }\n });\n\n return results;\n}\n\n\n/**\n * Get a combined string of any found search term occurrences in the content with the surrounding words as context.\n * @param {string} content The text content to search in.\n * @param {string} searchTerm The term to search for in this content.\n * @param {number} contextLength The number of words on each side surrounding the found occurrence to be returned as context.\n * @return {string} Text snippets for each term occurrence with their context, combined as one.\n */\nfunction getSectionSearchResultContext(content, searchTerm, contextLength) {\n const searchContent = content.toLowerCase();\n searchTerm = searchTerm.toLowerCase();\n\n // Check if the search term is present in the content.\n if (searchContent.indexOf(searchTerm) === -1) {\n return \"\";\n }\n\n // Get the text indexes of the term occurrences. Array of objects with 'start' and 'end' properties.\n const occurrenceIndexes = findOccurrences(searchContent, searchTerm);\n\n // Get the text as words and word starting indexes.\n const [words, wordIndexes] = splitTextIntoWords(content);\n\n // Get the word number positions of the context we want to return. Objects with 'start' and 'end' properties.\n const contextPositions = getContextPositions(occurrenceIndexes, wordIndexes, contextLength);\n\n // Get the combined string context.\n const context = getContext(words, contextPositions);\n\n return context;\n}\n\n\n/**\n * Searches for occurrences of a term in a given text and returns an array of occurrence objects.\n * Each occurrence object contains the start index (position in text) and the end index.\n * @param {string} text The text in which to search for the term.\n * @param {string} term The term to search for within the text.\n * @return {Array} An array of objects, each with 'start' and 'end' properties.\n */\nfunction findOccurrences(text, term) {\n const occurrences = [];\n const termLength = term.length;\n\n // Use indexOf to find the occurrences of the term in the text.\n let offset = 0;\n let index;\n\n while ((index = text.indexOf(term, offset)) !== -1) {\n const occurrence = {\n start: index,\n end: index + termLength\n };\n occurrences.push(occurrence);\n // Update the offset to search for the next occurrence.\n offset = index + 1;\n }\n\n return occurrences;\n}\n\n\n/**\n * Splits the text into words and returns an array of word strings and an array of word starting indexes.\n * @param {string} text The original text.\n * @return {Array} A pair of arrays [array of string words, array of word starting indexes].\n */\nfunction splitTextIntoWords(text) {\n const words = [];\n const wordIndexes = [];\n\n const regex = /\\S+/g; // Matches any non-whitespace sequence.\n\n let match;\n while ((match = regex.exec(text)) !== null) {\n words.push(match[0]); // Gather the word string.\n wordIndexes.push(match.index); // Gather the word starting index.\n }\n\n return [words, wordIndexes];\n}\n\n\n/**\n * Returns an array of positional data for each occurrence, including starting word number and ending word number.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (last word number of context or null if at end of text) properties.\n */\nfunction getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength) {\n const results = [];\n let currentOccurrenceIndex = 0;\n\n // Iterate through each word index\n for (let wordNumber = 0; wordNumber < wordIndexes.length; wordNumber++) {\n // If there are no more occurrences to check, exit the loop\n if (currentOccurrenceIndex >= occurrences.length) {\n break;\n }\n\n // Check if this is the last word\n if (wordNumber + 1 >= wordIndexes.length) {\n const start = Math.max(0, wordNumber - contextLength);\n const length = null;\n results.push({\n start: start,\n end: length\n });\n continue;\n }\n\n // The current occurrence to check against\n const currentOccurrence = occurrences[currentOccurrenceIndex];\n\n // If this word is not (yet) part of the context\n if (wordIndexes[wordNumber + 1] <= currentOccurrence.start) {\n continue;\n }\n\n // This word begins an occurrence\n const start = Math.max(0, wordNumber - contextLength);\n const end = getContextEnd(wordIndexes, wordNumber, currentOccurrence.end, contextLength);\n\n const position = {\n start: start,\n end: end\n };\n\n results.push(position);\n\n currentOccurrenceIndex++;\n }\n\n return results;\n}\n\n\n/**\n * Returns the number of the last word still in the context. Returns null if the context is the rest of all words.\n * @param {Array} wordIndexes An array that has the text starting index for each word.\n * @param {number} startNumber The word number where the occurrence starts.\n * @param {number} endIndex The text index where the occurrence ends.\n * @param {number} contextLength The amount of words that get returned on each side of the occurrence as context.\n * @return {?number} The word number of the last word in the context or null if it ends with the text.\n */\nfunction getContextEnd(wordIndexes, startNumber, endIndex, contextLength) {\n for (let i = startNumber; i < wordIndexes.length; i++) {\n // Check if the context reaches the last word\n if (i + contextLength + 1 >= wordIndexes.length) {\n return null;\n }\n\n // Check if the occurrence is part of the next word\n if (wordIndexes[i + 1] <= endIndex) {\n continue;\n }\n\n // Calculate the last word number in context\n const lastWordInContext = i + contextLength;\n\n return lastWordInContext;\n }\n return null;\n}\n\n\n/**\n * Returns an array of positional data for each occurrence set, including starting word number, ending word number, and word count.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction getContextPositions(occurrences, wordIndexes, contextLength) {\n let occurrenceContextPositions = getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength);\n occurrenceContextPositions = mergeOccurrenceContextPositions(occurrenceContextPositions);\n return occurrenceContextPositions;\n}\n\n\n/**\n * Merge overlapping occurrence positions together.\n * @param {Array} contextPositions Array of position objects with 'start' (first word in context)\n * and 'end' (last word in context or null if at end of text) properties.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction mergeOccurrenceContextPositions(contextPositions) {\n const results = [];\n\n for (let i = 0; i < contextPositions.length; i++) {\n // First position.\n let position = contextPositions[i];\n let start = position.start;\n let end = position.end;\n\n // Further positions.\n while (\n i + 1 < contextPositions.length && // Check if there is a next position.\n contextPositions[i].end && // Check if 'end' is null. We can then ignore all upcoming positions.\n contextPositions[i].end >= contextPositions[i + 1].start // Check if this and the next positions overlap.\n ) {\n end = contextPositions[i + 1].end; // The positions overlap so the end gets set to ht next positions end.\n i++;\n }\n\n const mergedPosition = {\n start: start,\n end: end !== null ? end + 1 : undefined // If end is not null we set end to the next element.\n };\n\n results.push(mergedPosition);\n\n if (!end) { // We can ignore all later positions as we already are at the end of possible context.\n break;\n }\n }\n\n return results;\n}\n\n\n/**\n * Based on given context positions, return a combined string from a list of words.\n * @param {Array} words List of all words.\n * @param {Array} contextPositions An array of occurrence position objects, each with 'start' (first word number of context),\n * and 'end' (first word number outside the context or undefinded if at end of text)\n * @return {string} A combined string of all given positions.\n */\nfunction getContext(words, contextPositions) {\n let context = \"... \";\n\n contextPositions.forEach(position => {\n const subcontextWords = words.slice(position.start, position.end);\n context += subcontextWords.join(\" \");\n context += \" ... \";\n });\n\n return context;\n}"],"names":["label","chapterLabel","document","getElementById","addEventListener","handleSearchInputChange","event","searchTerm","target","value","searchTermLabel","courseContent","JSON","parse","atob","searchResults","contextLength","results","forEach","section","context","content","searchContent","toLowerCase","indexOf","occurrenceIndexes","text","term","occurrences","termLength","length","index","offset","occurrence","start","end","push","findOccurrences","words","wordIndexes","regex","match","exec","splitTextIntoWords","contextPositions","occurrenceContextPositions","currentOccurrenceIndex","wordNumber","Math","max","currentOccurrence","position","getContextEnd","getEachOccurrenceContextPosition","i","mergedPosition","undefined","mergeOccurrenceContextPositions","getContextPositions","subcontextWords","slice","join","getContext","getSectionSearchResultContext","hasOwnProperty","filename","page","url","bookUrl","getSearchResults","innerHTML","display","pdfName","chapter","bookurl","getResultsUI","startNumber","endIndex"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/display.js b/amd/src/display.js new file mode 100644 index 0000000..3a0d0b1 --- /dev/null +++ b/amd/src/display.js @@ -0,0 +1,77 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Block core and UI + * + * @module block_booksearch/display + * @copyright 2024 University of Stuttgart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { render } from 'core/templates'; + +/** + * Processes and Displays the search results inside the given element. + * @param {HTMLElement} element The elemtent to display the results inside. + * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]]. + * @param {String} chapterLabel The label meaning "Chapter" in the active language. + */ +export function displayResults(element, searchResults, chapterLabel) { + const data = preprocessSearchResults(searchResults, chapterLabel); + render('block_booksearch/display', data).then((html) => { + element.innerHTML = html; + }).catch(ex => { + window.console.error('Template rendering failed: ', ex); + }); +} + + +/** + * This function processes the searchResults, so they are usable for the template. + * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]]. + * @param {String} chapterLabel The label meaning "Chapter" in the active language. + * @returns Array of search results processed to be perfectly used by the template. + */ +function preprocessSearchResults(searchResults, chapterLabel) { + const data = []; + + for (const pdfName in searchResults) { + if (!searchResults.hasOwnProperty(pdfName)) { + continue; + } + const chapters = []; + + for (const chapter in searchResults[pdfName]) { + if (!searchResults[pdfName].hasOwnProperty(chapter)) { + continue; + } + chapters.push({ + chapter: chapter, + bookurl: searchResults[pdfName][chapter].bookurl, + context: searchResults[pdfName][chapter].context, + chapterLabel: chapterLabel + }); + + } + + data.push({ + filename: pdfName, + chapters: chapters + }); + + } + + return { data: data }; +} \ No newline at end of file diff --git a/amd/src/search_and_display.js b/amd/src/search.js similarity index 89% rename from amd/src/search_and_display.js rename to amd/src/search.js index 79657a2..ba0c2f2 100644 --- a/amd/src/search_and_display.js +++ b/amd/src/search.js @@ -16,10 +16,11 @@ /** * Block core and UI * - * @module block_booksearch/search_and_display + * @module block_booksearch/search * @copyright 2024 University of Stuttgart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import { displayResults } from 'block_booksearch/display'; /** * String label 'Chapter' in the current language. @@ -56,39 +57,7 @@ function handleSearchInputChange(event) { document.getElementById("bs-search-term").innerHTML = searchTermLabel + searchTerm; // Update the inner HTML of the element with ID 'bs-content' to display the results. - document.getElementById("bs-content").innerHTML = getResultsUI(searchResults); -} - - -/** - * Generates an HTML string to display search results for PDFs and their chapters. - * @param {Object} searchResults - An object where keys are PDF names and values are objects of chapters. - * @returns {string} An HTML string with headings for each PDF name and an unordered list of chapters, each with link and context. - */ -function getResultsUI(searchResults) { - // Initialize an empty string to build the HTML display - let display = ''; - - // Iterate over each PDF name in the search results - for (var pdfName in searchResults) { - // Add the PDF name as a heading - display += '

' + pdfName + '

'; - // Start an unordered list for the chapters - display += '
    '; - // Iterate over each chapter in the current PDF - for (var chapter in searchResults[pdfName]) { - // Add each chapter as a list item with a link and context - display += '
  • ' + - '' + - chapterLabel + '-' + chapter + - ': ' + searchResults[pdfName][chapter].context + - '
  • '; - } - // Close the unordered list - display += '
'; - } - - return display; + displayResults(document.getElementById("bs-content"), searchResults, chapterLabel); } @@ -124,9 +93,7 @@ function getSearchResults(courseContent, searchTerm, contextLength) { // Set chapter entry as section or add section context to existing chapter entry. if (!results[section.filename].hasOwnProperty(section.page)) { results[section.filename][section.page] = { - filename: section.filename, - url: section.url, - bookUrl: section.bookUrl, + bookurl: section.bookurl, context: section.context }; } else { diff --git a/block_booksearch.php b/block_booksearch.php index 6eaa5d3..a75e940 100644 --- a/block_booksearch.php +++ b/block_booksearch.php @@ -162,7 +162,7 @@ private function add_search_and_results_ui(string $text, string $footer): array } // Display the search input and results. - $text .= $OUTPUT->render_from_template('block_booksearch/search_and_display', [ + $text .= $OUTPUT->render_from_template('block_booksearch/search', [ 'search_term_placeholder' => get_string('search', get_class($this)), 'search_term_label' => get_string('search_term', get_class($this)), 'chapter_label' => get_string('chapter', get_class($this)), diff --git a/templates/display.mustache b/templates/display.mustache new file mode 100644 index 0000000..946d4e1 --- /dev/null +++ b/templates/display.mustache @@ -0,0 +1,78 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . + + Template to display search input bar and it's search results. + @package block_booksearch + @copyright 2024 University of Stuttgart + @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +}} +{{! + @template block_booksearch/display + + Display Search Results + + Example context (json): + { + "data": [ + { + "filename": "filename1", + "chapters": [ + { + "chapter": "1", + "bookurl": "https://localhost/1/1/1", + "context": "text", + "chapterLabel": "Chapter" + }, + { + "chapter": "2", + "bookurl": "https://localhost/1/1/2", + "context": "Hello World", + "chapterLabel": "Chapter" + } + ] + }, + { + "filename": "filename2", + "chapters": [ + { + "chapter": "1", + "bookurl": "https://localhost/1/2/1", + "context": "text two", + "chapterLabel": "Chapter" + }, + { + "chapter": "2", + "bookurl": "https://localhost/1/2/2", + "context": "Hello World two", + "chapterLabel": "Chapter" + } + ] + } + ] + } +}} +{{#data}} +

{{filename}}

+ +{{/data}} diff --git a/templates/search_and_display.mustache b/templates/search.mustache similarity index 92% rename from templates/search_and_display.mustache rename to templates/search.mustache index 4ecb3fd..a5ec6cb 100644 --- a/templates/search_and_display.mustache +++ b/templates/search.mustache @@ -20,7 +20,7 @@ @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later }} {{! - @template block_booksearch/search_and_display + @template block_booksearch/search Complex Lecture Search @@ -58,6 +58,6 @@ {{#js}} - require(['block_booksearch/search_and_display'], (module) => module.init("{{ chapter_label }}")); + require(['block_booksearch/search'], (module) => module.init("{{ chapter_label }}")); {{/js}} diff --git a/version.php b/version.php index 707a11b..36262c3 100644 --- a/version.php +++ b/version.php @@ -23,7 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024081200; // The current plugin version (Date: YYYYMMDDHH). +$plugin->version = 2024081204; // The current plugin version (Date: YYYYMMDDHH). $plugin->requires = 2020061510; // Requires this Moodle version. $plugin->component = 'block_booksearch'; // Full name of the plugin (used for diagnostics). $plugin->release = '1.1.3'; From fc7b3a2600ec67943b056e03e8be0ad2818cbd6d Mon Sep 17 00:00:00 2001 From: st143971 Date: Mon, 12 Aug 2024 15:44:14 +0200 Subject: [PATCH 2/2] Fixing GitHub Action Warnings & Errors --- .gitignore | 2 +- amd/build/display.min.js.map | 2 +- amd/build/search.min.js.map | 2 +- amd/src/display.js | 9 +++++++-- amd/src/search.js | 4 +++- version.php | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 30df0b1..bed07ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store .git/ -/amd/Makefile +/amd/HowToGrunt.md /lang/de Changelog.md \ No newline at end of file diff --git a/amd/build/display.min.js.map b/amd/build/display.min.js.map index 7370a48..0388670 100644 --- a/amd/build/display.min.js.map +++ b/amd/build/display.min.js.map @@ -1 +1 @@ -{"version":3,"file":"display.min.js","sources":["../src/display.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/display\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport { render } from 'core/templates';\n\n/**\n * Processes and Displays the search results inside the given element.\n * @param {HTMLElement} element The elemtent to display the results inside.\n * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]].\n * @param {String} chapterLabel The label meaning \"Chapter\" in the active language.\n */\nexport function displayResults(element, searchResults, chapterLabel) {\n const data = preprocessSearchResults(searchResults, chapterLabel);\n render('block_booksearch/display', data).then((html) => {\n element.innerHTML = html;\n }).catch(ex => {\n window.console.error('Template rendering failed: ', ex);\n });\n}\n\n\n/**\n * This function processes the searchResults, so they are usable for the template.\n * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]].\n * @param {String} chapterLabel The label meaning \"Chapter\" in the active language.\n * @returns Array of search results processed to be perfectly used by the template.\n */\nfunction preprocessSearchResults(searchResults, chapterLabel) {\n const data = [];\n\n for (const pdfName in searchResults) {\n if (!searchResults.hasOwnProperty(pdfName)) {\n continue;\n }\n const chapters = [];\n\n for (const chapter in searchResults[pdfName]) {\n if (!searchResults[pdfName].hasOwnProperty(chapter)) {\n continue;\n }\n chapters.push({\n chapter: chapter,\n bookurl: searchResults[pdfName][chapter].bookurl,\n context: searchResults[pdfName][chapter].context,\n chapterLabel: chapterLabel\n });\n\n }\n\n data.push({\n filename: pdfName,\n chapters: chapters\n });\n\n }\n\n return { data: data };\n}"],"names":["element","searchResults","chapterLabel","data","pdfName","hasOwnProperty","chapters","chapter","push","bookurl","context","filename","preprocessSearchResults","then","html","innerHTML","catch","ex","window","console","error"],"mappings":";;;;;;;;SA8B+BA,QAASC,cAAeC,oBAC7CC,cAeuBF,cAAeC,oBACtCC,KAAO,OAER,MAAMC,WAAWH,cAAe,KAC5BA,cAAcI,eAAeD,wBAG5BE,SAAW,OAEZ,MAAMC,WAAWN,cAAcG,SAC3BH,cAAcG,SAASC,eAAeE,UAG3CD,SAASE,KAAK,CACVD,QAASA,QACTE,QAASR,cAAcG,SAASG,SAASE,QACzCC,QAAST,cAAcG,SAASG,SAASG,QACzCR,aAAcA,eAKtBC,KAAKK,KAAK,CACNG,SAAUP,QACVE,SAAUA,iBAKX,CAAEH,KAAMA,MA5CFS,CAAwBX,cAAeC,oCAC7C,2BAA4BC,MAAMU,MAAMC,OAC3Cd,QAAQe,UAAYD,QACrBE,OAAMC,KACLC,OAAOC,QAAQC,MAAM,8BAA+BH"} \ No newline at end of file +{"version":3,"file":"display.min.js","sources":["../src/display.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/display\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {\n render\n} from 'core/templates';\n\n/**\n * Processes and Displays the search results inside the given element.\n * @param {HTMLElement} element The elemtent to display the results inside.\n * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]].\n * @param {String} chapterLabel The label meaning \"Chapter\" in the active language.\n */\nexport function displayResults(element, searchResults, chapterLabel) {\n const data = preprocessSearchResults(searchResults, chapterLabel);\n render('block_booksearch/display', data).then((html) => {\n element.innerHTML = html;\n return undefined;\n }).catch(ex => {\n window.console.error('Template rendering failed: ', ex);\n });\n}\n\n\n/**\n * This function processes the searchResults, so they are usable for the template.\n * @param {Object} searchResults The search results to display as [filename[chapter{bookurl, context}]].\n * @param {String} chapterLabel The label meaning \"Chapter\" in the active language.\n * @returns Array of search results processed to be perfectly used by the template.\n */\nfunction preprocessSearchResults(searchResults, chapterLabel) {\n const data = [];\n\n for (const pdfName in searchResults) {\n if (!searchResults.hasOwnProperty(pdfName)) {\n continue;\n }\n const chapters = [];\n\n for (const chapter in searchResults[pdfName]) {\n if (!searchResults[pdfName].hasOwnProperty(chapter)) {\n continue;\n }\n chapters.push({\n chapter: chapter,\n bookurl: searchResults[pdfName][chapter].bookurl,\n context: searchResults[pdfName][chapter].context,\n chapterLabel: chapterLabel\n });\n\n }\n\n data.push({\n filename: pdfName,\n chapters: chapters\n });\n\n }\n\n return {\n data: data\n };\n}"],"names":["element","searchResults","chapterLabel","data","pdfName","hasOwnProperty","chapters","chapter","push","bookurl","context","filename","preprocessSearchResults","then","html","innerHTML","catch","ex","window","console","error"],"mappings":";;;;;;;;SAgC+BA,QAASC,cAAeC,oBAC7CC,cAgBuBF,cAAeC,oBACtCC,KAAO,OAER,MAAMC,WAAWH,cAAe,KAC5BA,cAAcI,eAAeD,wBAG5BE,SAAW,OAEZ,MAAMC,WAAWN,cAAcG,SAC3BH,cAAcG,SAASC,eAAeE,UAG3CD,SAASE,KAAK,CACVD,QAASA,QACTE,QAASR,cAAcG,SAASG,SAASE,QACzCC,QAAST,cAAcG,SAASG,SAASG,QACzCR,aAAcA,eAKtBC,KAAKK,KAAK,CACNG,SAAUP,QACVE,SAAUA,iBAKX,CACHH,KAAMA,MA9CGS,CAAwBX,cAAeC,oCAC7C,2BAA4BC,MAAMU,MAAMC,OAC3Cd,QAAQe,UAAYD,QAErBE,OAAMC,KACLC,OAAOC,QAAQC,MAAM,8BAA+BH"} \ No newline at end of file diff --git a/amd/build/search.min.js.map b/amd/build/search.min.js.map index 50feb53..10aec6d 100644 --- a/amd/build/search.min.js.map +++ b/amd/build/search.min.js.map @@ -1 +1 @@ -{"version":3,"file":"search.min.js","sources":["../src/search.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/search\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport { displayResults } from 'block_booksearch/display';\n\n/**\n * String label 'Chapter' in the current language.\n */\nlet chapterLabel = 'Chapter';\n\n/**\n * Sets up the event listener for search input changes.\n * @param {string} label String label 'Chapter' in the current language.\n */\nexport function init(label) {\n chapterLabel = label;\n const inputElement = document.getElementById('bs-search-input');\n inputElement.addEventListener('input', handleSearchInputChange);\n}\n\n/**\n * Function to handle search term input event\n * @param {*} event The input event catched by the listener.\n */\nfunction handleSearchInputChange(event) {\n const searchTerm = event.target.value;\n const searchTermLabel = document.getElementById('bs-search-term-label').value;\n const courseContent = JSON.parse(atob(document.getElementById('bs-json-course-content').value));\n const contextLength = 5;\n\n let searchResults = [];\n // We search the content if we have a search term.\n if (searchTerm) {\n searchResults = getSearchResults(courseContent, searchTerm, contextLength);\n }\n\n // Update the inner HTML of the element with ID 'bs-search-term' to display the current search Term.\n document.getElementById(\"bs-search-term\").innerHTML = searchTermLabel + searchTerm;\n\n // Update the inner HTML of the element with ID 'bs-content' to display the results.\n displayResults(document.getElementById(\"bs-content\"), searchResults, chapterLabel);\n}\n\n\n/**\n * Processes an array of content sections to extract and format search results based on a search term and context length.\n * @param {Array} courseContent Array of content sections, where each section is an object\n * with keys 'content', 'filename', and 'page'.\n * @param {string} searchTerm The term to search for within the content sections.\n * @param {number} contextLength The number of words to include before and after each occurrence of the search term.\n * @return {Object} The search results, with filenames as keys and sections as values.\n * [filename: {page: {section}}]\n */\nfunction getSearchResults(courseContent, searchTerm, contextLength) {\n const results = {};\n\n courseContent.forEach(section => {\n // Get any search results with context from the section.\n const context = getSectionSearchResultContext(section.content, searchTerm, contextLength);\n\n // Add result to the section object.\n section.context = context;\n\n // Skip this section if there's no context (no result).\n if (context.length < 1) {\n return;\n }\n\n // Create new file entry in results if it does not exist.\n if (!results.hasOwnProperty(section.filename)) {\n results[section.filename] = {};\n }\n\n // Set chapter entry as section or add section context to existing chapter entry.\n if (!results[section.filename].hasOwnProperty(section.page)) {\n results[section.filename][section.page] = {\n bookurl: section.bookurl,\n context: section.context\n };\n } else {\n // Append to existing context if the section already exists.\n results[section.filename][section.page].context += section.context;\n }\n });\n\n return results;\n}\n\n\n/**\n * Get a combined string of any found search term occurrences in the content with the surrounding words as context.\n * @param {string} content The text content to search in.\n * @param {string} searchTerm The term to search for in this content.\n * @param {number} contextLength The number of words on each side surrounding the found occurrence to be returned as context.\n * @return {string} Text snippets for each term occurrence with their context, combined as one.\n */\nfunction getSectionSearchResultContext(content, searchTerm, contextLength) {\n const searchContent = content.toLowerCase();\n searchTerm = searchTerm.toLowerCase();\n\n // Check if the search term is present in the content.\n if (searchContent.indexOf(searchTerm) === -1) {\n return \"\";\n }\n\n // Get the text indexes of the term occurrences. Array of objects with 'start' and 'end' properties.\n const occurrenceIndexes = findOccurrences(searchContent, searchTerm);\n\n // Get the text as words and word starting indexes.\n const [words, wordIndexes] = splitTextIntoWords(content);\n\n // Get the word number positions of the context we want to return. Objects with 'start' and 'end' properties.\n const contextPositions = getContextPositions(occurrenceIndexes, wordIndexes, contextLength);\n\n // Get the combined string context.\n const context = getContext(words, contextPositions);\n\n return context;\n}\n\n\n/**\n * Searches for occurrences of a term in a given text and returns an array of occurrence objects.\n * Each occurrence object contains the start index (position in text) and the end index.\n * @param {string} text The text in which to search for the term.\n * @param {string} term The term to search for within the text.\n * @return {Array} An array of objects, each with 'start' and 'end' properties.\n */\nfunction findOccurrences(text, term) {\n const occurrences = [];\n const termLength = term.length;\n\n // Use indexOf to find the occurrences of the term in the text.\n let offset = 0;\n let index;\n\n while ((index = text.indexOf(term, offset)) !== -1) {\n const occurrence = {\n start: index,\n end: index + termLength\n };\n occurrences.push(occurrence);\n // Update the offset to search for the next occurrence.\n offset = index + 1;\n }\n\n return occurrences;\n}\n\n\n/**\n * Splits the text into words and returns an array of word strings and an array of word starting indexes.\n * @param {string} text The original text.\n * @return {Array} A pair of arrays [array of string words, array of word starting indexes].\n */\nfunction splitTextIntoWords(text) {\n const words = [];\n const wordIndexes = [];\n\n const regex = /\\S+/g; // Matches any non-whitespace sequence.\n\n let match;\n while ((match = regex.exec(text)) !== null) {\n words.push(match[0]); // Gather the word string.\n wordIndexes.push(match.index); // Gather the word starting index.\n }\n\n return [words, wordIndexes];\n}\n\n\n/**\n * Returns an array of positional data for each occurrence, including starting word number and ending word number.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (last word number of context or null if at end of text) properties.\n */\nfunction getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength) {\n const results = [];\n let currentOccurrenceIndex = 0;\n\n // Iterate through each word index\n for (let wordNumber = 0; wordNumber < wordIndexes.length; wordNumber++) {\n // If there are no more occurrences to check, exit the loop\n if (currentOccurrenceIndex >= occurrences.length) {\n break;\n }\n\n // Check if this is the last word\n if (wordNumber + 1 >= wordIndexes.length) {\n const start = Math.max(0, wordNumber - contextLength);\n const length = null;\n results.push({\n start: start,\n end: length\n });\n continue;\n }\n\n // The current occurrence to check against\n const currentOccurrence = occurrences[currentOccurrenceIndex];\n\n // If this word is not (yet) part of the context\n if (wordIndexes[wordNumber + 1] <= currentOccurrence.start) {\n continue;\n }\n\n // This word begins an occurrence\n const start = Math.max(0, wordNumber - contextLength);\n const end = getContextEnd(wordIndexes, wordNumber, currentOccurrence.end, contextLength);\n\n const position = {\n start: start,\n end: end\n };\n\n results.push(position);\n\n currentOccurrenceIndex++;\n }\n\n return results;\n}\n\n\n/**\n * Returns the number of the last word still in the context. Returns null if the context is the rest of all words.\n * @param {Array} wordIndexes An array that has the text starting index for each word.\n * @param {number} startNumber The word number where the occurrence starts.\n * @param {number} endIndex The text index where the occurrence ends.\n * @param {number} contextLength The amount of words that get returned on each side of the occurrence as context.\n * @return {?number} The word number of the last word in the context or null if it ends with the text.\n */\nfunction getContextEnd(wordIndexes, startNumber, endIndex, contextLength) {\n for (let i = startNumber; i < wordIndexes.length; i++) {\n // Check if the context reaches the last word\n if (i + contextLength + 1 >= wordIndexes.length) {\n return null;\n }\n\n // Check if the occurrence is part of the next word\n if (wordIndexes[i + 1] <= endIndex) {\n continue;\n }\n\n // Calculate the last word number in context\n const lastWordInContext = i + contextLength;\n\n return lastWordInContext;\n }\n return null;\n}\n\n\n/**\n * Returns an array of positional data for each occurrence set, including starting word number, ending word number, and word count.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction getContextPositions(occurrences, wordIndexes, contextLength) {\n let occurrenceContextPositions = getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength);\n occurrenceContextPositions = mergeOccurrenceContextPositions(occurrenceContextPositions);\n return occurrenceContextPositions;\n}\n\n\n/**\n * Merge overlapping occurrence positions together.\n * @param {Array} contextPositions Array of position objects with 'start' (first word in context)\n * and 'end' (last word in context or null if at end of text) properties.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction mergeOccurrenceContextPositions(contextPositions) {\n const results = [];\n\n for (let i = 0; i < contextPositions.length; i++) {\n // First position.\n let position = contextPositions[i];\n let start = position.start;\n let end = position.end;\n\n // Further positions.\n while (\n i + 1 < contextPositions.length && // Check if there is a next position.\n contextPositions[i].end && // Check if 'end' is null. We can then ignore all upcoming positions.\n contextPositions[i].end >= contextPositions[i + 1].start // Check if this and the next positions overlap.\n ) {\n end = contextPositions[i + 1].end; // The positions overlap so the end gets set to ht next positions end.\n i++;\n }\n\n const mergedPosition = {\n start: start,\n end: end !== null ? end + 1 : undefined // If end is not null we set end to the next element.\n };\n\n results.push(mergedPosition);\n\n if (!end) { // We can ignore all later positions as we already are at the end of possible context.\n break;\n }\n }\n\n return results;\n}\n\n\n/**\n * Based on given context positions, return a combined string from a list of words.\n * @param {Array} words List of all words.\n * @param {Array} contextPositions An array of occurrence position objects, each with 'start' (first word number of context),\n * and 'end' (first word number outside the context or undefinded if at end of text)\n * @return {string} A combined string of all given positions.\n */\nfunction getContext(words, contextPositions) {\n let context = \"... \";\n\n contextPositions.forEach(position => {\n const subcontextWords = words.slice(position.start, position.end);\n context += subcontextWords.join(\" \");\n context += \" ... \";\n });\n\n return context;\n}"],"names":["label","chapterLabel","document","getElementById","addEventListener","handleSearchInputChange","event","searchTerm","target","value","searchTermLabel","courseContent","JSON","parse","atob","searchResults","contextLength","results","forEach","section","context","content","searchContent","toLowerCase","indexOf","occurrenceIndexes","text","term","occurrences","termLength","length","index","offset","occurrence","start","end","push","findOccurrences","words","wordIndexes","regex","match","exec","splitTextIntoWords","contextPositions","occurrenceContextPositions","currentOccurrenceIndex","wordNumber","Math","max","currentOccurrence","position","getContextEnd","getEachOccurrenceContextPosition","i","mergedPosition","undefined","mergeOccurrenceContextPositions","getContextPositions","subcontextWords","slice","join","getContext","getSectionSearchResultContext","hasOwnProperty","filename","page","bookurl","getSearchResults","innerHTML","startNumber","endIndex"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"search.min.js","sources":["../src/search.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Block core and UI\n *\n * @module block_booksearch/search\n * @copyright 2024 University of Stuttgart \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {\n displayResults\n} from 'block_booksearch/display';\n\n/**\n * String label 'Chapter' in the current language.\n */\nlet chapterLabel = 'Chapter';\n\n/**\n * Sets up the event listener for search input changes.\n * @param {string} label String label 'Chapter' in the current language.\n */\nexport function init(label) {\n chapterLabel = label;\n const inputElement = document.getElementById('bs-search-input');\n inputElement.addEventListener('input', handleSearchInputChange);\n}\n\n/**\n * Function to handle search term input event\n * @param {*} event The input event catched by the listener.\n */\nfunction handleSearchInputChange(event) {\n const searchTerm = event.target.value;\n const searchTermLabel = document.getElementById('bs-search-term-label').value;\n const courseContent = JSON.parse(atob(document.getElementById('bs-json-course-content').value));\n const contextLength = 5;\n\n let searchResults = [];\n // We search the content if we have a search term.\n if (searchTerm) {\n searchResults = getSearchResults(courseContent, searchTerm, contextLength);\n }\n\n // Update the inner HTML of the element with ID 'bs-search-term' to display the current search Term.\n document.getElementById(\"bs-search-term\").innerHTML = searchTermLabel + searchTerm;\n\n // Update the inner HTML of the element with ID 'bs-content' to display the results.\n displayResults(document.getElementById(\"bs-content\"), searchResults, chapterLabel);\n}\n\n\n/**\n * Processes an array of content sections to extract and format search results based on a search term and context length.\n * @param {Array} courseContent Array of content sections, where each section is an object\n * with keys 'content', 'filename', and 'page'.\n * @param {string} searchTerm The term to search for within the content sections.\n * @param {number} contextLength The number of words to include before and after each occurrence of the search term.\n * @return {Object} The search results, with filenames as keys and sections as values.\n * [filename: {page: {section}}]\n */\nfunction getSearchResults(courseContent, searchTerm, contextLength) {\n const results = {};\n\n courseContent.forEach(section => {\n // Get any search results with context from the section.\n const context = getSectionSearchResultContext(section.content, searchTerm, contextLength);\n\n // Add result to the section object.\n section.context = context;\n\n // Skip this section if there's no context (no result).\n if (context.length < 1) {\n return;\n }\n\n // Create new file entry in results if it does not exist.\n if (!results.hasOwnProperty(section.filename)) {\n results[section.filename] = {};\n }\n\n // Set chapter entry as section or add section context to existing chapter entry.\n if (!results[section.filename].hasOwnProperty(section.page)) {\n results[section.filename][section.page] = {\n bookurl: section.bookurl,\n context: section.context\n };\n } else {\n // Append to existing context if the section already exists.\n results[section.filename][section.page].context += section.context;\n }\n });\n\n return results;\n}\n\n\n/**\n * Get a combined string of any found search term occurrences in the content with the surrounding words as context.\n * @param {string} content The text content to search in.\n * @param {string} searchTerm The term to search for in this content.\n * @param {number} contextLength The number of words on each side surrounding the found occurrence to be returned as context.\n * @return {string} Text snippets for each term occurrence with their context, combined as one.\n */\nfunction getSectionSearchResultContext(content, searchTerm, contextLength) {\n const searchContent = content.toLowerCase();\n searchTerm = searchTerm.toLowerCase();\n\n // Check if the search term is present in the content.\n if (searchContent.indexOf(searchTerm) === -1) {\n return \"\";\n }\n\n // Get the text indexes of the term occurrences. Array of objects with 'start' and 'end' properties.\n const occurrenceIndexes = findOccurrences(searchContent, searchTerm);\n\n // Get the text as words and word starting indexes.\n const [words, wordIndexes] = splitTextIntoWords(content);\n\n // Get the word number positions of the context we want to return. Objects with 'start' and 'end' properties.\n const contextPositions = getContextPositions(occurrenceIndexes, wordIndexes, contextLength);\n\n // Get the combined string context.\n const context = getContext(words, contextPositions);\n\n return context;\n}\n\n\n/**\n * Searches for occurrences of a term in a given text and returns an array of occurrence objects.\n * Each occurrence object contains the start index (position in text) and the end index.\n * @param {string} text The text in which to search for the term.\n * @param {string} term The term to search for within the text.\n * @return {Array} An array of objects, each with 'start' and 'end' properties.\n */\nfunction findOccurrences(text, term) {\n const occurrences = [];\n const termLength = term.length;\n\n // Use indexOf to find the occurrences of the term in the text.\n let offset = 0;\n let index;\n\n while ((index = text.indexOf(term, offset)) !== -1) {\n const occurrence = {\n start: index,\n end: index + termLength\n };\n occurrences.push(occurrence);\n // Update the offset to search for the next occurrence.\n offset = index + 1;\n }\n\n return occurrences;\n}\n\n\n/**\n * Splits the text into words and returns an array of word strings and an array of word starting indexes.\n * @param {string} text The original text.\n * @return {Array} A pair of arrays [array of string words, array of word starting indexes].\n */\nfunction splitTextIntoWords(text) {\n const words = [];\n const wordIndexes = [];\n\n const regex = /\\S+/g; // Matches any non-whitespace sequence.\n\n let match;\n while ((match = regex.exec(text)) !== null) {\n words.push(match[0]); // Gather the word string.\n wordIndexes.push(match.index); // Gather the word starting index.\n }\n\n return [words, wordIndexes];\n}\n\n\n/**\n * Returns an array of positional data for each occurrence, including starting word number and ending word number.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (last word number of context or null if at end of text) properties.\n */\nfunction getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength) {\n const results = [];\n let currentOccurrenceIndex = 0;\n\n // Iterate through each word index\n for (let wordNumber = 0; wordNumber < wordIndexes.length; wordNumber++) {\n // If there are no more occurrences to check, exit the loop\n if (currentOccurrenceIndex >= occurrences.length) {\n break;\n }\n\n // Check if this is the last word\n if (wordNumber + 1 >= wordIndexes.length) {\n const start = Math.max(0, wordNumber - contextLength);\n const length = null;\n results.push({\n start: start,\n end: length\n });\n continue;\n }\n\n // The current occurrence to check against\n const currentOccurrence = occurrences[currentOccurrenceIndex];\n\n // If this word is not (yet) part of the context\n if (wordIndexes[wordNumber + 1] <= currentOccurrence.start) {\n continue;\n }\n\n // This word begins an occurrence\n const start = Math.max(0, wordNumber - contextLength);\n const end = getContextEnd(wordIndexes, wordNumber, currentOccurrence.end, contextLength);\n\n const position = {\n start: start,\n end: end\n };\n\n results.push(position);\n\n currentOccurrenceIndex++;\n }\n\n return results;\n}\n\n\n/**\n * Returns the number of the last word still in the context. Returns null if the context is the rest of all words.\n * @param {Array} wordIndexes An array that has the text starting index for each word.\n * @param {number} startNumber The word number where the occurrence starts.\n * @param {number} endIndex The text index where the occurrence ends.\n * @param {number} contextLength The amount of words that get returned on each side of the occurrence as context.\n * @return {?number} The word number of the last word in the context or null if it ends with the text.\n */\nfunction getContextEnd(wordIndexes, startNumber, endIndex, contextLength) {\n for (let i = startNumber; i < wordIndexes.length; i++) {\n // Check if the context reaches the last word\n if (i + contextLength + 1 >= wordIndexes.length) {\n return null;\n }\n\n // Check if the occurrence is part of the next word\n if (wordIndexes[i + 1] <= endIndex) {\n continue;\n }\n\n // Calculate the last word number in context\n const lastWordInContext = i + contextLength;\n\n return lastWordInContext;\n }\n return null;\n}\n\n\n/**\n * Returns an array of positional data for each occurrence set, including starting word number, ending word number, and word count.\n * @param {Array} occurrences Array of search term occurrence objects, each with 'start' and 'end' properties.\n * @param {Array} wordIndexes Array of word indexes that indicate the start position of each word in the text.\n * @param {number} contextLength The number of words to include as context on each side of the search term occurrence.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction getContextPositions(occurrences, wordIndexes, contextLength) {\n let occurrenceContextPositions = getEachOccurrenceContextPosition(occurrences, wordIndexes, contextLength);\n occurrenceContextPositions = mergeOccurrenceContextPositions(occurrenceContextPositions);\n return occurrenceContextPositions;\n}\n\n\n/**\n * Merge overlapping occurrence positions together.\n * @param {Array} contextPositions Array of position objects with 'start' (first word in context)\n * and 'end' (last word in context or null if at end of text) properties.\n * @return {Array} An array of occurrence position objects, each with 'start' (first word number of context)\n * and 'end' (first word number outside the context or undefined if at end of text).\n */\nfunction mergeOccurrenceContextPositions(contextPositions) {\n const results = [];\n\n for (let i = 0; i < contextPositions.length; i++) {\n // First position.\n let position = contextPositions[i];\n let start = position.start;\n let end = position.end;\n\n // Further positions.\n while (\n i + 1 < contextPositions.length && // Check if there is a next position.\n contextPositions[i].end && // Check if 'end' is null. We can then ignore all upcoming positions.\n contextPositions[i].end >= contextPositions[i + 1].start // Check if this and the next positions overlap.\n ) {\n end = contextPositions[i + 1].end; // The positions overlap so the end gets set to ht next positions end.\n i++;\n }\n\n const mergedPosition = {\n start: start,\n end: end !== null ? end + 1 : undefined // If end is not null we set end to the next element.\n };\n\n results.push(mergedPosition);\n\n if (!end) { // We can ignore all later positions as we already are at the end of possible context.\n break;\n }\n }\n\n return results;\n}\n\n\n/**\n * Based on given context positions, return a combined string from a list of words.\n * @param {Array} words List of all words.\n * @param {Array} contextPositions An array of occurrence position objects, each with 'start' (first word number of context),\n * and 'end' (first word number outside the context or undefinded if at end of text)\n * @return {string} A combined string of all given positions.\n */\nfunction getContext(words, contextPositions) {\n let context = \"... \";\n\n contextPositions.forEach(position => {\n const subcontextWords = words.slice(position.start, position.end);\n context += subcontextWords.join(\" \");\n context += \" ... \";\n });\n\n return context;\n}"],"names":["label","chapterLabel","document","getElementById","addEventListener","handleSearchInputChange","event","searchTerm","target","value","searchTermLabel","courseContent","JSON","parse","atob","searchResults","contextLength","results","forEach","section","context","content","searchContent","toLowerCase","indexOf","occurrenceIndexes","text","term","occurrences","termLength","length","index","offset","occurrence","start","end","push","findOccurrences","words","wordIndexes","regex","match","exec","splitTextIntoWords","contextPositions","occurrenceContextPositions","currentOccurrenceIndex","wordNumber","Math","max","currentOccurrence","position","getContextEnd","getEachOccurrenceContextPosition","i","mergedPosition","undefined","mergeOccurrenceContextPositions","getContextPositions","subcontextWords","slice","join","getContext","getSectionSearchResultContext","hasOwnProperty","filename","page","bookurl","getSearchResults","innerHTML","startNumber","endIndex"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/display.js b/amd/src/display.js index 3a0d0b1..b3180ed 100644 --- a/amd/src/display.js +++ b/amd/src/display.js @@ -20,7 +20,9 @@ * @copyright 2024 University of Stuttgart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import { render } from 'core/templates'; +import { + render +} from 'core/templates'; /** * Processes and Displays the search results inside the given element. @@ -32,6 +34,7 @@ export function displayResults(element, searchResults, chapterLabel) { const data = preprocessSearchResults(searchResults, chapterLabel); render('block_booksearch/display', data).then((html) => { element.innerHTML = html; + return undefined; }).catch(ex => { window.console.error('Template rendering failed: ', ex); }); @@ -73,5 +76,7 @@ function preprocessSearchResults(searchResults, chapterLabel) { } - return { data: data }; + return { + data: data + }; } \ No newline at end of file diff --git a/amd/src/search.js b/amd/src/search.js index ba0c2f2..6fc2627 100644 --- a/amd/src/search.js +++ b/amd/src/search.js @@ -20,7 +20,9 @@ * @copyright 2024 University of Stuttgart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import { displayResults } from 'block_booksearch/display'; +import { + displayResults +} from 'block_booksearch/display'; /** * String label 'Chapter' in the current language. diff --git a/version.php b/version.php index 36262c3..82a01d0 100644 --- a/version.php +++ b/version.php @@ -23,7 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024081204; // The current plugin version (Date: YYYYMMDDHH). +$plugin->version = 2024081206; // The current plugin version (Date: YYYYMMDDHH). $plugin->requires = 2020061510; // Requires this Moodle version. $plugin->component = 'block_booksearch'; // Full name of the plugin (used for diagnostics). $plugin->release = '1.1.3';