1

I would like to format Markdown headings to titlecase, and I have a bash command that works to format input:

echo "convert to title" | awk 'BEGIN{split("a the to at in on with and but or",w); for(i in w)nocap[w[i]]}function cap(word){return toupper(substr(word,1,1)) tolower(substr(word,2))}{for(i=1;i<=NF;++i){printf "%s%s",(i==1||i==NF||!(tolower($i) in nocap)?cap($i):tolower($i)),(i==NF?"\n":" ")}}'

I would like to format all my Markdown headings in the current buffer to have this conversion done. I know I need to use ! somehow, basically:

IF line begins with # or ## or ### 
  THEN run the line through the external command

The vimscript is too advanced for me... can anyone help?

2 Answers 2

3

The question was about using external command to change the headings to title case. Anyway I would propose a Vim only solution - for me it looks simpler than awk ;-).

"words that should not be changed to title case
let s:TC_IGN_WORDS = [ 'a', 'is', 'the', 'to', 'at', 'in', 'on', 'with', 'and', 'but', 'or' ]

function! Tc_Word(word)
    if index(s:TC_IGN_WORDS, a:word) >= 0
        return a:word
    else
        return toupper(a:word[0]) . a:word[1:]
    endif
endfunction

command TcHeaders g/^#/s/\<\w\+\>/\u\0/|s/\<\w\+\>/\=Tc_Word(submatch(0))/g

The command TcHeaders runs a substitution on all markdown headings (g/^#/).

The substitution takes every word, feeds it to the function Tc_Word() and replaces it with what Tc_Word() returns.

The search pattern could be further refined like:

  • \<\l\w*\>: Only words that begin with a lower case character
  • \<\l\w\w+\>: Begins with a lower case and is at least three chars long.

Note: I didn't lowercase the second part of the word, as this would replace something like HTTP with Http. I'm not sure this is intended.

See :help s/\\=, :help submatch.

2
  • Brilliant! The only thing missing is that the first word of the heading should always be capitalized. There are more advanced grammar based rules to be perfect depending on which style you go for, but your function is enough for me :) Just out of interest this guy lists the full rules and the JavaScript code can be looked at in the the browser titlecaseconverter.com
    – Sumomo
    Apr 17, 2020 at 9:08
  • 1
    @Sumomo I updated it to always capitalize the first word, but I don't have time to implement all the described rules.
    – Ralf
    Apr 17, 2020 at 9:37
3

All you need is the global command:

:global/^#/.!your awk command

This says « for all lines matching ^#, filter them through your awk command ». The . is explicitly needed here (where it usually isn't) because :! runs a command, but :[range]! runs the command as a filter. While :global usually acts like a range, in this odd case it doesn't.

Since % is always current filename, you need to escape it as \%. And when running ! commands, ! will be replaced by the last ! command. To avoid that, you can escape it too: \!.

This gives

global/^#/.!awk 'BEGIN{split("a the to at in on with and but or",w); for(i in w)nocap[w[i]]}function cap(word){return toupper(substr(word,1,1)) tolower(substr(word,2))}{for(i=1;i<=NF;++i){printf "\%s\%s",(i==1||i==NF||\!(tolower($i) in nocap)?cap($i):tolower($i)),(i==NF?"\n":" ")}}'

As was pointed out in the comments, an even more compose-able/reusable way of doing this would be to create a script something like:

#! /usr/bin/awk -f

BEGIN {
    split("a the to at in on with and but or",w)
    for(i in w)
        nocap[w[i]]
}
function cap(word) {
    return toupper(substr(word,1,1)) tolower(substr(word,2))
}
{
    for(i=1;i<=NF;++i) {
        printf "%s%s", (i==1 || i==NF || !(tolower($i) in nocap) ? cap($i) : tolower($i)), (i==NF?"\n":" ")
    }
}

If you save this on somewhere on your PATH (call it, say, titlecase) and chmod u+x it, then you can call it from your shell and from vim:

:global/^#/.!titlecase

One benefit this has is you don't need the escaping at all.

6
  • Thanks, but I am afraid I can't get my capital case awk command to work. I tried escaping % and ! but it still gives all sorts of errors. Could you possible replace your answer with the full command that works? Cheers! Here is my failing version: nnoremap <F7> :global/^#/!awk 'BEGIN{split("a the to at in on with and but or",w); for(i in w)nocap[w[i]]}function cap(word){return toupper(substr(word,1,1)) tolower(substr(word,2))}{for(i=1;i<=NF;++i){printf "\%s\%s",(i==1||i==NF||\!(tolower($i) in nocap)?cap($i):tolower($i)),(i==NF?"\n":" ")}}'<cr>
    – Sumomo
    Apr 14, 2020 at 9:43
  • @Sumomo updated
    – D. Ben Knoble
    Apr 14, 2020 at 21:49
  • @DavidBenKnoble Thanks! It now works great when I copy it into vim manually, but when I add it to .vimrc I get E749: Empty buffer E10: \ should be followed by /, ? or &. This is so weird! Here is my line: nnoremap <F7> :global/^#/.!awk 'BEGIN{split("and or but nor yet so for a an the in to of at by up for off on",w); for(i in w)nocap[w[i]]}function cap(word){return toupper(substr(word,1,1)) tolower(substr(word,2))}{for(i=1;i<=NF;++i){printf "\%s\%s",(i==1||i==NF||\!(tolower($i) in nocap)?cap($i):tolower($i)),(i==NF?"\n":" ")}}'<CR>
    – Sumomo
    Apr 14, 2020 at 23:17
  • @Sumomo you might have to double sone of the escapes. Tbh im not sure
    – D. Ben Knoble
    Apr 14, 2020 at 23:43
  • Maybe I missed something but why not just stick all that awk code in a file like foo.txt then use :g/^#/.!awk -f foo.txt. You won't have to escape anything. You can even format the awk script file so code spans multiple lines...becoming readable.
    – B Layer
    Apr 15, 2020 at 2:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.