Action

Block level filter to new draft

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

UPDATES

over 2 years ago

Added “ignoreResultsTaggedWith” preference to suppress results from drafts produced by this action (tagged with “filter output” by default).

Branched off from the original Block level filter to write filter results to a new draft. In response to this conversation.

Could do with some polish…
- The draft that’s produced by this action will skew the results. It’d be better to output to a fixed draft every time. That would allow users to work with filter results as a slide over or split screen panel on iOS. Alternatively, we’d need to hide the draft(s) produced by this action from the filter for future searches… Fixed this with a preferences update…
- As this action actually creates a draft (rather than presenting results in a menu), we’ll need a separate action to target the exact location of any specific block of context within its source draft.
- Including the UUID probably isn’t necessary. Edge case: might be useful if a title is updated before the filter output is refreshed?
- Ultimately, this could be just be folded into the original action (as an option from the initial prompt)

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:","")
    	// 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-z_]+\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-z_]+\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
    }
    
    // https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
    
    /* function timeSince(date) {
    
      var seconds = Math.floor((new Date() - date) / 1000);
    
      var interval = seconds / 31536000;
    
      if (interval > 1) {
        return Math.floor(interval) + " years";
      }
      interval = seconds / 2592000;
      if (interval > 1) {
        return Math.floor(interval) + " months";
      }
      interval = seconds / 86400;
      if (interval > 1) {
        return Math.floor(interval) + " days";
      }
      interval = seconds / 3600;
      if (interval > 1) {
        return Math.floor(interval) + " hours";
      }
      interval = seconds / 60;
      if (interval > 1) {
        return Math.floor(interval) + " minutes";
      }
      return Math.floor(seconds) + " seconds";
    }
    
    */
    
    
  • script

    // DISPLAY RESULTS AND NAVIGATE TO LOCATIONS WITHIN SELECTED DRAFTS
    
    var i = 0;
    var backlinks = []
    
    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, [],[ignoreResultsTaggedWith],modOrCreated,true)
    
    	while(drafts[i]){
    	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]){
        			// descr = (typeof hits[n] == 'undefined') ? drafts[i].title : 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(", ") + " ... " : ""
    
    			backlinks.push({
    				title: drafts[i].title,
    				description: descr.replace(/\t/g,""),
    				// info: drafts[i].tags.join()+ timeDifference(drafts[i].createdAt),
    				info: inlineTags + drafts[i].tags.join(", ") + separator + timeDifference(drafts[i][mocProp]),
    				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]){
    
        		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].processTemplate("[[body]]") : hits[n]
        			} else {
        			// descr = drafts[i].title == hits[n] ? "" : hits[n]
        			descr = drafts[i].title == hits[n] ? draft.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(", ") + " ... " : ""
    			
    			backlinks.push({
    						title: drafts[i].title,
    						// description: hits[n],
    						description: descr.replace(/\t/g,""),
    						// info: drafts[i].tags.join()+ timeDifference(drafts[i][mocProp]),
    						info: inlineTags + drafts[i].tags.join(", ") + separator + timeDifference(drafts[i][mocProp]),
    						metadata: hits[n],
    						uuid: drafts[i].uuid
            			});
        		n++;
        		}
    	i++;
    	}
    
    }
    
    let output = "# FILTER OUTPUT RESULTS:" + searchStr + "\n"
    
    backlinks.forEach(function(item){
    	if ((item.description === undefined) ||
        (item.description == null) || (item.description == "undefined")) {
    		output += "\n[[" + item.title + "]]\n+ " + item.info + "\n[[u:" + item.uuid + "]]\n"
    	} else {
    		output += "\n[[" + item.title + "]]\n↳ " + item.description.replace("undefined\n↳","") + "\n\t+ " + item.info + "\n\t[[u:" + item.uuid + "]]\n"
    	}
    })
    
    
    let filterD = new Draft()
    filterD.content = output
    filterD.addTag(ignoreResultsTaggedWith)
    filterD.update()
    editor.load(filterD)
    
    
    // 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;
    // }
    // </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.