Action
Block level filter (220324)
Update 220324:
- better rendering of tags in information line (now no longer truncated)
- appropriate markdown header for each block is now listed in the information line, except where the draft only has one header in the first line (title)
- other miscellaneous tweaks
ORIGINAL NOTES
Depends on Matt Gemmell’s MGCheckListPrompt action. This won’t work if you don’t have that installed (with apologies to Matt for my own less-than-elegant hackery around his brilliant work…;)
This action queries drafts for a search term…
- if there’s any text selected, that text will be used as a search.
- if no text is selected, you’ll be offered a prompt to enter a search term
- if no text is selected, and the cursor is in a line with a wiki-linked phrase, that wiki-linked phrase is offered as an option for the search— if there’s more than one, each will be offered as a separate option
Once your search term is decided, hits are loaded into an interface (the Matt Gemmell Checklist Prompt), displaying the entirety of the line that the search term is found in for context.
Selecting an option from the list should highlight the search term in the draft.
220131: added 0-9- to the regexes…
Steps
-
script
// PREFERENCES // META: Do you want to set preferences here or on the form itself? Doing it here keeps the form tidy and allows for a wider range of options in search scope. Doing it on the form allows you to change things on the fly. Quotation marks required. // Options: "here", "form". var preferences = "form" // The next setting is used to suppress results from Drafts with a specified tag. Currently works best if you focus on one tag only, although you can enter a list of tags separated by commas if you choose... var ignoreResultsTaggedWith = "filter output" // Don't edit this next line... ;) // if (preferences == "here") { // If you've selected "form", any adjustments you make below will be ignored, except when starting a search from a wikilink or from selected text... // CONTEXT DISPLAY: How would you prefer the list to behave when the term you're searching for is found in the title of a draft? If it would be useful to see the whole draft in this instance, set this variable to true. Otherwise, if you have lots of long drafts, you may wish to keep this set as false, in which case the first non-empty line of the draft is returned. No quotation marks required. // Options: true, false var fullDraftsAsContext = false // SCOPE OF SEARCH: Where do you want your search to be run? Quotation marks required! // Options: "inbox", "archive", "flagged", "trash","all". var searchScope = "inbox" // SORT ORDER: How do you want your search results sorted? Quotation marks required! // Options: "modified", "created" var modOrCreated = "modified" // (Don't change this...) var mocProp = modOrCreated + "At" // }
-
includeAction
name MGCheckListPrompt Library
-
script
// INITIAL PROMPT TO DEFINE FILTER // the way the action is invoked determines whether this prompt is displayed... if (editor.getSelectedText().length > 1) { var searchStr = editor.getSelectedText() } else if (draft.content.startsWith("fromWikiLink:")) { var searchStr = draft.content.replace("fromWikiLink:","") // alert(searchStr) // 20211026: if tag includes an underscore, search both as tag AND key phrase if (searchStr.includes("_")){ var queries = [ searchStr, searchStr.replaceAll("_"," ").replace("#",""), ]; } } else { // no selection, and it wasn't invoked from a wikilink, so let's build the prompt... var searchStr = "" lSel = editor.getSelectedLineRange() sel = editor.getTextInRange(lSel[0],lSel[1]) var p = Prompt.create(); p.title = "Filter"; p.addTextField("searchTerm", "Search for", "", { "wantsFocus": true }); p.addLabel("label","ALTERNATIVELY, FILTER BY:", { "textSize": "caption" }); // options for backlinks... var linkRefs = ["(Linked references for this draft)","(Unlinked references for this draft)"]; // is there a wiki-link in the current line? Let's check, and if so, add to the list of potential filters... var buildArr = []; if (/\B#(\d*[A-Za-z0-9-_]+\w*)\b(?!;)/.test(sel)){ buildArr = [...new Set(buildArr.concat(sel.match(/\B#(\d*[A-Za-z_]+\w*)\b(?!;)/g)))]; } if (/\[\[[\#|\w|\s|@|\.|\?|»|\:|\(|\)|\/|\||\-|\+"]+\]\]/.test(sel)) { buildArr = [...new Set(buildArr.concat(sel.match(/\[\[[\#|\w|\s|@|\.|\?|»|\:|\(|\)|\/|\||\-|\+"]+\]\]/g)))]} var opts = buildArr.concat(linkRefs) p.addSelect("linkSel", "", opts, [], false); // show search settings if enabled in first action step... if (preferences == "form") { p.addSwitch("modOrCreated", "Sort by modified? (off = created)", false); p.addSwitch("context", "Show full draft if search term is in title?", false); p.addSwitch("searchScope", "Search all? (off = search inbox only)", false); } p.addButton("run"); var didSelect = p.show(); // set variables and settings in response to chosen/selected terms... if (p.buttonPressed == "run") { if (preferences == "form") { var fullDraftsAsContext = p.fieldValues["context"] var searchScope = p.fieldValues["searchScope"] ? "all" : "inbox" var modOrCreated = p.fieldValues["modOrCreated"] ? "modified" : "created" var mocProp = modOrCreated + "At" } if (p.fieldValues["searchTerm"].length > 0){ searchStr = p.fieldValues["searchTerm"] } else if (p.fieldValues["linkSel"].toString().startsWith("[[s:")||p.fieldValues["linkSel"].toString().startsWith("[[@:")){ // a syntax defined search should just be a search term... searchStr = p.fieldValues["linkSel"].toString().replace("[[s:","").replace("[[@:","").replace("]]","") } else if (p.fieldValues["linkSel"].toString().includes("(Linked references for this draft)")){ searchStr = p.fieldValues["linkSel"].toString() var queries = [ "[["+ draft.displayTitle +"]]", "[[# "+ draft.displayTitle +"]]", ] } else if (p.fieldValues["linkSel"].toString().includes("(Unlinked references for this draft)")){ var searchStr = draft.displayTitle; } else if (/\B#(\d*[A-Za-z0-9-_]+\w*)\b(?!;)/.test(p.fieldValues["linkSel"].toString())){ var searchStr = p.fieldValues["linkSel"].toString(); } else if (p.fieldValues["linkSel"].length > 0){ // searchStr = p.fieldValues["linkSel"].toString().replace("[[s:","[[") searchStr = p.fieldValues["linkSel"].toString() searchStr = searchStr.split('[[').pop().split(']]')[0].replace("# ","") // searchStr = "\"[[" + searchStr + "\" OR \"[[# " + searchStr + "\"" var queries = [ "[["+ searchStr +"]]", "[[# "+ searchStr +"]]", ]; } else { context.cancel() } } } searchStr == "" ? context.cancel() : searchStr
-
script
// FUNCTIONS // escape characters for regex function escapeRegex(string) { return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } // relative time formatting: https://stackoverflow.com/a/37802747 function timeDifference(previous) { var prefix = "(" + modOrCreated + " " var suffix = ")" var output = "" var current = new Date() var msPerMinute = 60 * 1000; var msPerHour = msPerMinute * 60; var msPerDay = msPerHour * 24; var msPerMonth = msPerDay * 30; var msPerYear = msPerDay * 365; var elapsed = current - previous; if (elapsed < msPerMinute) { if (Math.round(elapsed/1000)==1) { output = 'a second ago'; } else { output = Math.round(elapsed/1000) + ' seconds ago'; } } else if (elapsed < msPerHour) { if (Math.round(elapsed/msPerMinute)==1) { output = 'a minute ago'; } else { output = Math.round(elapsed/msPerMinute) + ' minutes ago'; } } else if (elapsed < msPerDay ) { if (Math.round(elapsed/msPerHour)==1) { output = 'an hour ago'; } else { output = Math.round(elapsed/msPerHour) + ' hours ago'; } } else if (elapsed < msPerMonth) { if (Math.round(elapsed/msPerDay)==1) { output = 'yesterday'; } else { output = '~' + Math.round(elapsed/msPerDay) + ' days ago'; } } else if (elapsed < msPerYear) { if (Math.round(elapsed/msPerMonth)==1) { output = 'last month'; } else { output = '~' + Math.round(elapsed/msPerMonth) + ' months ago'; } } else { if (Math.round(elapsed/msPerYear)==1) { output = 'last year'; } else { output = '~' + Math.round(elapsed/msPerYear) + ' years ago'; } } return prefix + output + suffix } // get indexes for headers function getAllIndexes(arr, val) { var indexes = [], i; for(i = 0; i < arr.length; i++) // if (arr[i].includes(val)) if (val.test(arr[i])) indexes.push(i); return indexes; } // find next smallest index in header array: https://stackoverflow.com/a/47498151 function getClosestValue(myArray, myValue){ //optional var i = 0; while(myArray[++i] < myValue); return myArray[--i]; }
-
script
// DISPLAY RESULTS AND NAVIGATE TO LOCATIONS WITHIN SELECTED DRAFTS var i = 0; var backlinks = [] var myTitle = "" var myInfo = "" if (queries) { // this, if we're searching for a linked reference let queriesDD = [...new Set(queries)]; var searchStr = "\"" + queriesDD.join("\" OR \"") + "\"" // let drafts = Draft.query(searchStr, searchScope,[],[],modOrCreated,true) let drafts = Draft.query(searchStr, searchScope,[],[ignoreResultsTaggedWith],modOrCreated,true) while(drafts[i]){ // get array of headers in this draft const headerIdx = getAllIndexes(drafts[i].lines,/^#+ /) var q=0 while(queries[q]){ hits = drafts[i].lines.filter(element => element.toString().toUpperCase().includes(queries[q].toString().toUpperCase()) ) var n=0 while(hits[n]){ let descr = hits[n] ? hits[n] : drafts[i].title let separator = drafts[i].tags.join() == "" ? "" : " ... " let regex = /(\B#(\d*[A-Za-z_]+\w*)\b(?!;))/g let inlineTags = regex.test(drafts[i].content) ? drafts[i].content.match(regex).filter((v, i, a) => a.indexOf(v) === i).join(", ") + " ... " : "" if (n>0) { myTitle = "..." myInfo = "" } else { myTitle = drafts[i].title myInfo = inlineTags + drafts[i].tags.join(", ") + separator + timeDifference(drafts[i][mocProp]) } const index = drafts[i].lines.findIndex(line => line === hits[n]); // index of current hit in array to calculate header... let myHeader = getClosestValue(headerIdx,index) // find header for this hit if (typeof myHeader == 'undefined') {myHeader = 0} // if draft contains no headers myHeader = (myHeader == 0) ? "" : drafts[i].lines[myHeader].replaceAll('#','').trim() + " ↵ " backlinks.push({ title: myTitle, description: descr.replace(/\t/g,""), info: myHeader + myInfo, uuid: drafts[i].uuid, metadata: drafts[i].content, permalink: drafts[i].permalink }); n++; } q++; } i++; } const keys = ['title', 'description'], filtered = backlinks.filter( (s => o => (k => !s.has(k) && s.add(k)) (keys.map(k => o[k]).join('|')) ) (new Set) ); } else { // all other searches... let drafts = Draft.query(searchStr, searchScope,[],[ignoreResultsTaggedWith],modOrCreated,true) while(drafts[i]){ // get array of headers in this draft const headerIdx = getAllIndexes(drafts[i].lines,/^#+ /) hits = drafts[i].lines.filter(element => element.toString().toUpperCase().includes(searchStr.toString().toUpperCase()) ) var n=0 while(hits[n]){ if (fullDraftsAsContext == true) { descr = drafts[i].title == hits[n] ? drafts[i].body : hits[n] } else { descr = drafts[i].title == hits[n] ? drafts[i].lines.filter((a) => a)[1] + "\n↳ _**first line of draft; search term in title**_" : hits[n] } let separator = drafts[i].tags.join() == "" ? "" : " ... " let regex = /(\B#(\d*[A-Za-z_]+\w*)\b(?!;))/g let inlineTags = regex.test(drafts[i].content) ? drafts[i].content.match(regex).filter((v, i, a) => a.indexOf(v) === i).join(", ") + " ... " : "" if (n>0) { myTitle = "..." myInfo = "" } else { myTitle = drafts[i].title myInfo = "**" + timeDifference(drafts[i][mocProp]) + "** " + inlineTags + drafts[i].tags.join(", ") } const index = drafts[i].lines.findIndex(line => line === hits[n]); // index of current hit in array to calculate header... let myHeader = getClosestValue(headerIdx,index) // find header for this hit if (typeof myHeader == 'undefined') {myHeader = 0} // if draft contains no headers myHeader = (myHeader == 0) ? "" : drafts[i].lines[myHeader].replaceAll('#','').trim() + " ↵ " backlinks.push({ title: myTitle, description: descr.replace(/\t/g,""), info: myHeader + myInfo, metadata: hits[n], uuid: drafts[i].uuid }); n++; } i++; } } // Create an MGCheckListPrompt. var prompt = new MGCheckListPrompt(); prompt.message = "Query: " + searchStr; prompt.addItems(backlinks) prompt.allowsTypeToFilter = true prompt.singleSelectionMode = true prompt.selectsImmediately = true prompt.processMarkdown = true prompt.includedContent = ` <style> .item-description { font-size: 90%; display: inline; } .item-info { font-size: 70%; display: inline; } </style> ` // Show the prompt. var selectedItems = prompt.show(); // Report the result. if (prompt.didShow) { if (selectedItems != null) { source = Draft.find(backlinks[selectedItems[0]].uuid); editor.load(source) fullText = editor.getText() anchor = backlinks[selectedItems[0]].metadata pos = fullText.indexOf(anchor) // find position of selected line // pos = fullText.search("/"+ escapeRegex(anchor) +"/i") if (queries) {searchStr = queries[0].toString().substr(2).slice(0, -2)} pos = fullText.toUpperCase().indexOf(searchStr.toString().toUpperCase(), pos) // this to select the search term editor.setSelectedRange(pos,searchStr.toString().length) // anchor.length or searchStr.length, to select search term editor.activate(); } else { } } else { app.displayErrorMessage("Looks like your query returned no results."); context.cancel() }
Options
-
After Success Nothing Notification None Log Level None