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 <[email protected]>
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 <[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

每次創建文檔時,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 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 腳本同一目錄下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章