Gar

Gar 是一个 Bash 脚本程序——我写的,基于我的野生的 Bash 编程经验——,用于管理 Markdown 文档项目,可将 Markdown 文档集合其转化为 HTML 文档集合。Gar 的运行,依赖 pandoc,git,tree 以及一个能在 Shell(命令行)里打开指定网页文件的网页浏览器。Gar 默认将 Firefox 作为网页浏览器,但是可在文档项目根目录的 gar.conf 文件中指定其它符合要求的网页浏览器。

文档项目初始化

命令:gar init 文档项目名

例如:

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

以下命令可观察 gar init 创造了什么:

$ cd demo
$ ls -a
.  ..  gar.conf  .git  .gitignore  images  output  source
$ gar tree
demo
demo
├── gar.conf
├── images
├── output
└── source
$ git log
commit e2eb30a6f915a8571fe026a76febbe52ac1ab38f (HEAD -> master)
Author: xxx 
Date:   Fri Mar 12 14:28:43 2021 +0800

    init

文档项目初始化后,文档的撰写和编辑工作主要在 source 目录进行。Gar 将 Markdown 文档转化为 HTML 文档后,放在 output 子目录内。

文档的插图皆位于 images 目录,且被 Markdown 和 HTML 文档共享,亦即在 Markdown 文档中要使用相对路径插入图片,例如:


... 上文 ...

在 Markdown 文档中,推荐使用引用式插图语法:

![test][test]

... 下文 ... 

[test]: ../../images/my-programs/gar/test.png

文档项目初始化后,可打开文档项目根目录里的配置文件 gar.conf,在其中设定 Gar 默认使用的网页浏览器以及文档作者的名字。例如:

#!/bin/bash
BROWSER_FOR_GAR=firefox
AUTHOR="李磨刀"

截止到目前,gar.conf 没有其他设定。

文集创建与删除

进入 source 目录:

$ cd source

创建文集 foo:

$ gar new class foo

可使用 gar tree 查看文档项目的目录变化,观察 gar new class 命令创造了什么:

$ gar tree
demo
├── gar.conf
├── images
│   └── foo
├── output
│   └── foo
└── source
    └── foo

可一次创建多个文集:

$ gar new class a b c

结果为:

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

删除文集:

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

可在文集里创建子文集:

$ cd foo
$ gar new-class a
$ gar tree
demo
├── gar.conf
├── images
│   └── foo
│       └── a
├── output
│   └── foo
│       └── a
└── source
    └── foo
        └── a

可创建嵌套文集:

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

将上述试验复盘:

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

提示,目前工作目录仍为 source/foo。

创建和删除文档

在文集目录内,使用 gar new post 创建内容为空的文档。例如,在 source/foo 内创建 test.md 文档:

$ 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 内容如下:

---
title: 
author: 李磨刀
date: 2021 年 03 月 12 日
...

这是 pandoc 能够支持的 YAML 格式的文件头。title 的值,需要手工设定,毕竟 Gar 没法知道我要写一份什么文章。

查看一下项目的目录发生了哪些变动:

$ gar tree
demo
├── gar.conf
├── images
│   └── foo
│       └── test
├── output
│   └── foo
└── source
    └── foo
        └── test.md
$ git log
commit 91ea8d1599269ad4fdb4aae15b73d5e4cbd7a4ad (HEAD -> master)
Author: xxx 
Date:   Fri Mar 12 14:49:41 2021 +0800

    Added test.md

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

    init

每次创建文档时,Gar 会调用 git 记录文档创建历史。

可一次创建多份内容为空的文档:

$ 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
│       └── test
├── output
│   └── foo
└── source
    └── foo
        ├── a.md
        ├── b.md
        ├── c.md
        └── test.md

使用 gar remove post 可删除当前工作目录下的文档。以下命令可将上述创建的文档一举删除:

$ 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

每次删除文档,git 会记录文档的删除历史。

经过上述操作后,这个试验性的文档项目又复盘为:

$ gar tree
demo
├── gar.conf
├── images
│   └── foo
├── output
│   └── foo
└── source
    └── foo

网页的生成和预览

记住,当前的工作目录依然是 source/foo。下面的命令重新创建 test.md:

$ gar new post test.md

然后用文本编辑器打开 test.md,将其内容修改为:

---
title: Hello Gar!
author: 李磨刀
date: 2021 年 03 月 12 日
...

这只是一份无用的的示例文档。

使用 gar convert 命令可将文档 test.md 转换为网页文件 test.html:

$ gar convert test.md

查看文档项目发生的变化:

$ gar tree
demo
├── gar.conf
├── images
│   └── foo
│       └── test
├── output
│   └── foo
│       └── test.html
└── source
    └── foo
        └── test.md

倘若当前文集内有多份文档,也可以一次性将其转换为一组网页文件,例如:

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

使用 gar preview 命令,可将文档转化为网页文件,并由 Gar 默认的网页浏览器打开:

$ gar preview test.md

Gar_第1张图片

gar preview 不支持多份文档一次性转换和预览。

附录

Gar 的全部代码:

#!/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

Gar 在使用 pandoc 将 Markdown 文档转化为网页时,需要一个 CSS 文件 gar.css,其内容如下:

html {
    font-size: 16px;
    line-height: 1.8rem;
}

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: 2.3rem; }
h1 { font-size: 1.8rem; }
h2 { font-size: 1.65rem; }
h3 { font-size: 1.5em; }
h4 { font-size: 1.35rem; }
h5 { font-size: 1.2rem; }

p {
    margin: 1.3rem 0;
    text-align: justify;
}

figure {
    text-align: center;
}
figure img {
    width: 80%;
}
figure figcaption {
    font-size: 0.9rem;
}

pre {
    padding: 1rem;
    font-size: 0.9rem;
    line-height: 1.6em;
    overflow:auto;
    background: #f8f8f8;
    border: 1px solid #ccc;
    border-radius: 0.25rem;
}

code {
    color: #e83e8c;
}
pre code {
    color: #333366;
}

/* metadata */
p.author, p.date { text-align: center; margin: 0 auto;}

/* 文章里小节标题的序号与标题名称之间的间距 */
span.section-sep { margin-left: 0.5rem; margin-right: 0.5rem; }


blockquote {
    margin: 0px !important;
    border-left: 4px solid #009A61;
}

blockquote p {
    font-size: 1rem;
    line-height: 1.8rem;
    margin: 0px !important;
    text-align: justify;
    padding:0.5em;
}

上述 gar.css 并无特别之处,完全可根据自己对 css 的熟悉程度并结合需要自行定制,但是要记得将它放在 gar 脚本同一目录下。

你可能感兴趣的:(Gar)