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 <xxx@yyy.zzz>
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 <xxx@yyy.zzz>
Date: Fri Mar 12 14:49:41 2021 +0800
 Added test.md
commit e2eb30a6f915a8571fe026a76febbe52ac1ab38f (HEAD -> master)
Author: xxx <xxx@yyy.zzz>
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 脚本同一目录下。

    作者:garfileo原文地址:https://segmentfault.com/a/1190000039370466

    %s 个评论

    要回复文章请先登录注册