Action

Block level filter (220324)

Posted by @jsamlarose, Last update about 2 years ago - Unlisted

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
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.