From f8123e829b61995e3c10cfbed16b41ceb17f6700 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 26 Aug 2026 09:19:08 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(gitea):=20=E6=96=B0=E5=A2=9E=20wiki=20?= =?UTF-8?q?=E8=BD=89=E8=AD=B0=E9=A1=8C=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 wiki-to-issue 技能,把一頁 wiki 轉成同一個存取庫的議題;配套新增 tools/gitea-link.sh 解析連結、tools/issue.sh 讀寫議題。 Why:wiki 頁要變成可追蹤的工作時,原本得自己拼 API:組 JSON、查標籤 id、挑回應欄位,跳脫一錯就把對外的議題內容寫壞。 How:連結是唯一入口,gitea-link.sh 解不出 wiki 頁就中止,不猜存取庫也不猜頁名;標籤只從既有的挑;站台沒有專案看板 API 時據實回報請使用者手動拖,不假裝關聯成功。 Who:使用 jsc 技能組、需要把 wiki 內容交辦出去的人。 Co-Authored-By: Claude Opus 5 --- skills/wiki-to-issue/SKILL.md | 23 +++++ tools/gitea-link.sh | 77 +++++++++++++++++ tools/issue.sh | 157 ++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 skills/wiki-to-issue/SKILL.md create mode 100755 tools/gitea-link.sh create mode 100755 tools/issue.sh diff --git a/skills/wiki-to-issue/SKILL.md b/skills/wiki-to-issue/SKILL.md new file mode 100644 index 0000000..24dc6b0 --- /dev/null +++ b/skills/wiki-to-issue/SKILL.md @@ -0,0 +1,23 @@ +--- +name: wiki-to-issue +description: Turn one Gitea wiki page into an issue in the same repository. Parse the link with tools/gitea-link.sh, read the page through jsc-gitea:wiki, draft title and body as a sub agent, then create the issue with tools/issue.sh - labels picked from the repository's existing set per the jsc-ask decision tree, project board attached or reported as manual when the site has no board API. A request without a wiki link stops the skill immediately: never guess the repository or the page. Use when a wiki page has to become trackable work; not for issues drafted from scratch, and not for syncing an issue that already exists. +--- + +# wiki-to-issue — a wiki page becomes an issue + +The wiki link is the only input. Everything else — repository, page name, host — comes out of that link. + +## Steps + +1. **Link gate.** Run `tools/gitea-link.sh parse {url}` on the link the user gave. Exit 3, no link in the request, or `kind=issue` (this skill reads wiki pages, not issues) all mean the same thing: **stop and report which one it was**. Never ask for a repository name instead, and never fall back to the working directory's remote — a page written into the wrong repository's issue tracker is public and hard to take back. Completion condition: `kind=wiki`, and `repo`, `page` and `host` are known. +2. Read the page with `jsc-gitea:wiki` (`wiki-get {repo} {page}`). Exit 4 means the page does not exist — stop and report the page name. Take the page's absolute URL from `wiki-url` in the same pass; it goes into the issue body. Completion condition: the page's markdown and its absolute URL are both in hand. +3. **Draft the issue — this step MUST run as a sub agent.** Title: the page's first heading, or the page name when it has none. Body: the page content in Traditional Chinese, opening with a 「來源:{絕對網址}」 line so the issue points back at the wiki. Convert `[[display|page]]` links to absolute URLs (`wiki-url`), because `[[...]]` resolves only inside a wiki. Drop personal data — an issue is read by more people than a wiki page. Completion condition: title and body file exist, the body carries the source line, and no `[[...]]` link is left in it. +4. **Labels come from what the repository already has.** Run `tools/issue.sh labels {repo}`, propose the fitting ones with a reason each, and confirm per `jsc-ask:ask` rules — every option states its impact scope (a label drives filters and board rules, so a wrong one routes the work to the wrong queue). Turn the confirmed names into ids with `tools/issue.sh label-ids`. An empty label list, or nothing fitting: ask whether to create the issue with no label, and record that answer. **Never invent a label that the repository does not have.** Completion condition: the user has confirmed a label set — possibly empty — and its ids are resolved. +5. **Project board.** Run `tools/issue.sh projects {repo}`. Exit 3 means this Gitea has no board API: say so plainly, and hand the user the board URL the script printed so they can drag the issue in themselves. A board list comes back: let the user pick one per `jsc-ask:ask` rules, attach it, and report the failure verbatim if the attach call is refused. Completion condition: the issue is either attached to a board, or the report states in one line that the board link is still outstanding and who has to do it. +6. Create the issue: `tools/issue.sh create {repo} {title} {body-file} [--labels {ids}]`. Report the `index=` and `url=` it prints. Completion condition: the issue URL is reported to the user, together with the labels applied and the board status from step 5. + +## Rules + +- One wiki page, one issue. Splitting a page into several issues is analysis work, not conversion — hand that to `jsc-sdlc:analyze`. +- The issue body stays Traditional Chinese per the STE100 rule, and keeps the source line at the top. +- Creating an issue is an outward-facing action: the title, body, labels and board pick are confirmed with the user before the create call, never after. diff --git a/tools/gitea-link.sh b/tools/gitea-link.sh new file mode 100755 index 0000000..c5e0cc4 --- /dev/null +++ b/tools/gitea-link.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env sh +# gitea-link.sh — 解析 Gitea 的 wiki 頁或議題連結(供 jsc-gitea:wiki-to-issue、html-export 使用)。 +# +# 為什麼要有這支腳本:兩支技能都以「一條連結」為唯一入口,沒有連結就中斷。連結長什麼樣、 +# 哪一段是存取庫、哪一段是頁名,是固定的字串規則,交給模型每次自己拆,拆錯就寫到別的存取庫去。 +# +# 用法: +# gitea-link.sh parse 解析連結,印出可直接 eval 的欄位 +# +# parse 輸出(每行一個 key=value,值已加單引號): +# kind='wiki' repo='owner/repo' page='PAGE_NAME' host='https://gitea.example' +# kind='issue' repo='owner/repo' index='12' host='https://gitea.example' +# +# 認得的形式: +# https://host/{owner}/{repo}/wiki/{page} (page 可含 %XX 編碼,會還原) +# https://host/{owner}/{repo}/wiki/{page}/_edit (尾巴的 _edit、_new 會去掉) +# https://host/{owner}/{repo}/issues/{index} +# https://host/{owner}/{repo}/issues/{index}#issuecomment-123 +# +# 結束碼: 0=解析成功 2=用法錯誤 3=不是認得的 wiki 或議題連結 +# +# 陷阱: +# - 結束碼 3 由呼叫端當成「沒有連結」處理,直接中斷流程,不要退回去猜存取庫或頁名。 +# - 只解析連結本身,不打網路,也不檢查頁面存不存在;那是 wiki-get 與 issue.sh 的事。 +set -eu + +usage() { + echo 'usage: gitea-link.sh parse ' >&2 + exit 2 +} + +[ "$#" -eq 2 ] || usage +[ "$1" = parse ] || usage +url="$2" +[ -n "$url" ] || usage + +python3 - "$url" <<'PY' +import re, sys +from urllib.parse import urlsplit, unquote + +url = sys.argv[1].strip() +if not re.match(r'^https?://', url): + print('[jsc][連結解析][ERR]:連結要以 http:// 或 https:// 開頭。', file=sys.stderr) + sys.exit(3) + +u = urlsplit(url) +host = f'{u.scheme}://{u.netloc}' +parts = [p for p in u.path.split('/') if p] +if len(parts) < 4: + print('[jsc][連結解析][ERR]:認不出存取庫與頁面,需要 /{owner}/{repo}/wiki/{page} 或 /{owner}/{repo}/issues/{index}。', file=sys.stderr) + sys.exit(3) + +owner, repo, kind = parts[0], parts[1], parts[2] +rest = parts[3:] + +def out(**kv): + for k, v in kv.items(): + print(f"{k}='{str(v)}'") + +if kind == 'wiki': + while rest and rest[-1] in ('_edit', '_new', '_pages'): + rest.pop() + if not rest: + print('[jsc][連結解析][ERR]:wiki 連結少了頁名。', file=sys.stderr) + sys.exit(3) + page = unquote('/'.join(rest)) + out(kind='wiki', repo=f'{owner}/{repo}', page=page, host=host) +elif kind == 'issues': + idx = rest[0].split('#')[0] + if not idx.isdigit(): + print('[jsc][連結解析][ERR]:議題編號不是數字。', file=sys.stderr) + sys.exit(3) + out(kind='issue', repo=f'{owner}/{repo}', index=idx, host=host) +else: + print(f'[jsc][連結解析][ERR]:只認得 wiki 與 issues 連結,收到「{kind}」。', file=sys.stderr) + sys.exit(3) +PY diff --git a/tools/issue.sh b/tools/issue.sh new file mode 100755 index 0000000..2fab5cd --- /dev/null +++ b/tools/issue.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh +# issue.sh — Gitea 議題的讀取與建立(供 jsc-gitea:wiki-to-issue、html-export 使用)。 +# +# 為什麼要有這支腳本:建議題要組 JSON、要把標籤名換成標籤 id、要把回應裡的編號與網址挑出來, +# 全是固定的輸入輸出。交給模型每次自己拼 curl,跳脫字元一錯就把議題內容寫壞,而議題是對外的。 +# +# 用法: +# issue.sh labels / 列出既有標籤:idname +# issue.sh label-ids / [,] 標籤名換成 id(逗號分隔);有名字對不到就 exit 4 +# issue.sh projects / 列出專案看板:idtitle;站台沒有這個 API 就 exit 3 +# issue.sh title / 印出議題標題 +# issue.sh body / 印出議題正文(markdown) +# issue.sh labels-of / 印出議題目前的標籤名,一行一個 +# issue.sh create / <body-file> [--labels <id>[,<id>]] [--milestone <id>] +# 建立議題。輸出兩行: index=<編號>、url=<議題網址> +# +# 環境變數: 同 gitea.sh(GITEA_HOST、GITEA_TOKEN;token 缺或被拒時退回 tea 登入金鑰)。 +# 結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援該 API 4=名稱對不到 id +# +# 陷阱: +# - 正文一律用檔案傳進來,內容維持 UTF-8 與真實換行;不要在參數裡塞 \n。 +# - Gitea 1.27 沒有公開的專案看板 API,projects 會 exit 3。呼叫端要據實回報「請手動把議題拖到看板」, +# 不要假裝已經關聯好——關聯不上跟關聯成功看起來一樣,是最容易被當成完成的一種失敗。 +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +GITEA="$script_dir/gitea.sh" + +usage() { + cat >&2 <<'EOF' +用法: + issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name + issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id + issue.sh projects <owner>/<repo> 列出專案看板;站台不支援就 exit 3 + issue.sh title|body|labels-of <owner>/<repo> <index> + issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>] +結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援 4=名稱對不到 id +EOF + exit 2 +} + +[ -f "$GITEA" ] || { echo "[jsc][議題][ERR]:找不到 $GITEA。" >&2; exit 1; } + +valid_repo() { + case "${1:-}" in + */*/*|/*|*/) return 1 ;; + */*) return 0 ;; + *) return 1 ;; + esac +} + +cmd="${1:-}"; [ -n "$cmd" ] || usage +shift || true +repo="${1:-}" +valid_repo "$repo" || { echo "[jsc][議題][ERR]:存取庫須為 {owner}/{repo},收到「${repo:-空值}」。" >&2; exit 2; } +shift + +case "$cmd" in + labels) + "$GITEA" api GET "/repos/$repo/labels?limit=100" | python3 -c ' +import json,sys +for l in json.load(sys.stdin): + print("%s\t%s" % (l["id"], l["name"])) +' ;; + + label-ids) + names="${1:-}"; [ -n "$names" ] || usage + "$GITEA" api GET "/repos/$repo/labels?limit=100" | python3 -c ' +import json,sys +want=[n.strip() for n in sys.argv[1].split(",") if n.strip()] +have={l["name"]: l["id"] for l in json.load(sys.stdin)} +miss=[n for n in want if n not in have] +if miss: + sys.stderr.write("[jsc][議題][ERR]:這些標籤在存取庫裡找不到:" + "、".join(miss) + "\n") + sys.exit(4) +print(",".join(str(have[n]) for n in want)) +' "$names" ;; + + projects) + if out=$("$GITEA" api GET "/repos/$repo/projects" 2>/dev/null); then + printf '%s' "$out" | python3 -c ' +import json,sys +try: + d=json.load(sys.stdin) +except Exception: + sys.exit(3) +if not isinstance(d, list): + sys.exit(3) +for p in d: + print("%s\t%s" % (p.get("id",""), p.get("title") or p.get("name",""))) +' || exit 3 + else + host="${GITEA_HOST:-}" + case "$host" in http://*|https://*) : ;; *) host="https://$host" ;; esac + echo "[jsc][議題][WARN]:這個 Gitea 站台沒有專案看板 API,程式關聯不了。請開 ${host%/}/$repo/projects 手動把議題拖進看板。" >&2 + exit 3 + fi ;; + + title) + idx="${1:-}"; [ -n "$idx" ] || usage + "$GITEA" api GET "/repos/$repo/issues/$idx" | python3 -c ' +import json,sys +print(json.load(sys.stdin).get("title","")) +' ;; + + body) + idx="${1:-}"; [ -n "$idx" ] || usage + "$GITEA" api GET "/repos/$repo/issues/$idx" | python3 -c ' +import json,sys +sys.stdout.write(json.load(sys.stdin).get("body","") or "") +' ;; + + labels-of) + idx="${1:-}"; [ -n "$idx" ] || usage + "$GITEA" api GET "/repos/$repo/issues/$idx" | python3 -c ' +import json,sys +for l in json.load(sys.stdin).get("labels") or []: + print(l.get("name","")) +' ;; + + create) + title="${1:-}"; body_file="${2:-}" + [ -n "$title" ] || usage + [ -n "$body_file" ] || usage + [ -f "$body_file" ] || { echo "[jsc][議題][ERR]:找不到正文檔「$body_file」。" >&2; exit 2; } + shift 2 + labels='' + milestone='' + while [ "$#" -gt 0 ]; do + case "$1" in + --labels) [ "$#" -ge 2 ] || usage; labels="$2"; shift 2 ;; + --milestone) [ "$#" -ge 2 ] || usage; milestone="$2"; shift 2 ;; + *) echo "[jsc][議題][ERR]:不認得的選項「$1」。" >&2; usage ;; + esac + done + payload=$(mktemp) + trap 'rm -f "$payload"' EXIT + python3 -c ' +import json,sys +title, body_file, labels, milestone, out = sys.argv[1:6] +d={"title": title, "body": open(body_file, encoding="utf-8").read()} +if labels: + d["labels"]=[int(x) for x in labels.split(",") if x.strip()] +if milestone: + d["milestone"]=int(milestone) +with open(out, "w", encoding="utf-8") as f: + json.dump(d, f, ensure_ascii=False) +' "$title" "$body_file" "$labels" "$milestone" "$payload" + "$GITEA" api POST "/repos/$repo/issues" "$payload" | python3 -c ' +import json,sys +d=json.load(sys.stdin) +print("index=%s" % (d.get("number") or d.get("index") or "")) +print("url=%s" % (d.get("html_url") or "")) +' ;; + + *) usage ;; +esac From a0c13f807a727f4087a327d161286deefaf42a5a Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 26 Aug 2026 09:19:25 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(gitea):=20=E6=96=B0=E5=A2=9E=20HTML=20?= =?UTF-8?q?=E5=8C=AF=E5=87=BA=E8=88=87=E7=AF=84=E6=9C=AC=E9=A2=A8=E6=A0=BC?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- skills/html-export/SKILL.md | 28 +++++ skills/html-style/SKILL.md | 23 ++++ templates/html/base.css | 131 +++++++++++++++++++++ templates/html/base.js | 63 ++++++++++ templates/html/layout/dashboard.html | 52 +++++++++ templates/html/layout/onepager.html | 46 ++++++++ templates/html/layout/report.html | 60 ++++++++++ templates/html/layout/slide.html | 90 ++++++++++++++ templates/html/layout/spec.html | 58 +++++++++ templates/html/layout/timeline.html | 61 ++++++++++ templates/html/style/corporate.css | 23 ++++ templates/html/style/dark.css | 21 ++++ templates/html/style/minimal.css | 21 ++++ templates/html/style/print.css | 30 +++++ templates/html/style/vivid.css | 34 ++++++ tools/html-render.sh | 111 ++++++++++++++++++ tools/html-style.sh | 169 +++++++++++++++++++++++++++ 17 files changed, 1021 insertions(+) create mode 100644 skills/html-export/SKILL.md create mode 100644 skills/html-style/SKILL.md create mode 100644 templates/html/base.css create mode 100644 templates/html/base.js create mode 100644 templates/html/layout/dashboard.html create mode 100644 templates/html/layout/onepager.html create mode 100644 templates/html/layout/report.html create mode 100644 templates/html/layout/slide.html create mode 100644 templates/html/layout/spec.html create mode 100644 templates/html/layout/timeline.html create mode 100644 templates/html/style/corporate.css create mode 100644 templates/html/style/dark.css create mode 100644 templates/html/style/minimal.css create mode 100644 templates/html/style/print.css create mode 100644 templates/html/style/vivid.css create mode 100755 tools/html-render.sh create mode 100755 tools/html-style.sh diff --git a/skills/html-export/SKILL.md b/skills/html-export/SKILL.md new file mode 100644 index 0000000..ec85277 --- /dev/null +++ b/skills/html-export/SKILL.md @@ -0,0 +1,28 @@ +--- +name: html-export +description: Export one Gitea wiki page or issue as a single self-contained HTML file. Parse the link with tools/gitea-link.sh, resolve that kind's layout and style through tools/html-style.sh (kind, then DEFAULT, then the built-in report/minimal), then render with tools/html-render.sh, which puts the markdown through Gitea's own renderer and inlines every asset. A request without a wiki or issue link stops the skill immediately: never guess the repository, the page or the issue number. Use when a page or issue has to leave Gitea as a document; not for choosing which template a kind uses - that is jsc-gitea:html-style. +--- + +# html-export — a wiki page or issue becomes one HTML file + +The link is the only input. The output is one file that opens anywhere, with no external asset. + +## Steps + +1. **Link gate.** Run `tools/gitea-link.sh parse {url}`. Exit 3 or no link in the request: **stop and report it**. Never fall back to the working directory's remote or to a page name the user mentioned in passing. Completion condition: `kind` is `wiki` or `issue`, and `repo` plus `page` or `index` are known. +2. **Work out the kind key** — it decides which template applies: + - wiki page → `WIKI:{prefix}`, where the prefix is the page name up to the first underscore (`ANALYZE_D3F1A2B0` → `WIKI:ANALYZE`; a page with no underscore uses the whole name). + - issue → run `tools/issue.sh labels-of {repo} {index}` and try `ISSUE:{label}` for each label in order; the first one `tools/html-style.sh get` answers with source `project` or `global` wins. No label matches: use `ISSUE:DEFAULT`. + + Completion condition: exactly one kind key is chosen, and you can say which label or prefix produced it. +3. Run `tools/html-style.sh get {key}`. It always prints `layout<TAB>style<TAB>source`. **Read the third column and report it**: `project` or `global` means the user configured this kind; `default` means it fell back to the DEFAULT row; `builtin` means nothing is configured at all and `report`/`minimal` was used. For `default` and `builtin`, tell the user in one line that `jsc-gitea:html-style` can set this kind's own template. Completion condition: layout, style and source are reported before anything is rendered. +4. Fetch the content: `jsc-gitea:wiki` `wiki-get` for a page, or `tools/issue.sh title` plus `tools/issue.sh body` for an issue. Exit 4 (page missing) or an API failure stops the skill with the page name or issue number in the report. Completion condition: the markdown and the document title are in hand. +5. **Prepare the markdown — this step MUST run as a sub agent.** Convert `[[display|page]]` wiki links to absolute URLs from `wiki-url`; the renderer does not resolve them, so they would ship as literal brackets. Strip personal data — an exported file travels further than the page it came from. Leave everything else exactly as written; this step never rewrites the content. Completion condition: no `[[...]]` remains, and the diff against the source is limited to link conversion and personal-data removal. +6. Ask per `jsc-ask:ask` rules where the file goes, proposing `./.jsc/html/{page-or-issue}.html`. State the impact scope: a path inside a repository gets committed unless it is ignored. Completion condition: the user has confirmed one output path. +7. Render: `tools/html-render.sh --markdown {file} --title {title} --layout {layout} --style {style} --source-url {absolute URL} --out {path}`. Exit 1 means Gitea's renderer failed — report it and stop, with no half-rendered file left behind. Completion condition: the file exists, and the report names its path, the layout, the style and where that pair came from. + +## Rules + +- One link, one file. Batch export is a loop the caller runs, not something this skill decides on its own. +- The rendered file inlines CSS and scripts on purpose: it is usually sent to someone outside Gitea, and an external asset breaks on their machine. +- The layout and style are never chosen by inspecting the content. The configuration decides, and `jsc-gitea:html-style` owns the configuration. diff --git a/skills/html-style/SKILL.md b/skills/html-style/SKILL.md new file mode 100644 index 0000000..424bddd --- /dev/null +++ b/skills/html-style/SKILL.md @@ -0,0 +1,23 @@ +--- +name: html-style +description: Set which HTML layout and style a kind of Gitea wiki page or issue gets when jsc-gitea:html-export renders it. Offer all six layouts from tools/html-style.sh layouts and all five styles from tools/html-style.sh styles as decision-tree options per jsc-ask, then write the pair with tools/html-style.sh set - either to the project file .jsc/html-styles or to the global $JSC_HOME/html-styles.conf. Use when a kind of page or issue should come out looking different, or when the export reported source builtin or default; not for rendering a page, which is jsc-gitea:html-export. +--- + +# html-style — which template a kind of page gets + +One kind of page, one layout, one style. `jsc-gitea:html-export` reads what this skill writes. + +## Steps + +1. **Settle the kind key.** Show the current configuration with `tools/html-style.sh list` first, then ask per `jsc-ask:ask` rules which kind this run sets. The three shapes are fixed: `WIKI:{page-name prefix}` (`WIKI:PLAN`, `WIKI:ANALYZE`, `WIKI:LOG` …), `ISSUE:{label name}` (`ISSUE:bug`), and `DEFAULT` for everything that matches nothing else. Every option states its impact scope — `DEFAULT` changes every kind that has no row of its own. Completion condition: exactly one key is agreed, and its current value from `tools/html-style.sh get {key}` has been read back with its source column. +2. **Pick the layout — offer all six.** Run `tools/html-style.sh layouts`; it prints each name with its Traditional Chinese description, taken from the template file itself. Present all six as options per `jsc-ask:ask` rules, each with what it does to the content (`report` builds a table of contents beside the text, `slide` turns every `##` into a keyboard-flipped page, `dashboard` turns them into cards, `spec` freezes table headers, `timeline` strings them along a line, `onepager` narrows everything into one printable page). Completion condition: the user has picked one layout name that the script listed. +3. **Pick the style — offer all five.** Run `tools/html-style.sh styles` and present every one it prints (`minimal`, `corporate`, `dark`, `print`, `vivid`) with its description. Never trim the list to a shortlist: the point of this skill is that the user sees the whole set. Completion condition: the user has picked one style name that the script listed. +4. **Pick the scope.** Ask per `jsc-ask:ask` rules: `--project` writes `./.jsc/html-styles`, which only applies inside this working directory and is committed with the repository; `--global` writes `$JSC_HOME/html-styles.conf`, which follows the user across every project on this machine. State that the project file wins whenever both hold the same key. Completion condition: the user has picked one scope. +5. Write it: `tools/html-style.sh set {key} {layout} {style} [--project|--global]`. Exit 4 means the name is not one of the listed templates — go back to step 2 or 3 rather than editing the file by hand. Completion condition: the script exits 0 and prints the file it wrote. +6. Read it back with `tools/html-style.sh get {key}` and report the resolved layout, style and source. Completion condition: the source column shows `project` or `global`, matching the scope chosen in step 4. + +## Rules + +- Only names the script listed may be written. A key holding a template that does not exist fails at export time, long after the mistake was made. +- Removing a row is `tools/html-style.sh unset {key} [--project|--global]`; after that the kind falls back to `DEFAULT`, then to the built-in `report`/`minimal`. +- New layouts live in `templates/html/layout/{name}.html` and new styles in `templates/html/style/{name}.css`, each starting with a one-line Traditional Chinese comment — that comment is what the option list shows. diff --git a/templates/html/base.css b/templates/html/base.css new file mode 100644 index 0000000..3694166 --- /dev/null +++ b/templates/html/base.css @@ -0,0 +1,131 @@ +/* 共用排版:所有版型與風格都吃這一份,顏色與字型一律走 CSS 變數,由風格檔決定。 */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font); + font-size: 16px; + line-height: 1.75; + -webkit-text-size-adjust: 100%; +} + +.page-head { + padding: 2.5rem 0 1.5rem; + border-bottom: 2px solid var(--accent); +} + +.page-head h1 { + margin: 0 0 .35rem; + font-family: var(--font-head); + font-size: 2rem; + line-height: 1.3; + color: var(--head-fg); +} + +.subtitle { margin: 0 0 .5rem; color: var(--muted); font-size: 1.05rem; } +.subtitle:empty { display: none; } + +.meta { + margin: 0; + color: var(--muted); + font-size: .85rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.meta a.source { color: var(--accent); text-decoration: none; word-break: break-all; } +.meta a.source:hover { text-decoration: underline; } + +.content { padding: 1.5rem 0 3rem; } + +.content h1, .content h2, .content h3, .content h4 { + font-family: var(--font-head); + color: var(--head-fg); + line-height: 1.35; + margin: 2rem 0 .75rem; +} + +.content h1 { font-size: 1.7rem; } +.content h2 { font-size: 1.4rem; } +.content h3 { font-size: 1.15rem; } +.content h4 { font-size: 1rem; } + +.content p { margin: 0 0 1rem; } +.content ul, .content ol { margin: 0 0 1rem; padding-left: 1.5rem; } +.content li { margin: .25rem 0; } +.content li input[type="checkbox"] { margin-right: .4rem; } + +.content a { color: var(--accent); text-decoration: none; } +.content a:hover { text-decoration: underline; } + +.content blockquote { + margin: 1rem 0; + padding: .6rem 1rem; + border-left: 4px solid var(--accent); + background: var(--card); + color: var(--muted); +} + +.content code { + font-family: var(--font-mono); + font-size: .9em; + background: var(--code-bg); + padding: .15em .4em; + border-radius: 4px; +} + +.content pre { + margin: 0 0 1rem; + padding: 1rem; + overflow-x: auto; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.content pre code { background: none; padding: 0; } + +/* 表格一律可橫向捲動,寬表格不會把整頁撐開 */ +.table-wrap { overflow-x: auto; margin: 0 0 1.25rem; } + +.content table { + border-collapse: collapse; + width: 100%; + font-size: .95rem; +} + +.content th, .content td { + border: 1px solid var(--border); + padding: .5rem .7rem; + text-align: left; + vertical-align: top; +} + +.content th { background: var(--table-head-bg); color: var(--head-fg); font-weight: 600; } +.content tbody tr:nth-child(even) { background: var(--stripe); } + +.content img { max-width: 100%; height: auto; } +.content hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; } + +.page-foot { + padding: 1.25rem 0 2rem; + border-top: 1px solid var(--border); + color: var(--muted); + font-size: .8rem; + display: flex; + flex-wrap: wrap; + gap: .75rem; + justify-content: space-between; +} + +.badge { + display: inline-block; + padding: .1rem .5rem; + border: 1px solid var(--border); + border-radius: 999px; + font-size: .75rem; + color: var(--muted); +} diff --git a/templates/html/base.js b/templates/html/base.js new file mode 100644 index 0000000..8d8e22c --- /dev/null +++ b/templates/html/base.js @@ -0,0 +1,63 @@ +// 共用行為:表格加捲動外框、依 h2 切段、產生目錄。各版型自己決定要用哪幾個。 +(function (w) { + 'use strict'; + + // 寬表格包一層可橫捲的外框,整頁就不會被撐開。 + function wrapTables(root) { + root.querySelectorAll('table').forEach(function (t) { + if (t.parentElement && t.parentElement.classList.contains('table-wrap')) return; + var box = document.createElement('div'); + box.className = 'table-wrap'; + t.parentNode.insertBefore(box, t); + box.appendChild(t); + }); + } + + // 依 h2 把內容切成一段一段。h2 之前的內容自成第一段(前言)。 + function splitBySection(root) { + var nodes = Array.prototype.slice.call(root.childNodes); + var sections = []; + var current = null; + + function open(headingText) { + current = document.createElement('section'); + current.className = 'jsc-section'; + current.dataset.title = headingText || ''; + sections.push(current); + } + + nodes.forEach(function (node) { + if (node.nodeType === 1 && node.tagName === 'H2') { + open(node.textContent.trim()); + } else if (!current) { + if (node.nodeType === 3 && !node.textContent.trim()) return; + open(''); + } + current.appendChild(node); + }); + + root.innerHTML = ''; + sections.forEach(function (s) { root.appendChild(s); }); + return sections; + } + + // 依 h2、h3 產生目錄,塞進指定容器。標題沒有 id 就補一個。 + function buildToc(root, target) { + var heads = root.querySelectorAll('h2, h3'); + if (!heads.length) { target.remove(); return; } + var list = document.createElement('ul'); + heads.forEach(function (h, i) { + if (!h.id) h.id = 'sec-' + (i + 1); + var li = document.createElement('li'); + li.className = 'toc-' + h.tagName.toLowerCase(); + var a = document.createElement('a'); + a.href = '#' + h.id; + a.textContent = h.textContent.trim(); + li.appendChild(a); + list.appendChild(li); + }); + target.appendChild(list); + } + + w.jsc = { wrapTables: wrapTables, splitBySection: splitBySection, buildToc: buildToc }; +})(window); diff --git a/templates/html/layout/dashboard.html b/templates/html/layout/dashboard.html new file mode 100644 index 0000000..434fbce --- /dev/null +++ b/templates/html/layout/dashboard.html @@ -0,0 +1,52 @@ +<!-- 看板:每個 h2 一張卡片並排,適合進度、狀態、維護清單 --> +<!doctype html> +<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/onepager.html b/templates/html/layout/onepager.html new file mode 100644 index 0000000..8fad63a --- /dev/null +++ b/templates/html/layout/onepager.html @@ -0,0 +1,46 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/report.html b/templates/html/layout/report.html new file mode 100644 index 0000000..5abd440 --- /dev/null +++ b/templates/html/layout/report.html @@ -0,0 +1,60 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
+ +
{{CONTENT}}
+
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/slide.html b/templates/html/layout/slide.html new file mode 100644 index 0000000..c2ced90 --- /dev/null +++ b/templates/html/layout/slide.html @@ -0,0 +1,90 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ + + {{LAYOUT}}/{{STYLE_NAME}} + +
+
+ + + + diff --git a/templates/html/layout/spec.html b/templates/html/layout/spec.html new file mode 100644 index 0000000..5b6dd1d --- /dev/null +++ b/templates/html/layout/spec.html @@ -0,0 +1,58 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
+ +
{{CONTENT}}
+
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/timeline.html b/templates/html/layout/timeline.html new file mode 100644 index 0000000..18965a9 --- /dev/null +++ b/templates/html/layout/timeline.html @@ -0,0 +1,61 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/style/corporate.css b/templates/html/style/corporate.css new file mode 100644 index 0000000..dcac7ea --- /dev/null +++ b/templates/html/style/corporate.css @@ -0,0 +1,23 @@ +/* 商務:深藍主色、表頭反白、正式對外用 */ +:root { + --bg: #ffffff; + --fg: #22272e; + --head-fg: #0b3358; + --muted: #5c6b7a; + --accent: #0b5fa5; + --border: #c9d6e2; + --card: #eef4fa; + --code-bg: #eef2f6; + --table-head-bg: #0b3358; + --stripe: #f4f8fc; + --radius: 6px; + --shadow: 0 1px 2px rgba(11, 51, 88, .12); + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.page-head { border-bottom-width: 3px; } +.content th { color: #ffffff; letter-spacing: .03em; } +.content h2 { border-left: 5px solid var(--accent); padding-left: .6rem; } +.content table { box-shadow: var(--shadow); } diff --git a/templates/html/style/dark.css b/templates/html/style/dark.css new file mode 100644 index 0000000..bcdf929 --- /dev/null +++ b/templates/html/style/dark.css @@ -0,0 +1,21 @@ +/* 深色:深底亮字,長時間閱讀與投影機環境 */ +:root { + --bg: #11161c; + --fg: #d7dee6; + --head-fg: #f2f6fa; + --muted: #8b98a6; + --accent: #56a8f5; + --border: #2b3540; + --card: #1a2129; + --code-bg: #1c242d; + --table-head-bg: #1f2932; + --stripe: #161d24; + --radius: 6px; + --shadow: 0 1px 3px rgba(0, 0, 0, .5); + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.content pre { border-color: #2b3540; } +.content h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; } diff --git a/templates/html/style/minimal.css b/templates/html/style/minimal.css new file mode 100644 index 0000000..c492018 --- /dev/null +++ b/templates/html/style/minimal.css @@ -0,0 +1,21 @@ +/* 極簡:白底、細線、無襯線,資訊密度優先 */ +:root { + --bg: #ffffff; + --fg: #1f2328; + --head-fg: #0d1117; + --muted: #6a737d; + --accent: #2f6f9f; + --border: #d8dee4; + --card: #f6f8fa; + --code-bg: #f2f4f7; + --table-head-bg: #f6f8fa; + --stripe: #fbfcfd; + --radius: 4px; + --shadow: none; + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.page-head { border-bottom-width: 1px; } +.content h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; } diff --git a/templates/html/style/print.css b/templates/html/style/print.css new file mode 100644 index 0000000..a404b4a --- /dev/null +++ b/templates/html/style/print.css @@ -0,0 +1,30 @@ +/* 印刷:襯線字、A4 邊界、去掉陰影,列印或轉 PDF 用 */ +:root { + --bg: #ffffff; + --fg: #1b1b1b; + --head-fg: #000000; + --muted: #55555f; + --accent: #4a4a4a; + --border: #b8b8b8; + --card: #f4f4f2; + --code-bg: #f2f2f0; + --table-head-bg: #ececeb; + --stripe: #f9f9f8; + --radius: 0; + --shadow: none; + --font: "Noto Serif TC", "Songti TC", "PMingLiU", Georgia, serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", Consolas, monospace; +} + +@page { size: A4; margin: 20mm 18mm; } + +body { font-size: 15px; line-height: 1.85; } +.page-head { border-bottom: 1px solid var(--border); } +.content h2 { page-break-after: avoid; } +.content table, .content pre, .content blockquote { page-break-inside: avoid; } + +@media print { + .meta a.source { color: var(--fg); } + .page-foot { border-top: 1px solid var(--border); } +} diff --git a/templates/html/style/vivid.css b/templates/html/style/vivid.css new file mode 100644 index 0000000..c63e2f9 --- /dev/null +++ b/templates/html/style/vivid.css @@ -0,0 +1,34 @@ +/* 明亮:高彩度、圓角卡片、漸層標題,簡報與對內宣達用 */ +:root { + --bg: #fdfbff; + --fg: #241f2e; + --head-fg: #4c1d95; + --muted: #6d6480; + --accent: #7c3aed; + --border: #e2d9f5; + --card: #f6f1ff; + --code-bg: #f1ecfd; + --table-head-bg: #ede4ff; + --stripe: #faf7ff; + --radius: 12px; + --shadow: 0 2px 10px rgba(124, 58, 237, .12); + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.page-head { + border-bottom: 0; + background: linear-gradient(135deg, #7c3aed 0%, #e0378f 100%); + color: #ffffff; + border-radius: var(--radius); + padding: 2rem 1.5rem; + box-shadow: var(--shadow); +} + +.page-head h1, .page-head .subtitle, .page-head .meta { color: #ffffff; } +.page-head .meta a.source { color: #ffffff; text-decoration: underline; } + +.content h2 { border-left: 6px solid var(--accent); padding-left: .6rem; } +.content table { border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); } +.content blockquote { border-radius: var(--radius); } diff --git a/tools/html-render.sh b/tools/html-render.sh new file mode 100755 index 0000000..f96bea0 --- /dev/null +++ b/tools/html-render.sh @@ -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 = '來源:%s' % (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" diff --git a/tools/html-style.sh b/tools/html-style.sh new file mode 100755 index 0000000..af20e74 --- /dev/null +++ b/tools/html-style.sh @@ -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 <種類> 印出 版型風格來源(project/global/default/builtin) +# html-style.sh set <種類> <版型> <風格> [--project|--global] 寫入設定(預設 --global) +# html-style.sh unset <種類> [--project|--global] 移除設定 +# html-style.sh list 印出合併後的所有設定:種類版型風格來源 +# html-style.sh layouts 列出可用版型:名稱繁中說明 +# html-style.sh styles 列出可用風格:名稱繁中說明 +# +# 結束碼: 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=種類 -> 版型風格來源 + 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///; 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 From dc17907554d84d8612a1d5385d7677191c1a5787 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 26 Aug 2026 09:19:25 +0800 Subject: [PATCH 3/4] =?UTF-8?q?chore(gitea):=20markdown=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=AD=90=E5=91=BD=E4=BB=A4=E8=88=87=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E3=80=81manifest=20=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:gitea.sh 新增 markdown 子命令與可切換的 Content-Type,README 補上新工具、新技能與 HTML 範本說明,三份 manifest 同步升版到 0.1.3。 Why:markdown 轉 HTML 要交給 Gitea 自己渲染,排版才跟 wiki、議題頁一致;但 /markdown 端點在 Gitea 1.27 回 200 卻是空內容,看起來像成功。 How:改走吃純文字的 /markdown/raw,req 送出的 Content-Type 改由 REQ_CONTENT_TYPE 決定,預設仍是 application/json。代價寫進註解:raw 端點不吃 wiki 情境,[[頁名]] 要由呼叫端先換成絕對網址。 Who:所有透過 jsc-gitea 產生文件的技能。 Co-Authored-By: Claude Opus 5 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- README.md | 49 ++++++++++++++++++++++++++++++++++++++ plugin.json | 2 +- tools/gitea.sh | 17 +++++++++++-- 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 844ddc8..2c5ea67 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.2", + "version": "0.1.3", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 4657e0b..8363c08 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.2", + "version": "0.1.3", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills" } diff --git a/README.md b/README.md index 4aba7df..8e68ee6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ gitea.sh pr-comments / # 印出所有留言(issue gitea.sh pr-depend / / # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 gitea.sh repo-set / [website] # 設定 repo 描述與網頁 +gitea.sh markdown # markdown 檔渲染成 HTML 片段(走 /markdown/raw) gitea.sh api [json-file] hash-id # 與 gitea.sh hash-id 相同 repo-sync.sh / [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因} @@ -46,10 +47,46 @@ repo-sync.sh / [target-dir] # 同步單一存取庫;印 check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則 ``` +議題與 HTML 產出: + +``` +gitea-link.sh parse # 解析 wiki 或議題連結;不是這兩種就 exit 3(呼叫端據此中止) +issue.sh labels|label-ids|projects / +issue.sh title|body|labels-of / +issue.sh create / <body-file> [--labels <ids>] [--milestone <id>] +html-style.sh get|set|unset|list|layouts|styles # 種類對版型與風格的設定 +html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> [--layout] [--style] [--subtitle] [--source-url] +``` + ## 參考資料 - `references/wiki-links.md`:寫 wiki 頁才需要的連結規則。同類型用 `[[顯示文字|頁名]]`(顯示文字在左),跨類型用 `wiki-url` 給的絕對網址。 +## HTML 範本 + +版型(`templates/html/layout/*.html`)決定內容怎麼排,風格(`templates/html/style/*.css`)決定看起來長怎樣。兩者自由搭配,六乘五共三十種。 + +| 版型 | 內容排法 | +| --- | --- | +| `report` | 左側目錄加章節內文,長文件用 | +| `slide` | 一個 `##` 一張投影片,鍵盤左右鍵翻頁 | +| `dashboard` | 每個 `##` 一張卡片並排 | +| `spec` | 表格表頭固定、程式碼區塊放大,API 文件用 | +| `timeline` | 每個 `##` 一個節點串成一條線 | +| `onepager` | 窄欄單頁,印出來剛好一頁 | + +| 風格 | 視覺 | +| --- | --- | +| `minimal` | 白底細線、無襯線,資訊密度優先 | +| `corporate` | 深藍主色、表頭反白,正式對外 | +| `dark` | 深底亮字 | +| `print` | 襯線字、A4 邊界,列印或轉 PDF | +| `vivid` | 高彩度、圓角卡片、漸層標題 | + +哪一種頁面套哪一組,由 `html-style.sh` 的設定決定:專案的 `./.jsc/html-styles` 優先,其次 `$JSC_HOME/html-styles.conf`,種類對不到就退 `DEFAULT`,再對不到才用內建的 `report`/`minimal`。設定的 key 是 `WIKI:{頁名前綴}`、`ISSUE:{標籤名}` 或 `DEFAULT`。 + +自訂範本:版型放進 `templates/html/layout/`,風格放進 `templates/html/style/`,檔案第一行寫一句繁中說明——那句話就是技能問使用者時顯示的選項說明。版型檔可用的佔位有 `{{TITLE}}`、`{{SUBTITLE}}`、`{{CONTENT}}`、`{{BASE}}`、`{{BASE_JS}}`、`{{STYLE}}`、`{{SOURCE}}`、`{{GENERATED}}`、`{{LAYOUT}}`、`{{STYLE_NAME}}`。 + ## Skills 目錄 呼叫方式:Claude / Antigravity `/jsc-gitea:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。 @@ -64,6 +101,18 @@ Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZ 存取庫批次同步:列出 owner → 使用者選擇 → 逐 repo(sub agent)呼叫 `tools/repo-sync.sh` clone 或更新;回報 `dirty {分支}` 的存取庫交給 `jsc-git:pr`,base 直接用腳本帶出來的那個分支。 +### `html-export` + +把一頁 wiki 或一筆議題輸出成單一 HTML 檔:解析連結 → 判斷種類 → 查該種類的版型與風格 → 用 Gitea 自己的 markdown 渲染出圖。CSS 與腳本全部內嵌,檔案拿到哪裡都打得開。**沒有連結就直接中止**,不猜存取庫、不猜頁名、不猜議題編號。 + +### `html-style` + +設定「哪一種 wiki 頁或議題,出 HTML 時用哪一種版型與風格」:六種版型與五種風格全部列給使用者選,再寫進專案的 `.jsc/html-styles` 或全域的 `$JSC_HOME/html-styles.conf`。`html-export` 讀的就是這份設定。 + +### `wiki-to-issue` + +把一頁 wiki 轉成同一個存取庫的議題:讀頁面 → sub agent 起草標題與正文(開頭附來源連結)→ 從既有標籤挑合適的 → 關聯專案看板 → 建立議題。標籤只從存取庫既有的挑,不自己發明;站台沒有看板 API 時據實回報請使用者手動拖,不假裝關聯成功。**沒有連結就直接中止**。 + <!-- JSC-SKILLS:END --> ## 環境變數 diff --git a/plugin.json b/plugin.json index 3c54f99..0b089b1 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.2", + "version": "0.1.3", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills/" } diff --git a/tools/gitea.sh b/tools/gitea.sh index 45bf432..f311640 100755 --- a/tools/gitea.sh +++ b/tools/gitea.sh @@ -19,6 +19,7 @@ # gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index> # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 # gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁 +# gitea.sh markdown <file> # markdown 檔渲染成 HTML 片段(走 /markdown/raw) # gitea.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始) # 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN # GITEA_TOKEN 未設定,或請求遇 401/403 時,自動退回 tea CLI 的登入 token @@ -95,13 +96,16 @@ fi case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac API="${HOST%/}/api/v1" -req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次) +req() { # METHOD path [body-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次) + # 送出的 Content-Type 由 REQ_CONTENT_TYPE 決定,預設 application/json; + # /markdown/raw 這種吃純文字的端點要先改成 text/plain 再呼叫。 method="$1"; path="$2"; body_file="${3:-}" + ctype="${REQ_CONTENT_TYPE:-application/json}" retried=0 while :; do if [ -n "$body_file" ]; then out=$(curl -sS -w '\n%{http_code}' -X "$method" \ - -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ + -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: $ctype" \ --data-binary "@$body_file" "$API$path") else out=$(curl -sS -w '\n%{http_code}' -X "$method" \ @@ -279,6 +283,15 @@ print(json.dumps(b)) fi rm -f "$tmp" echo "OK $or" ;; + markdown) + # markdown 檔 -> HTML 片段。走 /markdown/raw(吃純文字)而不是 /markdown: + # 後者在 Gitea 1.27 回 200 但內容是空的,看起來像成功,其實什麼都沒渲染。 + # 代價:raw 端點不吃 context,wiki 的 [[頁名]] 內部連結不會變成連結, + # 呼叫端要先把它換成絕對網址。 + file="${1:?markdown file}" + [ -f "$file" ] || { echo "找不到 markdown 檔: $file" >&2; exit 2; } + REQ_CONTENT_TYPE='text/plain' + req POST "/markdown/raw" "$file" ;; api) method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}" req "$method" "$path" $body ;; From dbe800d7a7c9928a0241b7e4a06a48f1ff48d8f8 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 26 Aug 2026 09:22:00 +0800 Subject: [PATCH 4/4] =?UTF-8?q?chore(gitea):=20manifest=20=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E8=A3=9C=E4=B8=8A=E8=AD=B0=E9=A1=8C=E8=BD=89=E6=8F=9B?= =?UTF-8?q?=E8=88=87=20HTML=20=E5=8C=AF=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:三份 plugin manifest 的 description 補上這次新增的兩類能力。 Why:description 是各 CLI 顯示這個 plugin 用途的依據,少了新能力就找不到人用。 How:三份同步改成同一句;marketplace 正本的條目描述留給下一次 skill-check 統一處理,因為那份要同時改到十一個存取庫的副本。 Who:在 CLI 裡挑 plugin 的人。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2c5ea67..9dbc225 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jsc-gitea", "version": "0.1.3", - "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", + "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "author": { "name": "JSC" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8363c08..d6a7060 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", "version": "0.1.3", - "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", + "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 0b089b1..312ee78 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", "version": "0.1.3", - "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", + "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills/" }