feat(gitea): 新增 HTML 匯出與範本風格設定

What:新增 html-export 與 html-style 兩支技能、tools/html-render.sh 與 tools/html-style.sh 兩支工具,以及六種版型乘五種風格的 HTML 範本。

Why:wiki 頁與議題要拿給 Gitea 以外的人看時,只能複製 markdown;不同類型的文件也該有各自的版面,不是每份都長一樣。

How:版型(report、slide、dashboard、spec、timeline、onepager)決定內容怎麼排,風格(minimal、corporate、dark、print、vivid)決定看起來長怎樣,兩者自由搭配。哪一種頁面套哪一組由設定決定:專案的 .jsc/html-styles 優先,其次 $JSC_HOME/html-styles.conf,對不到退 DEFAULT,再對不到才用內建的 report/minimal。產出是單一 HTML 檔,CSS 與腳本全部內嵌。

Who:需要把 wiki 頁或議題寄給客戶、主管或跨團隊同事的人。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:19:25 +08:00
co-authored by Claude Opus 5
parent f8123e829b
commit a0c13f807a
17 changed files with 1021 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env sh
# html-render.sh — 把 markdown 套上版型與風格,輸出單一 HTML 檔(供 jsc-gitea:html-export 使用)。
#
# 為什麼要有這支腳本:markdown 轉 HTML 交給 Gitea 自己渲染(gitea.sh markdown),出來的排版才跟
# wiki、議題頁看到的一致;版型與風格則是固定的字串替換。兩件事都有標準輸入輸出,不必每次重寫。
#
# 用法:
# html-render.sh --markdown <檔案> --title <標題> --out <輸出檔>
# [--subtitle <副標>] [--layout <版型>] [--style <風格>]
# [--source-url <來源網址>]
#
# --layout 預設 report,--style 預設 minimal。可用清單見 html-style.sh layouts / styles。
#
# 輸出: 寫出 --out 指定的 HTML 檔,並在標準輸出印出該檔路徑。
# 結束碼: 0=成功 1=渲染或寫檔失敗 2=用法錯誤 4=找不到版型或風格範本
#
# 陷阱:
# - HTML 是單一檔案,CSS 直接內嵌,不外連任何資源;產出物常常是寄給別人看的,外連在對方那裡會破圖。
# - markdown 渲染走 Gitea API。連不上就失敗收場,不自己拼一套半套的轉換——半套轉換出來的表格
# 跟 wiki 上看到的不一樣,比失敗更難發現。
# - 渲染端點不吃 wiki 情境,`[[頁名]]` 這種 wiki 內部連結會原樣留著。呼叫端要先換成絕對網址,
# 產出的 HTML 才連得回去。
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
TPL="$plugin_root/templates/html"
GITEA="$script_dir/gitea.sh"
usage() {
cat >&2 <<'EOF'
用法:
html-render.sh --markdown <檔案> --title <標題> --out <輸出檔>
[--subtitle <副標>] [--layout <版型>] [--style <風格>]
[--source-url <來源網址>]
結束碼: 0=成功 1=渲染或寫檔失敗 2=用法錯誤 4=找不到版型或風格範本
EOF
exit 2
}
md=''; title=''; out=''; subtitle=''; layout=report; style=minimal; source_url=''
while [ "$#" -gt 0 ]; do
case "$1" in
--markdown) [ "$#" -ge 2 ] || usage; md="$2"; shift 2 ;;
--title) [ "$#" -ge 2 ] || usage; title="$2"; shift 2 ;;
--out) [ "$#" -ge 2 ] || usage; out="$2"; shift 2 ;;
--subtitle) [ "$#" -ge 2 ] || usage; subtitle="$2"; shift 2 ;;
--layout) [ "$#" -ge 2 ] || usage; layout="$2"; shift 2 ;;
--style) [ "$#" -ge 2 ] || usage; style="$2"; shift 2 ;;
--source-url) [ "$#" -ge 2 ] || usage; source_url="$2"; shift 2 ;;
*) echo "[jsc][HTML 產生][ERR]:不認得的選項「$1」。" >&2; usage ;;
esac
done
[ -n "$md" ] && [ -n "$title" ] && [ -n "$out" ] || usage
[ -f "$md" ] || { echo "[jsc][HTML 產生][ERR]:找不到 markdown 檔「$md」。" >&2; exit 2; }
layout_file="$TPL/layout/$layout.html"
style_file="$TPL/style/$style.css"
[ -f "$layout_file" ] || { echo "[jsc][HTML 產生][ERR]:找不到版型範本「$layout_file」。" >&2; exit 4; }
[ -f "$style_file" ] || { echo "[jsc][HTML 產生][ERR]:找不到風格範本「$style_file」。" >&2; exit 4; }
# markdown -> HTML 片段:交給 Gitea 自己渲染,排版才跟站上一致。
fragment=$(mktemp)
trap 'rm -f "$fragment"' EXIT
if ! "$GITEA" markdown "$md" > "$fragment" 2>/dev/null || [ ! -s "$fragment" ]; then
echo '[jsc][HTML 產生][ERR]:Gitea 的 markdown 渲染失敗,這次不出檔。請確認 GITEA_HOST 與權杖後重跑。' >&2
exit 1
fi
python3 - "$layout_file" "$style_file" "$fragment" "$out" "$title" "$subtitle" "$source_url" "$layout" "$style" "$TPL" <<'PY'
import html, sys, datetime, os
layout_file, style_file, frag_file, out_file, title, subtitle, source_url, layout, style, tpl_dir = sys.argv[1:11]
def read(p):
with open(p, encoding='utf-8') as f:
return f.read()
page = read(layout_file)
css = read(style_file)
base_css = read(os.path.join(tpl_dir, 'base.css'))
base_js = read(os.path.join(tpl_dir, 'base.js'))
content = read(frag_file)
generated = datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M')
source_html = ''
if source_url:
safe = html.escape(source_url, quote=True)
source_html = '<a class="source" href="%s" rel="noreferrer">來源:%s</a>' % (safe, safe)
for key, value in (
('{{TITLE}}', html.escape(title)),
('{{SUBTITLE}}', html.escape(subtitle)),
('{{BASE}}', base_css),
('{{BASE_JS}}', base_js),
('{{STYLE}}', css),
('{{CONTENT}}', content),
('{{SOURCE}}', source_html),
('{{GENERATED}}', generated),
('{{LAYOUT}}', html.escape(layout)),
('{{STYLE_NAME}}', html.escape(style)),
):
page = page.replace(key, value)
with open(out_file, 'w', encoding='utf-8') as f:
f.write(page)
PY
printf '%s\n' "$out"
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env sh
# html-style.sh — 「哪一種 wiki 頁或議題,用哪一種版型與風格出 HTML」的設定(供 jsc-gitea:html-style、html-export 使用)。
#
# 設定格式(一行一筆):{種類}={版型},{風格}
# 種類:WIKI:{頁名前綴}(例 WIKI:PLAN)、ISSUE:{標籤名}(例 ISSUE:bug)、DEFAULT(都對不到時用)
# 「#」開頭為註解,空白行忽略。同一種類出現多行時取最後一行。
# 解析順序:
# 1. 目前工作目錄的 ./.jsc/html-styles(專案覆寫)
# 2. $JSC_HOME/html-styles.conf(JSC_HOME 預設 ~/.jsc)
# 3. 種類對不到就退 DEFAULT,DEFAULT 也沒有才用內建預設 report,minimal
#
# 用法:
# html-style.sh get <種類> 印出 版型<TAB>風格<TAB>來源(project/global/default/builtin)
# html-style.sh set <種類> <版型> <風格> [--project|--global] 寫入設定(預設 --global)
# html-style.sh unset <種類> [--project|--global] 移除設定
# html-style.sh list 印出合併後的所有設定:種類<TAB>版型<TAB>風格<TAB>來源
# html-style.sh layouts 列出可用版型:名稱<TAB>繁中說明
# html-style.sh styles 列出可用風格:名稱<TAB>繁中說明
#
# 結束碼: 0=成功 2=用法錯誤 4=版型或風格沒有對應範本檔
#
# 陷阱:
# - get 永遠印得出一組值:對不到就退 DEFAULT,再對不到就退內建預設。呼叫端不必自己準備退路,
# 但要看第三欄,才知道這組值是使用者設的還是撿來的。
# - set 會先確認範本檔真的存在,擋掉打錯字的版型或風格;設定寫得進去、出圖卻失敗最難查。
set -u
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
TPL="$plugin_root/templates/html"
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
PROJECT_FILE=./.jsc/html-styles
GLOBAL_FILE="$JSC_HOME/html-styles.conf"
BUILTIN_LAYOUT=report
BUILTIN_STYLE=minimal
usage() {
cat >&2 <<'EOF'
用法:
html-style.sh get <種類>
html-style.sh set <種類> <版型> <風格> [--project|--global]
html-style.sh unset <種類> [--project|--global]
html-style.sh list | layouts | styles
種類: WIKI:{頁名前綴}、ISSUE:{標籤名}、DEFAULT
結束碼: 0=成功 2=用法錯誤 4=版型或風格沒有對應範本檔
EOF
exit 2
}
read_value() { # $1=設定檔 $2=種類 -> 「版型,風格」
[ -f "$1" ] || return 0
awk -F= -v want="$2" '
{ sub(/\r$/, "") }
/^[ \t]*#/ { next }
/^[ \t]*$/ { next }
index($0, "=") == 0 { next }
{
key = $1
gsub(/[ \t]/, "", key)
if (key != want) next
val = substr($0, index($0, "=") + 1)
gsub(/[ \t]/, "", val)
if (val != "") v = val
}
END { if (v != "") print v }
' "$1"
}
resolve() { # $1=種類 -> 版型<TAB>風格<TAB>來源
for _f in "$PROJECT_FILE:project" "$GLOBAL_FILE:global"; do
_file=${_f%:*}; _src=${_f##*:}
_v=$(read_value "$_file" "$1")
if [ -n "$_v" ]; then
printf '%s\t%s\t%s\n' "${_v%%,*}" "${_v##*,}" "$_src"
return 0
fi
done
if [ "$1" != DEFAULT ]; then
_d=$(resolve DEFAULT)
_src=$(printf '%s' "$_d" | cut -f3)
# DEFAULT 自己也沒設定時,來源照實說是 builtin,不要蓋成 default
[ "$_src" = builtin ] || _src=default
printf '%s\t%s\n' "$(printf '%s' "$_d" | cut -f1,2)" "$_src"
return 0
fi
printf '%s\t%s\tbuiltin\n' "$BUILTIN_LAYOUT" "$BUILTIN_STYLE"
}
# 範本檔第一行註解就是繁中說明:版型放在 <!-- 說明 -->,風格放在 /* 說明 */。
describe() { # $1=檔案
head -n1 "$1" 2>/dev/null | sed 's/<!--[[:space:]]*//; s/[[:space:]]*-->//; s|/\*[[:space:]]*||; s|[[:space:]]*\*/||'
}
list_layouts() {
for f in "$TPL"/layout/*.html; do
[ -f "$f" ] || continue
name=$(basename "$f" .html)
printf '%s\t%s\n' "$name" "$(describe "$f")"
done
}
list_styles() {
for f in "$TPL"/style/*.css; do
[ -f "$f" ] || continue
name=$(basename "$f" .css)
printf '%s\t%s\n' "$name" "$(describe "$f")"
done
}
write_kv() { # $1=檔案 $2=種類 $3=值
dir=$(dirname "$1")
mkdir -p "$dir" || { echo "[jsc][HTML 設定][ERR]:建不出目錄「$dir」。" >&2; exit 1; }
tmp="$1.tmp.$$"
{ [ -f "$1" ] && grep -v "^[[:space:]]*$2[[:space:]]*=" "$1" || true; } > "$tmp"
[ -n "$3" ] && printf '%s=%s\n' "$2" "$3" >> "$tmp"
mv "$tmp" "$1"
}
cmd="${1:-}"; [ -n "$cmd" ] || usage
shift || true
case "$cmd" in
layouts) list_layouts; exit 0 ;;
styles) list_styles; exit 0 ;;
list)
keys=$( { [ -f "$PROJECT_FILE" ] && cut -d= -f1 "$PROJECT_FILE" || true
[ -f "$GLOBAL_FILE" ] && cut -d= -f1 "$GLOBAL_FILE" || true; } \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | grep -v '^#' | grep -v '^$' | sort -u)
[ -n "$keys" ] || { printf 'DEFAULT\t%s\t%s\tbuiltin\n' "$BUILTIN_LAYOUT" "$BUILTIN_STYLE"; exit 0; }
printf '%s\n' "$keys" | while IFS= read -r k; do
printf '%s\t%s\n' "$k" "$(resolve "$k")"
done
exit 0 ;;
get)
key="${1:-}"; [ -n "$key" ] || usage
resolve "$key"
exit 0 ;;
set)
key="${1:-}"; layout="${2:-}"; style="${3:-}"
[ -n "$key" ] && [ -n "$layout" ] && [ -n "$style" ] || usage
shift 3
target="$GLOBAL_FILE"; scope=global
case "${1:-}" in
--project) target="$PROJECT_FILE"; scope=project ;;
--global|'') ;;
*) usage ;;
esac
[ -f "$TPL/layout/$layout.html" ] || {
echo "[jsc][HTML 設定][ERR]:沒有版型「$layout」。可用:$(list_layouts | cut -f1 | tr '\n' ' ')" >&2; exit 4; }
[ -f "$TPL/style/$style.css" ] || {
echo "[jsc][HTML 設定][ERR]:沒有風格「$style」。可用:$(list_styles | cut -f1 | tr '\n' ' ')" >&2; exit 4; }
write_kv "$target" "$key" "$layout,$style"
printf '已寫入 %s:%s=%s,%s(%s)\n' "$target" "$key" "$layout" "$style" "$scope"
exit 0 ;;
unset)
key="${1:-}"; [ -n "$key" ] || usage
shift
target="$GLOBAL_FILE"
case "${1:-}" in
--project) target="$PROJECT_FILE" ;;
--global|'') ;;
*) usage ;;
esac
[ -f "$target" ] || { echo "找不到設定檔:$target" >&2; exit 0; }
write_kv "$target" "$key" ''
printf '已移除 %s 的 %s\n' "$target" "$key"
exit 0 ;;
*) usage ;;
esac