GAR is a Bash script — I wrote it, based on my wild Bash programming experience — that manages Markdown document projects and converts Markdown document collections into HTML document collections. GAR works by pandoc, git, tree, and a web browser that opens the specified web file in the Shell (command line). GAR defaults to Firefox as a Web browser, but you can specify other eligible Web browsers in the gar.conf file at the root of the document project.

Initialize the document project

Command: gar init document project name

Such as:

$ gar init demo
[master (root-commit) 6f7dd1c] init
 1 file changed, 2 insertions(+)
 create mode 100644 gar.conf

To see what Gar init has created, follow the following command:

$ cd demo
$ ls -a
.  ..  gar.conf  .git  .gitignore  images  output  source
$Gar Tree Demo Demo - Gar. Conf Heavy Metal Exercises - Images Heavy Metal Exercises - Output Trunk - Source
$ git log
commit e2eb30a6f915a8571fe026a76febbe52ac1ab38f (HEAD -> master)
Author: xxx <[email protected]>
Date:   Fri Mar 12 14:28:43 2021 +0800

    init

After the document project is initialized, the writing and editing of the document is mainly carried out in the source directory. After GAR converts a Markdown document to an HTML document, it is placed in the output subdirectory.

Documents’ illustrations are located in the images directory and are shared between Markdown and HTML documents. This means that relative paths are used to insert images in Markdown documents, for example:

. The above... Referential illustration syntax is recommended in Markdown documentation:! [test][test] ... Below... [test]: .. /.. /images/my-programs/gar/test.png

Once the document project is initialized, you can open the configuration file gar.conf in the root of the document project and set the default web browser for gar. and the name of the author of the document. Such as:

#! /bin/bash BROWSER_FOR_GAR= Firefox AUTHOR=" Li Hua"

As of now, there are no other Settings for gar.conf.

Collection creation and deletion

Enter the source directory:

$ cd source

Create foo:

$ gar new class foo

You can use GAR Tree to view directory changes for document items and see what the GAR New Class command creates:

$Gar Tree Demo - - Gar. Conf Heavy Metal Exercises ── Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises

You can create more than one corpus at a time:

$ gar new class a b c

The result is:

$gar tree demo ├ ─ ─ gar. Conf ├ ─ ─ images │ ├ ─ ─ a │ ├ ─ ─ b │ ├ ─ ─ c │ └ ─ ─ foo ├ ─ ─ the output │ ├ ─ ─ a │ ├ ─ ─ b │ └ ─ ─ c └ ─ ─ the source ─ A Heavy School ─ B Heavy School ─ C Heavy School ─ Foo

Delete corpus:

$gar remove class a b c $gar tree demo - - gar.conf - images │ ├── foo ├── output ├── foo ├── ─ source ├─ foo

You can create a subcorpus in the corpus:

$CD Foo $Gar New - Class A $Gar Tree Demo - └ ├── Foo │ ├── A ├── Output ├── Foo │ ├── A ├── Source sigma ─ sigma ─ foo sigma ─ a

You can create a nested corpus:

$gar new class b/c/d/e/f $gar tree demo ├ ─ ─ gar. Conf ├ ─ ─ images │ └ ─ ─ foo │ ├ ─ ─ a │ └ ─ ─ b │ └ ─ ─ c │ └ ─ ─ d │ └ ─ ─ e │ └ ─ ─ f ├ ─ ─ the output │ └ ─ ─ foo │ ├ ─ ─ a │ └ ─ ─ b │ └ ─ ─ c │ └ ─ ─ d │ └ ─ ─ e │ └ ─ ─ f └ ─ ─ source └ ─ ─ foo ├ ─ ─ a └ ─ ─ b └ ─ ─ c └ ─ ─ d └ ─ ─ e └ ─ ─ f

Repeat the above tests:

$gar remove class a b $gar tree demo - - gar.conf - images │ ├── foo ├── output ├── foo ├── ─ source ├─ foo

Note that the working directory is still source/foo.

Create and delete documents

Within the corpus directory, use GAR New Post to create a document with empty content. For example, create a test.md document in source/foo:

$ gar new post test.md
[master 6a894eb] Added test.md
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 source/foo/test.md

Test. md has the following contents:

-- Title: Author: Li Modao Date: March 12, 2021...

This is the YAML file header that Pandoc can support. The title value needs to be set manually. After all, GAR doesn’t know what article I’m going to write.

See what has happened to the project’s catalog:

$Gar Tree Demo - - Gar. Conf └ ├── Foo ├─ Test ─ Output ├── Foo ├── Source ├── Foo ├── Test.md
$ git log
commit 91ea8d1599269ad4fdb4aae15b73d5e4cbd7a4ad (HEAD -> master)
Author: xxx <[email protected]>
Date:   Fri Mar 12 14:49:41 2021 +0800

    Added test.md

commit e2eb30a6f915a8571fe026a76febbe52ac1ab38f (HEAD -> master)
Author: xxx <[email protected]>
Date:   Fri Mar 12 14:28:43 2021 +0800

    init

Each time a document is created, GAR calls Git to record the document creation history.

Can create more than one empty document at a time:

$ gar new post a.md b.md c.md
[master 25e7d65] Added a.md b.md c.md
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 source/foo/a.md
 create mode 100644 source/foo/b.md
 create mode 100644 source/foo/c.md
$gar tree demo ├ ─ ─ gar. Conf ├ ─ ─ images │ └ ─ ─ foo │ ├ ─ ─ a │ ├ ─ ─ b │ ├ ─ ─ c │ └ ─ ─ the test ├ ─ ─ the output │ └ ─ ─ foo └ ─ ─ source └ ─ ─ Foo ─ A.MD Heavy Exercises ─ B.MD Heavy Exercises ─ C.MD Heavy Exercises ─ Test

Use GAR Remove POST to delete documents in the current working directory. The following command can delete the document created above in one fell swoop:

$ gar remove post test.md a.md b.md c.md [master 3684217] Remove test.md a.md b.md c.md 4 files changed, 24 deletions(-) delete mode 100644 source/foo/a.md delete mode 100644 source/foo/b.md delete mode 100644 source/foo/c.md  delete mode 100644 source/foo/test.md

Each time you delete a document, Git keeps track of the document’s deletion history.

After doing this, the pilot document project is reformatted as:

$Gar Tree Demo - - Gar. Conf Heavy Metal Exercises ── Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises - - Bass Exercises

Page generation and preview

Remember that the current working directory is still source/foo. Recreate test.md with the following command:

$ gar new post test.md

Then open test.md in a text editor and change its contents to:

--- title: Hello Gar! Author: Li Modao Date: March 12, 2021... This is just a useless sample document.

Use the gar convert command to convert a document test.md to a web file test. HTML:

$ gar convert test.md

To view the changes in the document project:

$gar tree demo ├ ─ ─ gar. Conf ├ ─ ─ images │ └ ─ ─ foo │ └ ─ ─ the test ├ ─ ─ the output │ └ ─ ─ foo │ └ ─ ─ test. The HTML └ ─ ─ source └ ─ ─ foo └ ─ ─  test.md

If there are multiple documents in the current corpus, you can also convert them to a set of web files at once, for example:

$ gar convert test.md a.md b.md c.md

Use the GAR Preview command to convert a document to a web file and open it in the GAR default web browser:

$ gar preview test.md

GAR Preview does not support converting and previewing multiple documents at once.

The appendix

GAR’s full code:

#!/usr/bin/env bash

SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GAR_CONF=gar.conf
GAR_CSS=gar.css
SOURCE=source
IMAGES=images
OUTPUT=output
GAR_ALL=("$SOURCE" "$IMAGES" "$OUTPUT")

function error_msg {
    echo "$1"; exit -1
}

function check_argument {
    if [ -z "$1" ]; then error_msg "$2"; fi
}

function gar_commit {
    gar_goto_root
    git add .
    git commit -a -m "$1"
}

function gar_goto_root {
    if [ "$(pwd)" = "/" ]
    then
        erroe_msg "gar.conf Not found!"
    elif [ -e "$GAR_CONF" ]
    then
        return 0
    else
        cd ../
        gar_goto_root
    fi
}

function gar_is_not_workspace {
    local current_path="$(pwd)"
    gar_goto_root
    local root_path="$(pwd)"
    local relative_path=$(realpath "$current_path" \
                                   --relative-to="$(pwd)")
    if [ "${relative_path#"$SOURCE"}" = "$relative_path" ]
    then
        echo "true"
    else
        echo "false"
    fi
}

function gar_shortcut {
    if [ -z "$1" ]
    then
        local current_path="$(pwd)"
    else
        local current_path="$1"
    fi
    gar_goto_root
    local short_path=$(realpath "$current_path" \
                                --relative-to="$(pwd)/$SOURCE")
    if [ "$short_path" = "." ]
    then
        echo ""
    else
        echo "$short_path"
    fi
}

# 给文件添加 pandoc 支持的 YAML metadata
function gar_init_post {
    local MARK="$(pwd)"
    gar_goto_root
    source "$GAR_CONF"
    cd "$MARK"
    local DATE="$(date +"%Y 年 %m 月 %d 日")"
    echo -e "---\ntitle: \nauthor: $AUTHOR\ndate: $DATE\n...\n" > "$1"
}

function gar_markdown_to_html {
    CURRENT_PATH="$(pwd)"
    gar_goto_root
    local css_path="$(realpath "./" --relative-to="$OUTPUT/$1")"
    pandoc "$SOURCE/$1/$2" -s --mathjax \
           -c "$css_path/$GAR_CSS" --highlight-style pygments \
           -o "$OUTPUT/$1/${2%.*}.html"
    cd "$CURRENT_PATH"
}

function gar_init {
    check_argument "$1" "You should tell me the name of the project!"
    mkdir "$1"
    cd "$1"
    echo "BROWSER_FOR_GAR=firefox" > $GAR_CONF
    mkdir $SOURCE $OUTPUT $IMAGES
    cat "$SCRIPT_PATH/gar.css" > $GAR_CSS
    git init -q
    touch .gitignore
    for i in ".gitignore" "gar.css" "$OUTPUT" "$IMAGES"
    do
        echo "$i" >> .gitignore
    done
    gar_commit "init"
}

function gar_new {
    if [ "$(gar_is_not_workspace)" = "true" ]
    then
        error_msg "This is not workspace!"
    fi
    case $1 in
        class)
            check_argument "$2" "Tell me the name of the class!"
            for i in "${@:2}"
            do
                local CURRENT_PATH="$(pwd)"
                local CLASS="$(gar_shortcut "$CURRENT_PATH")"
                for j in "${GAR_ALL[@]}"
                do
                    gar_goto_root
                    cd "$j/$CLASS" && mkdir -p "$i"
                done
                cd "$CURRENT_PATH"
            done
        ;;
        post)
            check_argument "$2" "Tell me the name of the post!"
            for i in "${@:2}"
            do
                gar_init_post "$i"
                local CURRENT_PATH="$(pwd)"
                local POST="$(gar_shortcut "$CURRENT_PATH/$i")"
                gar_goto_root
                mkdir -p "$IMAGES/${POST%.*}"
                cd "$CURRENT_PATH"
            done
            gar_commit "Added ${*:2}"
            ;;
        *)
            error_msg "I do not understand you!"
            ;;
    esac
}

function gar_remove {
    if [ "$(gar_is_not_workspace)" = "true" ]
    then
        error_msg "This is not workspace!"
    fi
    local CURRENT_PATH="$(pwd)"
    case $1 in
        class)
            check_argument "$2" "Tell me the name of the class!"
            for i in "${@:2}"
            do
                local CLASS="$(gar_shortcut "$CURRENT_PATH/$i")"
                for j in "${GAR_ALL[@]}"
                do
                    gar_goto_root
                    cd "$j" && rm -rf "$CLASS"
                done
                cd "$CURRENT_PATH"
            done
            gar_commit "Remove ${*:2}"
        ;;
        post)
            check_argument "$2" "Tell me the name of the post!"
            for i in "${@:2}"
            do
                rm -f "$i"
                local POST="$(gar_shortcut "$CURRENT_PATH/$i")"
                gar_goto_root
                rm -rf "$IMAGES/${POST%.*}"
                rm -f "$OUTPUT/${POST%.*}.html"
                cd $CURRENT_PATH
            done
            gar_commit "Remove ${*:2}"
            ;;
        *)
            error_msg "I do not understand you!"
            ;;
    esac
}

function gar_rename {
    if [ "$(gar_is_not_workspace)" = "true" ]
    then
        error_msg "This is not workspace!"
    fi
    local CURRENT_PATH="$(pwd)"
    case $1 in
        class)
            check_argument "$2" "Tell me the name of the class!"
            if [ ! -d "$2" ]
            then
                error_msg "The class not found!"
            fi
            check_argument "$3" "Tell me the new name of the class!"
            local CLASS="$(gar_shortcut "$CURRENT_PATH")"
            for i in "${GAR_ALL[@]}"
            do
                gar_goto_root
                cd "$i/$CLASS" && mv "$2" "$3"
            done
            gar_commit "$2 -> $3"
            ;;
        post)
            check_argument "$2" "Tell me the name of the post!"
            if [ ! -e "$2" ]
            then
                error_msg "The post not found!"
            fi
            check_argument "$3" "Tell me the new name of the post!"
            mv "$2" "$3"

            local CLASS="$(dirname "$(gar_shortcut "$(pwd)/$2")")"
            gar_goto_root && cd "$IMAGES/$CLASS" && mv "${2%.*}" "${3%.*}"
            gar_goto_root && cd "$OUTPUT/$CLASS"
            if [ -e "${2%.*}.html" ]
            then
                mv "${2%.*}.html" "${3%.*}.html"
            fi
            gar_commit "$2 -> $3"
            ;;
        *)
            error_msg "I do not understand you!"
            ;;
    esac
}

function gar_convert {
    if [ "$(gar_is_not_workspace)" = "true" ]
    then
        error_msg "This is not workspace!"
    fi
    check_argument "$1" "Tell me the name of the post!"
    for i in "${@:1}"
    do
        local CLASS="$(gar_shortcut "$CURRENT_PATH")"
        gar_markdown_to_html "$CLASS" "$i"
    done
    gar_commit "Modified ${*:2}"
}

function gar_preview {
    if [ "$(gar_is_not_workspace)" = "true" ]
    then
        error_msg "This is not workspace!"
    fi
    check_argument "$1" "You should tell me the name of the post!"
    local CLASS="$(dirname "$(gar_shortcut "$(pwd)/$1")")"
    gar_markdown_to_html "$CLASS" "$1"

    gar_goto_root && source $GAR_CONF
    $BROWSER_FOR_GAR "$OUTPUT/$CLASS/${1%.*}.html"
}

# 选项:
case $1 in
    init) gar_init "$2" ;;
    new)  gar_new "${@:2}" ;;
    remove) gar_remove "${@:2}" ;;
    rename) gar_rename "${@:2}" ;;
    convert) gar_convert "${@:2}" ;;
    preview) gar_preview "$2" ;;
    tree)
        gar_goto_root
        GAR_ROOT="$(basename "$(pwd)")"
        case $2 in
            source) tree "$SOURCE" ;;
            output) tree "$OUTPUT" ;;
            images) tree "$IMAGES" ;;
            *) cd .. && tree "$GAR_ROOT" ;;
        esac
        ;;
    *)
      error_msg "I do not understand you!"
      ;;
esac

When GAR uses pandoc to convert Markdown documents to web pages, it needs a CSS file, gar.css, which reads as follows:

html { font-size: 16px; The line - height: 1.8 rem; } body { margin: 0 auto; max-width: 50rem; padding: 50px; hyphens: auto; word-wrap: break-word; font-kerning: normal; } header { text-align: center; margin-bottom: 4rem; } h1, h2, h3, h4, h5 { margin-top: 2rem; margin-bottom: 2rem; color: #d35400; } h1.title {font-size: 1.rem; font-size: 1.rem; } h1 {font-size: 1.REM; } h2 {font-size: 1.65rem; } h3 {font-size: 1.5em; } h4 {font-size: 1.35rem; } h5 {font-size: 1.2rem; } p {margin: 1.6rem 0; text-align: justify; } figure { text-align: center; } figure img { width: 80%; } Figure FigureCaption {font-size: 14.0pt; font-size: 14.0pt; } pre { padding: 1rem; The font - size: 0.9 rem; The line - height: 1.6 em. overflow:auto; background: #f8f8f8; border: 1px solid #ccc; Border - the radius: 0.25 rem; } code { color: #e83e8c; } pre code { color: #333366; } /* metadata */ p.author, p.date { text-align: center; margin: 0 auto; } /* The number of sections */ span. Section-sep {margin-left: 0.5rem; margin-left: 0.5rem; Margin - right: 0.5 rem; } blockquote { margin: 0px ! important; border-left: 4px solid #009A61; } blockquote p { font-size: 1rem; The line - height: 1.8 rem; margin: 0px ! important; text-align: justify; Padding: 0.5 em. }

There is nothing special about gar.css above, and you can customize it according to your familiarity with CSS and your needs, but be sure to place it in the same directory as your gar script.