Blog/Projects/parseArger/My markdown tool, a parsearger project

My markdown tool, a parsearger project

A realistic example is worth days of hello world tutorials !

10/27/2023
10 min read

Let's create a project together !

I'm one of those learn by doing guys (mostly), I'm assuming your kind of are too, so let's build something together.

A bit of context and basic requirements

For this blog, I'm using hugo as a static site generator, and I'm writing my articles in markdown. hugo new does the job, but I want something a bit different, with a folder that contains everything I need for the article (asset, code, etc...). I would like my new post to be prefilled as much as possible so the tool should be able to set any meta. Also, I tend to craft my article squeletton before writting (#trauma #school XD), so I should at least be able to add main headings from the CLI. Because I do not know where this is goping, I won't just generate a script, I am going to start a new project so I can add more stuff later on.

Leeeeeet's goooooo !

Generate the project

We are using the project command, more information can be found in the previous article if you need help.

# create a new project called mdd (MarkDown for Didi. Much creativity, such accronym, #wow)
# one subcommand 'article' for now.
parseArger project mdd \
  --description "markdown tools for my blog" \
  --git-repo "DimitriGilbert/mdd" \
  --project-subcommand article

This command will create a new folder mdd with the following structure:

|_ mdd
|_ documentation.md
|_ form.html
|_ Makefile
|_ readme.md
|_ bin
  |_ article
|_ utils
  |_ get_mdd
  |_ install
  |_ webserver 

Our entry point mdd should handle the command routing on its own, no work needed so we go along to article.

Parse the article command

After some careful thoughts, I have refined my requirements which are now the following:

  • create a new folder with the article name
    • if $(pwd) contains a content folder, create the article in it
    • article can have hierarchical categories. (if more than one, the previous is the parent)
      • categories make the path to the article
  • create index.md from template you can specify
  • tags, series, title, draft (on), date (now), summary metas handled by CLI
    • add any other meta you want
  • add headings
    • headings level can be specified

From all that, we know that we'll need the folowing :

  • arguments :
    • title (the only thing mandatory, so it's an argument)
  • options :
    • categories (repeatable)
    • folder name (not strictly necessary, but i can forsee a need soooo...)
    • tags (repeatable)
    • series (repeatable)
    • date
    • summary
    • template
    • headings (repeatable)
    • headings level (default 2)
  • flags :
    • draft (on by default)
  • nested options :
    • meta
      • due to bash limitation, no repeatable nested options, so only the last value for a key will be kept

With that in mind, let's build our parse command to update our bin/article file. Don't forget to remove comment before executing, they don't play well with backslashes ^^.

# parse the bin/article file
parseArger parse bin/article \
  # add the title argument, nothing special here
  --pos 'title "article title"' \
  # add the folder name option, using title if none provided. I also declare aliases for this option, namely 'dir' and 'directory'
  --opt 'folder "article folder name" --alias directory --alias dir' \
  # add the categories option, repeatable, with aliases 'cat' and 'parent', a short version of the option is given, namely 'c'
  --opt 'categories "article parent categories" --short c --alias cat --alias parent --repeat' \
  # add the tags option, repeatable, with alias 'tag' and short option 't'
  --opt 'tags "article tags" --repeat --short t --alias tag' \
  # add the series option, with alias 'group' and short option 'g'
  --opt 'series "article belongs to this series" --alias group --short g' \
  # add the date option with short option 'd' and aliases 'publication', 'publish-at'
  --opt 'date "publication date meta" --alias publication --alias publish-at --short d' \
  # add the summary option with short option 's' and aliases 'description', 'desc'
  --opt 'summary "summary meta" --short s --alias description --alias desc' \
  # add the template option with short option 'tpl' and alias 'template'
  --opt 'template "template file to use" --alias tpl' \
  # add the headings option, repeatable, with short option 'h' and aliases 'part', 'h2'
  --opt 'headings "add headings to your file" --repeat --alias part --alias h2' \
  # add the headings level option with short option 'hl' and alias 'headings-level'
  --opt 'headings-level "heading level" --default-value 2 --alias hl' \
  # add the draft flag on by default and with a no-alias as publish 
  #   meaning if you want it off, you can either use --no-draft or --publish
  --flag 'draft "is it a draft" --on --no-alias publish' \
  # add the meta nested option
  --nested-options 'meta "add any meta you want"' \
  # parse in place, -i is the short option
  --in-place

This will give us the following bin/article file :

{{% fileContent file="/content/Projects/parseArger/My markdown tool, a parsearger project/article_v1" language="bash" %}}

It's a whole bunch of code, but mercyfully you do not have to pay attention to it \o/ (I mean, you can if you want, but it's not necessary).

Instead we are going to focus on the code that is going to use those parsed arguments and stuff !

The (real) code

Well, let's start fullfilling our requirements !

Folder stuff

I use the log function with a level of 4 for comments ;)

# if nothing else, the title
_container_dir="$_arg_title";

if [ "$_arg_folder" != "" ]; then
	log "folder specified: $_arg_folder" 4;
	_container_dir="$_arg_folder";
fi

# there is categories at least 1
if [ "${#_arg_categories[@]}" -gt 0 ]; then
	log "categories specified:" 4;
	_cat_dir="";
	for _cat in "${_arg_categories[@]}"; do
		_cat_dir+="$_cat/";
	done
	log "	categories path : $_cat_dir" 4;
	_container_dir="$_cat_dir$_container_dir";
fi

# hugo directory, it's a poor check , I don't care ^^
if [ -d "content" ]; then
	log "using content directory" 4;
	_container_dir="content/$_container_dir";
fi

# create the directory if it does not exist
if [ ! -d "$_container_dir" ]; then
	log "creating container directory: $_container_dir" 2;
	mkdir -p "$_container_dir";
fi

Meta stuff

Here we'll handle all the metas so that we can put that to rest.

# start with the title, it's always here
_metas_str="title: $_arg_title\n";

if [ "$_arg_date" != "" ]; then
	log "date specified: $_arg_date" 4;
	_metas_str+="date: $_arg_date\n";
fi

if [ "$_arg_summary" != "" ]; then
	log "summary specified: $_arg_summary" 4;
	_metas_str+="summary: $_arg_summary\n";
fi

if [ "${#_arg_tags[@]}" -gt 0  ]; then
	log "${#_arg_tags[@]} tags specified:" 4;
	_metas_str+="tags: \n";
	for _tg in "${_arg_tags[@]}"; do
		log "\t$_tg" 4;
		_metas_str+="\t- $_tg\n";
	done
fi

if [ "$_arg_series" != "" ]; then
	log "series specified: $_arg_series" 4;
	_metas_str+="series: $_arg_series\n";
fi

if [ "$_arg_draft" == "on" ]; then
	log "draft on" 4;
	_metas_str+="draft: true\n";
fi

if [ "${#_arg_ns_meta[@]}" -gt 0 ]; then
	log "other metas specified:" 4;
	for _tmp_k_meta in "${!_arg_ns_meta[@]}"; do
		log "\t$_tmp_k_meta: ${_arg_ns_meta[$_tmp_k_meta]}" 4;
		_metas_str+="$_tmp_k_meta: ${_arg_ns_meta[$_tmp_k_meta]}\n";
	done
fi

Creating the headings

My article squeletton straight from the CLI

# empty content by default
_content_str="";
if [ "${#_arg_headings[@]}" -gt 0 ]; then
	_hd_level_str="";
	for (( i=0; i<_arg_headings_level; i++ )); do
		_hd_level_str+="#";
	done

	log "headings specified:" 4;
	for _hd in "${_arg_headings[@]}"; do
		log "\t$_hd" 4;
		_contents_str+="\n$_hd_level_str $_hd\n\n\n";
	done
fi

Creating the index.md file

Creating the file itself, but, damn, even after careful thoughts it looks like I'm missing something....

We'll have to come back to that later i guess...


# dont erase stuff willy nilly, but, hum, looks like I'm missing something here...
if [ ! -f "${_container_dir}/index.md" ]; then
	if [ "$_arg_template" == "" ] || [ ! -f "$_arg_template" ]; then
		if [ "$_arg_template" != "" ]; then
			log "template $_arg_template not found" -1;
		fi
		log "using default template" 4;
		_arg_template="${_SCRIPT_DIR}/../templates/article.md";
	fi
	sed \
		-e "s/{{metas}}/$_metas_str/g" \
		-e "s/{{content}}/$_contents_str/g" \
		"$_arg_template" > "${_container_dir}/index.md";
else
	die "file already exists: ${_container_dir}";
fi

befor going any further, let's test what we have so far :

./mdd article "A test article" \
  --cat my-cat \
  --cat "sub category" \
  -s "this is just a test" \
  --folder "My test article" \
  --h2 "A title for my test" --h2 "a second part to the test" --h2 "Conclusion"

Executing this command gives us the following structure :

|_ my-cat
  |_ sub category
    |_ My test article
      |_ index.md

and cat "my-cat/sub category/My test article/index.md" :

---
title: A test article
summary: this is just a test
draft: true

---

## A title for my test



## a second part to the test



## Conclusion

Well, looks like success to me ! Now let's get back to what we missed earlier !

Allow ovverride of index.md

Yeah, that's what we forgot, we need to be able to override the index.md file, so let's add a flag for that.

parseArger parse bin/article -i --flag 'force "erase if exists"'

This modify the parsing code so we just need to update the code accordingly :

this :

if [ ! -f "${_container_dir}/index.md" ]; then
	...

becomes that :

if [ "$_arg_force" == "on" ] && [ -f "${_container_dir}/index.md" ]; then 
  rm "${_container_dir}/index.md" -f; 
fi
if [ ! -f "${_container_dir}/index.md" ] || [ "$_arg_force" == "on" ]; then
	...

I do it this way if (when ?) we add a backup system ;)

Nice to haves

Let"s take care of our QoL as developers and help our future selves, let's start with documentation.

parseArger document --file ./mdd  --directory ./bin --title "MarkDown for Didi" > documentation.md

I just pipe the result too the file, je suis un geudin, faut pas me chercher !

Completion is next, and as usual for me, something crashes, the "normal" command is as follow :

parseArger completely "mdd" ./mdd --subcommand-directory ./bin

which fails for me, but I use this workaround :

parseArger completely "mdd" ./mdd --subcommand-directory ./bin --no-run-completely > completely.yaml
completely preview > completely.bash

Why does this work when otherwise fail ? \_(ツ)_/

That done, the rc file require creation, this is the content :

if [ "${MDD_DIR}" != "" ]; then
  alias mdd="${MDD_DIR}/mdd";
  [ -f "${MDD_DIR}/completely.bash" ] && source "${MDD_DIR}/completely.bash";
fi

A small detour by the readme file to fill a bit of information :

# Markdown for Didi

Markdown tools for my blog.
This is a tutorial project for [parseArger](https://github.com/DimitriGilbert/parseArger).

Tidy up !

I'll remove the form and the makefile, I don't need them for this project. And if I end up doing, their just a command away ^^

rm -f form.html Makefile

If you followed along you might have extra directory from your test, don't forget to clean them ! And we're just left with a commit to be done for now !

git add .
git commit -m "first commit"
# hub create <repo>
git push origin main

What now ?

I won't re write a heroic fantasy introduction in lieu of conclusion this time, but for real, I kinda have nothing more to teach you on parseArger for now.

As for mdd, I'll probably add some stuff to it, but I don't know what yet, so I'll just leave it there for now. bug report, feature request and PR are welcome though, I don't promise quick support but I'll sure have a look ;)

I'll probably go over a few other stuff I created before writting again about parseArger, but it'll be back, I promise !

{{% projectInteraction project="parseArger" %}}

{{% goodbye %}}