Action

RegEx Factory v.1.4

Posted by RoyRogers, Last update over 3 years ago

Multiple RegEx with preview and replace (or cancel).
- New: Run on selected text or whole draft if no selection.
- Save RegEx group expressions for future use.
- Each RegEx group can have one or more pairs of find and replace patterns.
- Generate RegEx JS script action as new draft in Inbox :)
- New: Easy ‘test and add’ new patterns to groups.
- New: Output to new draft or clipboard.
- New: Edit all, to delete or editing groups.

Steps

  • script

    // RegEx Factory v.1.4.4
    // RV 2020-09-12 at 19:56 EST
    
    // Multiple RegEx with preview/cancel or replace.
    // RegEx group expressions are automatically saved
    // to a text file in iCloud Drive on 'Preview/Run...'
    // Each RegEx group can have one or more replace patterns.
    // Each pattern is one line in the format: /regex find/, "replace"
    // RegEx swithes 'gm' are on by default (but can be changed)
    // Output to current draft, new draft or clipboard.
    // Run on selected text or whole draft if no selection.
    // Generate RegEx JS script action as new draft in Inbox :)
    
    'use strict';
    // Global variables:
    var path = '/RegExGroups.txt';
    var groupTitle = '';
    var regExGroup = '';
    var regExGroupList = [];
    var regExGroupIndex = -1;
    var startLastUsed = false;
    var find = '';
    var repl = '';
    var swi = false;
    var selRange = [];
    var draftText = '';
    
    function main() {
    	// Run on selected text or full draft if no selection:
    	selRange = editor.getSelectedRange();
    	if (selRange[1] == 0) {
        draftText = editor.getText();
      } else {
        draftText = editor.getSelectedText();  
      }
    
      let isRead = readGroupFile();
    
      if (selectGroup(isRead)) {
        while(regExFactory()); // Loops until false (done or canceled)
      } else {
        context.cancel('Canceled by user');
      }
    }
    
    function readGroupFile() {
      let fmCloud = FileManager.createCloud();
      let groupsText = fmCloud.readString(path);
      
      if (!groupsText) { 
        // No file found, use sample text instead:
        // see 'groupSample' at end of script.
        loadGroups(groupSample);
      } else {
        loadGroups(groupsText);
      }
    }
    
    function loadGroups(groupsText) {
    	regExGroupList = [];
    	
      let groups = groupsText.split('\n# ');
      let index = parseInt(groups[0].slice(12));
      if (index > -1) startLastUsed = true;
      
      for (let group of groups.slice(1)) {
        group = group.replace('\n', '\r');
        let item = group.split('\r');
        let title = item[0].replace(/^# /, '');
        let gRegEx = item[1].trim();
        regExGroupList.push([title, gRegEx]);
      }
      
      if (index == -1 || index >= regExGroupList.length) {
        regExGroupIndex = 0;
        getGroup(regExGroupIndex)
        return true;
      } else {      
        getGroup(index);
        return false;      
      }
    }
    
    function selectGroup(load) {
      if (!load) return true;
      let index = regExGroupIndex;
      
      if (regExGroupIndex == -1) {
        index = 0;
      }
      
      let p = Prompt.create();
      p.title = "Load RegEx Factory";
      p.addPicker('group', 'Group:', [listGroupTitles()], [index]);
      p.addSwitch('start', 'Start With Last Used', startLastUsed);
      p.addButton('Load Group');
      p.addButton('New Empty Group');
      p.addButton('Edit All Groups...');
      
      if (p.show()) {
        startLastUsed = p.fieldValues['start'];
        switch (p.buttonPressed) {
          case 'Load Group':
            getGroup(parseInt(p.fieldValues['group']));
            return false;
    
          case 'New Empty Group':
            groupTitle = '';
            regExGroup = '';
            regExGroupIndex = -1;
            return false;
    
          case 'Edit All Groups...':
            editAllGroups();
            return true;
    
          default:
            app.displayErrorMessage("switch/case not implemented!");
            return true;
        }
        return true;
      }
      return false;
    }
    
    function editAllGroups() {
      // regExGroupList.push([title, gRegEx])
      let groups = [];
      
      for (const group of regExGroupList) {
        groups.push('# ' + group[0]); // Group title
        groups.push(group[1] + '\n'); // Group body    
      }
        
    	let groupsText = groups.join('\n');
      let p = Prompt.create();
      p.title = 'Edit or Delete Groups:';
      p.message = 'Format: \n# Group title\n/find pattern/gm, "replace"';
      p.addTextView('text', 'RegEx Groups:', groupsText, {'height': 300})
      p.addButton('Save changes');
      
      if (p.show()) {
        let groupsText2 = p.fieldValues['text'];
        if (groupsText2.split('\n').length < 2) {
          app.displayErrorMessage("Wrong format!");
          return false;
        }
        if (p.buttonPressed == 'Save changes' && groupsText2 != groupsText) {
          let index = regExGroupIndex;
          if (startLastUsed) {
            groupsText2 = 'StartIndex = ' + regExGroupIndex + '\n' + groupsText2;
          } else {
            groupsText2 = 'StartIndex = -1\n' + groupsText2;
          }
          
          try {
            loadGroups(groupsText2);
          // saveGroupFile(regExGroupIndex);
          } catch(err) {
            alert(err.message);
            return false;
          }
          
          return true;
        }  
      } else {
        return false;
      }
    }
    
    function getGroup(index) {
      if (index == -1) return;
      groupTitle = regExGroupList[index][0];
      regExGroup = regExGroupList[index][1];
      regExGroupIndex = index;
    }
    
    function listGroupTitles() {
      let titleList = [];
      
      for (const group of regExGroupList) {
        titleList.push(group[0]);
      }
      
      return titleList;
    }
    
    function regExFactory() {
      let p = Prompt.create();
      p.title = "RegEx Factory";
      p.addTextField("groupTitle", "Title:", groupTitle, {"wantsFocus": true });
      p.addTextView('text', 'RegExGroup:', regExGroup)
      p.addButton('Test & Run on Current Draft...');
      p.addButton('Run to New Draft');
      p.addButton('Run to Clipboard');
      p.addButton('Add New Pattern...');
      p.addButton('Load Other Groups...');
      p.addButton('Save as New...');
      p.addButton('JS Code to Inbox');
      p.addButton('RegEx Help...');
      
      if (p.show()) {
        groupTitle = p.fieldValues['groupTitle'];
        regExGroup = p.fieldValues['text'];
        
        switch (p.buttonPressed) {
          case 'Add New Pattern...':
            while (addPattern(regExGroup)); // Loops until false = done or canceled
            break;
    
          case 'Load Other Groups...':
            while(selectGroup(true));
            break;
    
          case 'Save as New...':
            saveAsNew();
            break;        
          case 'Test & Run on Current Draft...':
            return (!runRegEx('current'));
    
          case 'Run to New Draft':
            return (!runRegEx('new'));
    
          case 'Run to Clipboard':
            return (!runRegEx('clipboard'));
    
          case 'JS Code to Inbox':
            generateJsCode(regExGroup);
            break;
    
          case 'RegEx Help...':
            app.openURL('https://regex101.com')
            break;      
    
          default:
            app.displayErrorMessage("switch/case not implemented!") ;
            break;
        }
        return true; // continue while loop
      } else {
        context.cancel('Canceled by user!');
        return false; // end while loop
      }
    }
    
    function saveAsNew() {
      if (regExGroup.trim() == '') {
        app.displayErrorMessage('RegEx arguments missing!');
        return false;
      }
    
      let p = Prompt.create();
      p.title = 'Save as New Group';
      p.message = regExGroup;
      p.addTextField('name', 'New Group Name:', groupTitle);
      p.addButton('Save');
    
      if (p.show()) {
        if (p.buttonPressed == 'Save') {
          groupTitle = p.fieldValues['name'];         
          saveGroupFile(-1);
          return true;
        }
      } 
      return false;
    }
    
    function runRegEx(type) {
      if (regExGroup.trim() == '') {
        app.displayErrorMessage('RegEx arguments missing!');
        return false;
      }
    
    	// Replace text with regExGroup:
      const [text, group] = replaceByRegexGroup(draftText, regExGroup);
      if (text == '') {
        app.displayErrorMessage('Empty result!');
        return false;
    	} 
    	
      saveGroupFile(regExGroupIndex);
      
      switch (type) {
        case 'current':
          if (preview(text, group)) {
            if (selRange[1]== 0) {
              editor.setText(text);
            } else {
              editor.setSelectedText(text);
              let diff = text.length - draftText.length;
              editor.setSelectedRange(selRange[0], selRange[1] + diff);        
            }
            
            draft.addTag('regexed');
            app.displayInfoMessage('Draft is "RegExed"!');
            return true;
            
          }
          return false;
    
        case 'new':
          let d = Draft.create();
          d.content = text;
          d.languageGrammar = draft.languageGrammar;
          d.update();
    
          for (const tag of draft.tags) {
            d.addTag(tag);
          }
    
          // d.isFlagged = draft.isFlagged;
          d.addTag('regexed');
          d.update();
          editor.load(d);
          app.displayInfoMessage('"RegExed" as new draft!');
          return true;
    
        case 'clipboard':
          app.setClipboard(text);
          app.displayInfoMessage('"RegExed" to clipboard!');
          return true;
     
        default:
          app.displayErrorMessage('switch/case not implemented!');
          return false;
      }
    }
    
    function addPattern() {
      let p = Prompt.create();
      p.title = 'New Pattern';
      p.message = 'Add to: "' + groupTitle + '"';
      p.addTextField('find', 'Find regx:', find);
      p.addTextField('repl', 'Replace: ', repl);
      p.addSwitch('swi', 'Case Insensitive', swi);
      
      p.addButton('Test and Add Pattern...');
      p.addButton('Test with Current Group...');
      p.addButton('Add without Testing...');
      
      if (p.show()) {
        find = p.fieldValues['find'];
        repl = p.fieldValues['repl'];
        swi = p.fieldValues['swi'];
        let i = ''
        
        if (swi) {
          i = 'gmi'; // add 'i' with default 'gm'
        } else {
          i = 'gm'; // add as default
        }
        
        let pattern = `/${find}/${i}, "${repl}"`;
        
        if (find == '') {
          app.displayInfoMessage('Find field: is empty!');
          return true;
        }
        let text = '';
        let group = '';
        let ln = '';
        if (regExGroup.trim() != '') ln = '\n';
        switch (p.buttonPressed) {
          case 'Test and Add Pattern...':
            [text, group] = replaceByRegexGroup(draftText, pattern);
            if (preview(text, group, true)) {
    	        regExGroup = regExGroup.trim() + ln + pattern;
              if (text) saveGroupFile(regExGroupIndex);
              return false;
            }
            break;
    
          case 'Test with Current Group...':
            let reGroup = regExGroup.trim() + ln + pattern;
            [text, group] = replaceByRegexGroup(draftText, reGroup);
            if (preview(text, group, true)) {
    	        regExGroup = reGroup;
              if (text) saveGroupFile(regExGroupIndex);
              return false;
            }
            break;
    
          case 'Add without Testing...':
            regExGroup = regExGroup.trim() + ln + pattern; 
            saveGroupFile(regExGroupIndex);
            return false;
     
          default:
            app.displayErrorMessage('switch/case not implemented!');
            break;
        }
        return true; // Continue loop
      }
      return false;
    }
    
    function saveGroupFile(ind) {
      let title = groupTitle;
      if (groupTitle == '') {
        title = 'Untitled';
      } 
      
      if (ind == -1) {
        regExGroupList.push([title, regExGroup]);
        regExGroupIndex = regExGroupList.length - 1
      } else {
        regExGroupList[regExGroupIndex] = [title, regExGroup];
      }
      
      let gList = [];
      if (startLastUsed) {
        gList = ['StartIndex = ' + regExGroupIndex]
      } else {
        gList = ['StartIndex = -1']
      }
      
      for (const group of regExGroupList) {
        gList.push('# ' + group[0]); // groupTitle
        gList.push(group[1]);      // regExGroup
      }
      
      let fmCloud = FileManager.createCloud();
      let success = fmCloud.write(path, gList.join('\n'));
    }
    
    function replaceByRegexGroup(text, reGroup) {  
      for (const row of reGroup.split('\n')) {
        // Skip commented and blank lines:
        if (row.startsWith('// ') || row.trim() == '') continue;
    
        let m = row.match(/^\/(.+?)\/([gmiyus]{0,6}), ?\"(.*)\"/)
        if (!m) {
          app.displayErrorMessage("Error in line syntax: " + row) ;
          return ['', reGroup];
        }
        
        let fnd = m[1];
        let sw = m[2];
        let rep = m[3];
        
        if (sw == '') {
          // If adding any switches, please include 'gm' as well!
          sw = 'gm'; // Default RegEx switch
        }
        
        let reFind = new RegExp(fnd, sw);
        rep = rep.replace(/\\n/g, '\n');
        rep = rep.replace(/\\t/g, '\t');
        rep = rep.replace(/\\r/g, '\r');
        text = text.replace(reFind, rep);
      }
      
      if (text.trim() == '') {
        app.displayErrorMessage("Empty text result!") ;
        return ['', reGroup];
      }
      
      return [text, reGroup];
    }
    
    function preview(text, group, testPattern=false) {
    	let message = '';
    
    	if (testPattern) {
        message = 'Press "Continue" to add pattern to current group!';
    	} else {
        message = 'Press "Continue" to replace current draft!' 
    	}
    
      // Test: < > "' &
      text = text.replace(/</g, '&lt;');
      // text = text.replace(/>/g, '&gt;');
      // text = text.replace(/&/g, '&amp;');
      // text = text.replace(/'/g, "&apos;");
      // text = text.replace(/"/g, '&quot;');
    
      if (app.currentThemeMode == 'dark') {
        var css = "body { background: #222; color: #ddd; }";
      } else {
        var css = "body { background: #fff; color: #444; }";
      }
      
      // add and show default switches 'gm' (if none):
      let groupWithSwitches = group.replace(/^(\/.+?\/)(?!\w+)(, ?".*")/gm, '$1gm$2');
      
      let html = `<html><style>${css}</style><body>
      <h3>Result-text preview below line<h3>
      <h4>${message}<br>
      or "Cancel\" to exit with no changes.</h4>
      <h4>RegEx group used: "${groupTitle}\"</h4>
      <pre>${groupWithSwitches}</pre>
      <p>(If no switches specified, 'gm\' = "Global, Multiline\" are added by default)</p>
      <hr>
      <pre>${text}</pre>
      </body></html>`
      let preview = HTMLPreview.create();
      return preview.show(html);
    }
    
    function generateJsCode(reGroup) {
      // Generates JS replace function snippet as draft in Inbox
      const now = new Date()
      const date = now.toString("yyyy-MM-dd")
      const time = now.toString("HH:mm")
    
      let codeLines = [];
      codeLines.push(`// RegEx Replace: ${groupTitle}`)
      codeLines.push(`// Code generated by "RegEx Factory": ${date} at ${time}`);
      codeLines.push('');
      makeDraftActionCode(codeLines); // Make Drafts Action Script (add comment to skip)
      codeLines.push('function regExReplace(text) {');
      codeLines.push(`  // Group Title: ${groupTitle}`)
      
      for (const row of reGroup.split('\n')) {
        if (row.startsWith('// ') || row.trim() == '') {
          // Blank and commented lines and set indent:
          codeLines.push(row.replace(/^\/\/ /, '  \/\/ '));
          continue;
        }
        let m = row.match(/^\/(.+?)\/([gmi]{0,3}), ?\"(.*)\"/)
        if (!m) {
          app.displayErrorMessage("Error in line syntax: " + row) ;
          return ['', reGroup];
        }
        
        let fnd = m[1];
        let rep = m[3];
        let sw = m[2];
        if (sw == '') { 
          sw = 'gm'; // Default RegEx switch
        }
        
        let line = `  text = text.replace(/${fnd}/${sw}, '${rep}');`
        codeLines.push(line)
      }
      codeLines.push('  return text;');  
      codeLines.push('}\n\n');
      let code = codeLines.join('\n');
      // app.setClipboard(code);
      
      let d = Draft.create();
      d.content = code;
      d.languageGrammar = "JavaScript";
      d.update();
      d.addTag("script");
      d.addTag("regex");
      d.update();
      
      app.displayInfoMessage('function myRegExReplace() saved to Inbox :)') ;
    }
    
    function makeDraftActionCode(codeLines) {
      // Makes fully functioning draft Action Script:
      codeLines.push("var p = Prompt.create();");
      codeLines.push("p.title = 'Run RegEx Replace on Current Draft';");
      codeLines.push("p.addButton('Save as New Draft');");
      codeLines.push("p.addButton('Replace Current Draft');");
      codeLines.push("");
      codeLines.push("if (p.show()) {");
      codeLines.push("  let text = regExReplace(editor.getText());");
      codeLines.push("  ");
      codeLines.push("  switch (p.buttonPressed) {");
      codeLines.push("    case 'Save as New Draft':");
      codeLines.push("      let d = Draft.create();");
      codeLines.push("      d.content = text;");
      codeLines.push("      // d.languageGrammar = 'MultiMarkdown';");
      codeLines.push("      // d.languageGrammar = 'JavaScript';");
      codeLines.push("      d.update();");
      codeLines.push("      d.addTag('regexed');");
      codeLines.push("      d.update();");
      codeLines.push("      break;");
      codeLines.push("    case 'Replace Current Draft':");
      codeLines.push("      editor.setText(text);");
      codeLines.push("      draft.addTag('regexed');");
      codeLines.push("      break;");
      codeLines.push("  }");
      codeLines.push("} else {");
      codeLines.push("  context.cancel('Canceled by user!');");
      codeLines.push("}");
      codeLines.push("");
    }
    
    // Note! double escaping of \n \w etc:
    var groupSample = `StartIndex = 2
    # Multiple to double line breaks
    /\\n\\n+/gm, "\\n\\n"
    
    # Multiple to single line breaks
    /\\n+/gm, "\\n"
    
    # Clean Markdown Lines
    /\\n+/gm, "\\n\\n"
    /\\n#/gm, "\\n\\n#"
    /(\\n###+.+?\\n)\\n/gm, "$1"
    /^([\\-+*>] .+?)\\n/gm, "$1"
    /^([\\-+*>] .+?\\n)(?![\\-+*>] )/gm, "$1\\n"
    
    # Smart quotes & m to n-dashes
    /"(.+?)"/gm, "“$1”"
    /(\\W)'(\\w.+?)'(\\W)/gm, "$1‘$2’$3"
    /(\\w)'/gm, "$1’"
    / ?-- ?/gm, " – "
    / ?— ?/gm, " – "`;
    
    // Run main function:
    main();
    
    

Options

  • After Success Default
    Notification Error
    Log Level Error
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.