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 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 脚本同一目录下。