Increase headers by one until reset? #, ##, ###, (none)

I wish that I could tap the “#” button multiple times and have it add that many header characters to the selected line. After 3 characters, the fourth tap would remove all the header characters.

Here’s an example:

Test Header Line <- zero taps  of the "#" button
# Test Header Line <- one tap of the "#" button
## Test Header Line <- two taps of the "#" button
### Test Header Line <- three taps of the "#" button
Test Header Line <- four taps of the "#" button

Background:

I was never much of a headers guy, but now that I’m integrating (and somewhat successfully round-tripping) my Drafts with my MindNode mind maps, header characters - “#” - are becoming important.

The OOTB add header button works just fine to add a single character - “#” - and then remove it.

What I’d like to do is to have subsequent presses of the “#” button/action, add that character to the existing line and then ultimately remove them all… for me, arbitrarily, I think three header characters would be good.

If someone has already done this, would you post a link? If not, is anyone interested in posting a solution? I’m not yet comfortable (at all) with the loc/len and push commands (but have learned much from the solutions others have posted for other requests).

Thanks, and here is the current action for reference:

// Toggle tasks marks on selected lines
var listMark = "#";

// grab state
var [lnStart, lnLen] = editor.getSelectedLineRange();
var lnText = editor.getTextInRange(lnStart, lnLen);
var [selStart, selLen] = editor.getSelectedRange();

// just add mark if empty line
if (lnText.length == 0 || lnText == "\n") { 
  editor.setSelectedText(`${listMark} `);
  editor.setSelectedRange(selStart + listMark.length + 1, 0);
}
else {
  // create line array and tracking vars
  var lines = lnText.split('\n');
  var startOffset = 0;
  var lengthAdjust = 0;
  var flTrailing = false;
  if (lines[lines.length - 1] == "") { 
    lines.pop();
    flTrailing = true;
  }
  var newLines = [];
  const re = /^(\s*)?([#] )?(.*)/;
  const containsRe = /^(\s?)([#] )/;

  // determine if we are removing or adding marks
  var flRemoving = true;
  for (var line of lines) {
   if (line.length > 0 && !line.match(containsRe)) {
     flRemoving = false;
    }
  }

  if (!flRemoving) {
    // add marks
    var isFirst = true;
    for (var line of lines) {
      const match = re.exec(line);
		if (match[2] || line.length == 0) {
      	  newLines.push(line);
      }
      else {
        var prefix = match[1];
        var suffix = match[3];
        if (!prefix) { prefix = ""; }
        if (!suffix) { suffix = ""; }
        newLines.push(`${prefix}${listMark} ${suffix}`);
        if (isFirst) {
          startOffset = listMark.length + 1;
        }
        else {
          lengthAdjust += (listMark.length + 1);
        }
      }
      isFirst = false;
    }
  } else {
    // remove marks
    var isFirst = true;
    for (var line of lines) {
      if (line.trim() == "") {
        newLines.push(line);
        continue;
      }
      const match = re.exec(line);
      var prefix = match[1];
      var suffix = match[3];
      var state = match[2];
      if (!prefix) { prefix = ""; }
      if (!suffix) { suffix = ""; }
      if (suffix.startsWith(" ")) { 
        suffix = suffix.substr(1);
        if (isFirst) { startOffset -= 1; }
        else { lengthAdjust -= 1; }
      }
      newLines.push(`${prefix}${suffix}`);
      if (isFirst) {
        startOffset -= state.length;
      }      
      else {
      	  lengthAdjust -= state.length;
      }
      isFirst = false;
    }
  }

  // update text and selection
  if (flTrailing) {
    newLines.push("");
  }
  var newText = newLines.join("\n");
  editor.setTextInRange(lnStart, lnLen, newText);
  editor.setSelectedRange(selStart + startOffset, selLen + lengthAdjust);
}

I had trouble following your script because of the formatting. Surrounding your code with ``` will make it easier to read.

After thinking about how I would do it from scratch, I came up with this:

// Extend the selection to include full lines
var r = editor.getSelectedLineRange();
editor.setSelectedRange(r[0], r[1]);
var sel = editor.getSelectedText();

// Don't include any trailing newline
if (sel.substring(sel.length-1) =="\n") {
	editor.setSelectedRange(r[0], r[1]-1);
	sel = editor.getSelectedText();
}

// Make a list of lines
var lines = sel.split("\n");
var n = lines.length;

// Adjust the leading hashes for each line in turn
for (var i=0; i<n; i++) {
	if (lines[i].substring(0, 4) == "### ") {
		lines[i] = lines[i].substring(4);
	} else if (lines[i].substring(0, 1) == "#") {
		lines[i] = "#" + lines[i];
	} else {
		lines[i] = "# " + lines[i];
	}
}
// Rejoin the lines and replace the text in the editor
var s = lines.join("\n");
editor.setSelectedText(s);

The key, I think, is to use getSelectedLineRange and setSelectedRange in concert to extend the selection to full lines (minus any trailing newline). Then you can change the hashes on each line individually and replace the selection in one fell swoop when you’re done.

You can get this action at https://actions.getdrafts.com/a/134

1 Like

Perfect. Thank you.

And, I tried to set the formatting above using your suggestion but I didn’t know how to do it without adding a leading and trailing back-tick on each line.

Formatting example:

Writing this in the post editor.

``` 
let foo = "bar";
let quz = "qux";
```

Becomes this when posting.

let foo = "bar";
let quz = "qux";
1 Like

Fixed it. Thank you.

I updated the default Markdown header action to cycle back to no header level when it is used on a header already at the maximum six levels.

That’s not what you were working from, because it only works on the current line, not a block of lines…but it is how it should have always behaved.

1 Like

I’ll be using this one, also. Thank you.