Merge pull request 'gitea 0.1.3 發佈:wiki 轉議題、HTML 匯出與範本風格設定' (#19) from develop into master
Reviewed-on: #19 Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #19.
This commit is contained in:
@@ -43,7 +43,7 @@
|
|||||||
"source": "url",
|
"source": "url",
|
||||||
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
||||||
},
|
},
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "jsc-log",
|
"name": "jsc-log",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"source": "url",
|
"source": "url",
|
||||||
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
||||||
},
|
},
|
||||||
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
|
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "jsc-sdlc",
|
"name": "jsc-sdlc",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"source": "url",
|
"source": "url",
|
||||||
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
||||||
},
|
},
|
||||||
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
|
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"source": "url",
|
"source": "url",
|
||||||
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
||||||
},
|
},
|
||||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
|
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "jsc-log",
|
"name": "jsc-log",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"source": "url",
|
"source": "url",
|
||||||
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
||||||
},
|
},
|
||||||
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
|
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "jsc-sdlc",
|
"name": "jsc-sdlc",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"source": "url",
|
"source": "url",
|
||||||
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
||||||
},
|
},
|
||||||
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
|
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-gitea",
|
"name": "jsc-gitea",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步",
|
"description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "JSC"
|
"name": "JSC"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-gitea",
|
"name": "jsc-gitea",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步",
|
"description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,12 +35,58 @@ gitea.sh wiki-put <owner>/<repo> <page> <file> # 自動判斷新建或更新
|
|||||||
gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用
|
gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用
|
||||||
gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file>
|
gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file>
|
||||||
gitea.sh pr-status <owner>/<repo> <pr-index> # 印出 {state} {merged} {mergeable}
|
gitea.sh pr-status <owner>/<repo> <pr-index> # 印出 {state} {merged} {mergeable}
|
||||||
gitea.sh pr-comments <owner>/<repo> <pr-index> # 所有留言(issue/審查/行內),依時間排序
|
gitea.sh pr-comments <owner>/<repo> <pr-index> # 印出所有留言(issue 留言、審查評語、行內留言),依時間排序
|
||||||
|
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]
|
gitea.sh api <METHOD> <path> [json-file]
|
||||||
hash-id <text> # 與 gitea.sh hash-id 相同
|
hash-id <text> # 與 gitea.sh hash-id 相同
|
||||||
|
repo-sync.sh <owner>/<repo> [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因}
|
||||||
|
# 基準分支的優先序只在這支腳本裡;dirty 會把解析好的分支帶出來當 PR 的 base
|
||||||
check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則
|
check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則
|
||||||
```
|
```
|
||||||
|
|
||||||
|
議題與 HTML 產出:
|
||||||
|
|
||||||
|
```
|
||||||
|
gitea-link.sh parse <url> # 解析 wiki 或議題連結;不是這兩種就 exit 3(呼叫端據此中止)
|
||||||
|
issue.sh labels|label-ids|projects <owner>/<repo>
|
||||||
|
issue.sh title|body|labels-of <owner>/<repo> <index>
|
||||||
|
issue.sh create <owner>/<repo> <title> <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 目錄
|
## Skills 目錄
|
||||||
|
|
||||||
呼叫方式:Claude / Antigravity `/jsc-gitea:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。
|
呼叫方式:Claude / Antigravity `/jsc-gitea:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。
|
||||||
@@ -49,11 +95,23 @@ check-wiki-rules.sh # 驗證 wiki repo 解析與 ha
|
|||||||
|
|
||||||
### `wiki`
|
### `wiki`
|
||||||
|
|
||||||
Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字為最後手段。
|
Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字每節最多三句。
|
||||||
|
|
||||||
### `repo-sync`
|
### `repo-sync`
|
||||||
|
|
||||||
存取庫批次同步:列出 owner → 使用者選擇 → 逐 repo(sub agent)clone 或切 develop/master 更新;有變更就開分支 commit、push、PR。
|
存取庫批次同步:列出 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 -->
|
<!-- JSC-SKILLS:END -->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-gitea",
|
"name": "jsc-gitea",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步",
|
"description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步",
|
||||||
"skills": "./skills/"
|
"skills": "./skills/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Wiki 頁之間的連結
|
||||||
|
|
||||||
|
寫 wiki 頁才需要這份規則。兩條規則任一條寫錯,連結會指向一個不存在的頁,畫面上看不出異常。
|
||||||
|
|
||||||
|
## 方向:顯示文字在左,頁名在右
|
||||||
|
|
||||||
|
Gitea 採 GitHub/Gollum 慣例:`[[顯示文字|頁名]]`。方向與 MediaWiki 相反。Gitea 原始碼(`modules/markup/html_link.go`)寫得很清楚:
|
||||||
|
|
||||||
|
> MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax
|
||||||
|
|
||||||
|
所以 `[[PLAN_H1234567|我的計畫]]` 會顯示成文字 `PLAN_H1234567`,連到一個叫「我的計畫」的頁——這是壞連結。要寫 `[[我的計畫|PLAN_H1234567]]`。
|
||||||
|
|
||||||
|
顯示文字與頁名相同時,用不帶豎線的 `[[PLAN_H1234567]]`,這種寫法不會寫錯。
|
||||||
|
|
||||||
|
## 範圍:`[[...]]` 只在同一個 wiki 內解析
|
||||||
|
|
||||||
|
`[[...]]` 與 markdown 相對連結都只在目前這個 wiki 內解析。跨存取庫沒有 wiki 連結語法。
|
||||||
|
|
||||||
|
| 連結 | 同一個 wiki? | 寫法 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 同頁面類型(例:`PLAN_CONTENTS` → `PLAN_{HASH}`) | 一定同一個:一個類型一個存取庫 | `[[顯示文字\|頁名]]` 或 `[[頁名]]` |
|
||||||
|
| 不同頁面類型(例:`LOG_{HASH}` → `PLAN_{HASH}`) | **只有兩個類型解析到同一個存取庫時才同一個** | `wiki-url` 給的絕對網址:`[顯示文字](https://…/wiki/PLAN_…)` |
|
||||||
|
|
||||||
|
每個類型各自解析自己的 `JSC_WIKI_REPO_{TYPE}`,所以跨類型連結**一律**用絕對網址:兩個類型剛好同存取庫也照樣正確,不必分兩種寫法。網址一律取自 `tools/gitea.sh wiki-url`,不要自己組路徑。
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
---
|
---
|
||||||
name: repo-sync
|
name: repo-sync
|
||||||
description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo; local changes become a branch, commit, push, and PR to develop or master. Use for workspace bootstrap or bulk refresh; not for a single repo.
|
description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo through tools/repo-sync.sh; a repo reported dirty goes to jsc-git:pr against the base branch that same script reports. Use for workspace bootstrap or bulk refresh; not for a single repo.
|
||||||
---
|
---
|
||||||
|
|
||||||
# repo-sync — batch-sync repositories
|
# repo-sync — batch-sync repositories
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Before calling `tools/gitea.sh`, resolve `GITEA_HOST` and `GITEA_TOKEN` from the current shell environment (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). If `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules before proceeding to any `tools/gitea.sh` call.
|
1. Before calling `tools/gitea.sh`, resolve `GITEA_HOST` and `GITEA_TOKEN` from the current shell environment (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). If `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules before proceeding to any `tools/gitea.sh` call. Done when `GITEA_HOST` holds a value and either `GITEA_TOKEN` or a tea login token is available.
|
||||||
2. Run `tools/gitea.sh owners` to list every `{owner}` the user can read.
|
2. Run `tools/gitea.sh owners` to list every `{owner}` the user can read. Done when the command has printed at least one `{owner}`.
|
||||||
3. Ask the user which `{owner}` to sync, per the `jsc-ask:ask` rules. Every option states the owner's repo count and impact scope.
|
3. Ask the user which `{owner}` to sync, per the `jsc-ask:ask` rules. Every option states the owner's repo count and impact scope. Done when the user has named exactly one `{owner}` from that list.
|
||||||
4. Run `tools/gitea.sh repos {owner}` to list every readable `{repo}` under that owner.
|
4. Run `tools/gitea.sh repos {owner}` to list every readable `{repo}` under that owner. Done when the command has printed the full `{owner}/{repo}` list for the chosen owner.
|
||||||
5. Sync each `{repo}` one by one. This step **MUST run as a sub agent** (one sub agent per repo):
|
5. Sync each `{repo}` one by one. This step **MUST run as a sub agent** (one sub agent per repo):
|
||||||
1. Missing locally → `git clone` into the working directory (clone URL from `tools/gitea.sh clone-url`).
|
1. Run `tools/repo-sync.sh {owner}/{repo}`. The script owns the clone-versus-pull decision and the base-branch precedence, so run no `git clone`, `git checkout` or `git pull` by hand, and derive no branch name yourself. Route on its single line of output: `cloned` or `updated` → this repo is done; `dirty {branch}` → go to substep 2; `failed {reason}` → record that reason and stop this repo. Done when exactly one of those four outcomes is recorded for this repo.
|
||||||
2. Present locally → switch to `develop`, else `master` (or the result of `tools/gitea.sh default-branch`), then `git pull`.
|
2. `dirty {branch}` → call `jsc-git:pr` with `{branch}` from that same output line as the base, passed through verbatim. It commits, branches, pushes and opens the PR itself, so add none of those steps. Done when `jsc-git:pr` returns the PR URL.
|
||||||
3. Local file changes → create a branch from develop or master, commit via `jsc-git:commit`, push, then open a PR back to develop or master via `jsc-git:pr`.
|
6. Report the sync result for every repo: cloned, updated, PR URL, or the failure reason. Done when every `{repo}` from step 4 carries one of those four results.
|
||||||
6. Report the sync result for every repo: cloned, updated, PR created, or the failure reason.
|
|
||||||
|
|||||||
@@ -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.
|
||||||
+8
-21
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: wiki
|
name: wiki
|
||||||
description: Read or write a Gitea wiki page through tools/gitea.sh and tools/hash-id. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Used by jsc-ask, jsc-sdlc, and jsc-log for wiki pages, including ERROR pages, not for repo code files.
|
description: Read or write a Gitea wiki page through tools/gitea.sh and tools/hash-id. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Callers are jsc-ask, jsc-sdlc, jsc-log, and jsc-hooks for its ERROR pages. Use for any wiki page in the skill set; not for repo code files.
|
||||||
---
|
---
|
||||||
|
|
||||||
# wiki — read and write Gitea wiki pages
|
# wiki — read and write Gitea wiki pages
|
||||||
@@ -9,11 +9,11 @@ Every wiki operation in the jsc skill set goes through this skill. One entry poi
|
|||||||
|
|
||||||
## Resolve the wiki location
|
## Resolve the wiki location
|
||||||
|
|
||||||
Different page types can live in different `{owner}/{repo}` repos, classified by page-name prefix: `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, `ERROR`.
|
Different page types can live in different `{owner}/{repo}` repos, classified by the page-name prefix.
|
||||||
|
|
||||||
1. Before asking the user, inspect the current shell environment for the needed repo variables and Gitea connection variables: `JSC_WIKI_REPO_{TYPE}`, `JSC_WIKI_REPO`, `GITEA_HOST`, and `GITEA_TOKEN` (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). Use inherited shell values first; only ask when the needed repo or connection value cannot be resolved after that check. `GITEA_HOST` and `GITEA_TOKEN` are both covered by this ask-if-unresolvable rule, the same as the wiki-repo variables below.
|
1. Before asking the user, inspect the current shell environment for the needed repo variables and Gitea connection variables: `JSC_WIKI_REPO_{TYPE}`, `JSC_WIKI_REPO`, `GITEA_HOST`, and `GITEA_TOKEN` (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). Use inherited shell values first; only ask when the needed repo or connection value cannot be resolved after that check. Done when every one of those variables is either resolved from the environment or listed as missing.
|
||||||
2. Run `tools/gitea.sh wiki-repo {TYPE}` (TYPE = the page-name prefix). Allowed types are `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, and `ERROR`. Resolution order is `JSC_WIKI_REPO_{TYPE}` first, then `JSC_WIKI_REPO`. Never borrow another type's repo.
|
2. Run `tools/gitea.sh wiki-repo {TYPE}` (TYPE = the page-name prefix). Allowed types are `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, and `ERROR`. Resolution order is `JSC_WIKI_REPO_{TYPE}` first, then `JSC_WIKI_REPO`. Never borrow another type's repo. Done when the command has printed exactly one `{owner}/{repo}`, or exited 3 and sent this page type to step 3.
|
||||||
3. On exit 3 (neither is set after env inspection), ask the user for that page type's `{owner}/{repo}` per the `jsc-ask:ask` rules, and suggest setting `JSC_WIKI_REPO_{TYPE}` (can differ per type) or `JSC_WIKI_REPO` (shared default).
|
3. On exit 3 (neither is set after env inspection), ask the user for that page type's `{owner}/{repo}` per the `jsc-ask:ask` rules, and suggest setting `JSC_WIKI_REPO_{TYPE}` (can differ per type) or `JSC_WIKI_REPO` (shared default). Done when the user has supplied one `{owner}/{repo}` for that page type.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
@@ -24,20 +24,7 @@ Different page types can live in different `{owner}/{repo}` repos, classified by
|
|||||||
| write page | write the content to a temp file first, then `tools/gitea.sh wiki-put {owner}/{repo} {page} {file}` (creates or updates automatically) |
|
| write page | write the content to a temp file first, then `tools/gitea.sh wiki-put {owner}/{repo} {page} {file}` (creates or updates automatically) |
|
||||||
| page URL | `tools/gitea.sh wiki-url {owner}/{repo} {page}` — the page's absolute URL, taken from the API's `html_url` (exit 4 when the page is missing) |
|
| page URL | `tools/gitea.sh wiki-url {owner}/{repo} {page}` — the page's absolute URL, taken from the API's `html_url` (exit 4 when the page is missing) |
|
||||||
|
|
||||||
## Linking between wiki pages
|
When writing a page, link same-type pages with the `[[display|page]]` form (display text on the LEFT) and cross-type pages with the absolute URL from `wiki-url`, because `[[...]]` resolves only inside one wiki. Full rules and the direction trap: `references/wiki-links.md`.
|
||||||
|
|
||||||
Two rules, and getting either wrong produces a link that silently points at a page that does not exist.
|
|
||||||
|
|
||||||
**Direction**: Gitea uses the GitHub/Gollum convention — `[[display text|page name]]`, **display text on the LEFT, page name on the RIGHT**. This is the opposite of MediaWiki. Gitea's own source says so (`modules/markup/html_link.go`): *"MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax"*. So `[[PLAN_H1234567|我的計畫]]` renders as the text `PLAN_H1234567` linking to a page named 我的計畫 — broken. Write `[[我的計畫|PLAN_H1234567]]`. When display text and page name are the same, use the no-pipe form `[[PLAN_H1234567]]`, which cannot be got wrong.
|
|
||||||
|
|
||||||
**Scope**: `[[...]]` and relative markdown links both resolve **only inside the current wiki**. There is no cross-repo wiki-link syntax.
|
|
||||||
|
|
||||||
| Link | Same wiki? | Use |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Same page type (e.g. `PLAN_CONTENTS` → `PLAN_{HASH}`) | Always — one type, one repo | `[[display\|page]]` or `[[page]]` |
|
|
||||||
| Different page type (e.g. `LOG_{HASH}` → `PLAN_{HASH}`) | **Only when both types resolve to the same repo** | Absolute URL from `wiki-url`: `[display](https://…/wiki/PLAN_…)` |
|
|
||||||
|
|
||||||
Because each type resolves its own `JSC_WIKI_REPO_{TYPE}`, a cross-type link **must** use the absolute URL — it stays correct whether or not the two types happen to share a repo, so never branch on that. Get the URL from `wiki-url`, never hand-assemble the path.
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
@@ -45,5 +32,5 @@ Because each type resolves its own `JSC_WIKI_REPO_{TYPE}`, a cross-type link **m
|
|||||||
2. Use `tools/hash-id` for `{HASH}` values. It returns the first 8 uppercase SHA-1 hex chars, or `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`.
|
2. Use `tools/hash-id` for `{HASH}` values. It returns the first 8 uppercase SHA-1 hex chars, or `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`.
|
||||||
3. To update a contents page (`*_CONTENTS`): `wiki-get` it first, apply the template to append or modify, then `wiki-put` the whole page back. Never overwrite entries owned by others.
|
3. To update a contents page (`*_CONTENTS`): `wiki-get` it first, apply the template to append or modify, then `wiki-put` the whole page back. Never overwrite entries owned by others.
|
||||||
4. Write all wiki content in UTF-8 Traditional Chinese, per the STE100 output rule.
|
4. Write all wiki content in UTF-8 Traditional Chinese, per the STE100 output rule.
|
||||||
5. Prefer visual forms for page content: use mermaid diagrams (flowchart, sequence, gantt, pie) and markdown tables wherever the information allows. Plain running text is the last resort, kept short.
|
5. Prefer visual forms for page content: use mermaid diagrams (flowchart, sequence, gantt, pie) and markdown tables wherever the information allows. Prose is capped at 3 sentences per section, and a sentence stays only when neither a mermaid diagram nor a markdown table can carry the same information.
|
||||||
6. Authentication fallback is built into `tools/gitea.sh`: on a missing GITEA_TOKEN or a 401/403 response it retries with the tea CLI login token automatically. But before calling it, resolve `GITEA_HOST` and `GITEA_TOKEN` per rule 1: if `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules — same decision-tree pattern as the missing-wiki-repo case above — before calling `tools/gitea.sh`. Only report a genuine failure when the user has no answer to give or Gitea itself rejects the request (e.g. a 401/403 even after the tea fallback).
|
6. `tools/gitea.sh` retries once with the tea CLI login token when `GITEA_TOKEN` is missing or the response is 401/403. Report a failure only after that retry also fails.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
|
<style>{{BASE}}</style>
|
||||||
|
<style>{{STYLE}}</style>
|
||||||
|
<style>
|
||||||
|
.shell { max-width: 1280px; margin: 0 auto; padding: 0 1.25rem; }
|
||||||
|
.content { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.25rem; align-items: start; }
|
||||||
|
.jsc-section {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.jsc-section h2 { margin-top: 0; font-size: 1.15rem; }
|
||||||
|
.jsc-section table { font-size: .88rem; background: var(--bg); }
|
||||||
|
.jsc-section pre { background: var(--bg); }
|
||||||
|
/* 只有一段內容時不要孤零零一張窄卡片 */
|
||||||
|
.content.single { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
@media print { .content { display: block; } .jsc-section { page-break-inside: avoid; margin-bottom: 1rem; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="page-head">
|
||||||
|
<h1>{{TITLE}}</h1>
|
||||||
|
<p class="subtitle">{{SUBTITLE}}</p>
|
||||||
|
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||||
|
</header>
|
||||||
|
<main class="content" id="content">{{CONTENT}}</main>
|
||||||
|
<footer class="page-foot">
|
||||||
|
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||||
|
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>{{BASE_JS}}</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
jsc.wrapTables(content);
|
||||||
|
var cards = jsc.splitBySection(content);
|
||||||
|
if (cards.length < 2) content.classList.add('single');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<!-- 單頁摘要:窄欄、重點框,適合議題摘要與會議結論,印出來剛好一頁 -->
|
||||||
|
<!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>
|
||||||
|
<style>{{BASE}}</style>
|
||||||
|
<style>{{STYLE}}</style>
|
||||||
|
<style>
|
||||||
|
.shell { max-width: 760px; margin: 0 auto; padding: 0 1.25rem; }
|
||||||
|
.page-head { padding-top: 2rem; }
|
||||||
|
.content { font-size: .97rem; }
|
||||||
|
.content h2 { font-size: 1.15rem; margin-top: 1.4rem; }
|
||||||
|
.content h3 { font-size: 1rem; }
|
||||||
|
.content ul { padding-left: 1.2rem; }
|
||||||
|
/* 待辦清單獨立成框,一眼看得到還有什麼沒做 */
|
||||||
|
.content li:has(input[type="checkbox"]) { list-style: none; margin-left: -1.2rem; }
|
||||||
|
.content blockquote { font-size: .95rem; }
|
||||||
|
.jsc-section + .jsc-section { border-top: 1px dashed var(--border); padding-top: .75rem; }
|
||||||
|
@media print { body { font-size: 13px; } .page-foot { display: none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="page-head">
|
||||||
|
<h1>{{TITLE}}</h1>
|
||||||
|
<p class="subtitle">{{SUBTITLE}}</p>
|
||||||
|
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||||
|
</header>
|
||||||
|
<main class="content" id="content">{{CONTENT}}</main>
|
||||||
|
<footer class="page-foot">
|
||||||
|
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||||
|
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>{{BASE_JS}}</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
jsc.wrapTables(content);
|
||||||
|
jsc.splitBySection(content);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!-- 報告書:左側目錄加章節內文,適合分析頁、交付頁這類長文件 -->
|
||||||
|
<!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>
|
||||||
|
<style>{{BASE}}</style>
|
||||||
|
<style>{{STYLE}}</style>
|
||||||
|
<style>
|
||||||
|
.shell { max-width: 1080px; margin: 0 auto; padding: 0 1.25rem; }
|
||||||
|
.body-grid { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: 2.5rem; }
|
||||||
|
.toc {
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
|
align-self: start;
|
||||||
|
max-height: calc(100vh - 3rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
.toc h2 { margin: 0 0 .5rem; font-size: .8rem; letter-spacing: .12em; color: var(--muted); text-transform: uppercase; }
|
||||||
|
.toc ul { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.toc li { margin: .2rem 0; }
|
||||||
|
.toc a { color: var(--fg); text-decoration: none; display: block; padding: .15rem .5rem; border-left: 2px solid var(--border); }
|
||||||
|
.toc a:hover { color: var(--accent); border-left-color: var(--accent); }
|
||||||
|
.toc .toc-h3 a { padding-left: 1.25rem; color: var(--muted); }
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.body-grid { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
|
||||||
|
.toc { position: static; max-height: none; border-bottom: 1px solid var(--border); }
|
||||||
|
}
|
||||||
|
@media print { .toc { display: none; } .body-grid { display: block; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="page-head">
|
||||||
|
<h1>{{TITLE}}</h1>
|
||||||
|
<p class="subtitle">{{SUBTITLE}}</p>
|
||||||
|
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||||
|
</header>
|
||||||
|
<div class="body-grid">
|
||||||
|
<nav class="toc" id="toc"><h2>目錄</h2></nav>
|
||||||
|
<main class="content" id="content">{{CONTENT}}</main>
|
||||||
|
</div>
|
||||||
|
<footer class="page-foot">
|
||||||
|
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||||
|
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>{{BASE_JS}}</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
jsc.wrapTables(content);
|
||||||
|
jsc.buildToc(content, document.getElementById('toc'));
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<!-- 投影片:一個 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>
|
||||||
|
<style>{{BASE}}</style>
|
||||||
|
<style>{{STYLE}}</style>
|
||||||
|
<style>
|
||||||
|
body { display: flex; flex-direction: column; min-height: 100vh; }
|
||||||
|
.shell { flex: 1; display: flex; flex-direction: column; max-width: 1200px; width: 100%; margin: 0 auto; padding: 0 1.5rem; }
|
||||||
|
.page-head { padding: 1.5rem 0 1rem; }
|
||||||
|
.page-head h1 { font-size: 1.5rem; }
|
||||||
|
.content { flex: 1; padding: 0; }
|
||||||
|
.jsc-section { display: none; padding: 1.5rem 0; }
|
||||||
|
.jsc-section.is-active { display: block; animation: fade .18s ease-out; }
|
||||||
|
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
||||||
|
.jsc-section h2 { margin-top: 0; font-size: 2rem; }
|
||||||
|
.jsc-section p, .jsc-section li { font-size: 1.15rem; }
|
||||||
|
.deck-bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .75rem;
|
||||||
|
padding: .75rem 0 1.25rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.deck-bar button {
|
||||||
|
font: inherit;
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: .35rem .9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.deck-bar button:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.deck-pos { color: var(--muted); font-size: .9rem; margin-left: auto; }
|
||||||
|
@media print {
|
||||||
|
.jsc-section, .jsc-section.is-active { display: block; page-break-after: always; }
|
||||||
|
.deck-bar { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="page-head">
|
||||||
|
<h1>{{TITLE}}</h1>
|
||||||
|
<p class="subtitle">{{SUBTITLE}}</p>
|
||||||
|
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||||
|
</header>
|
||||||
|
<main class="content" id="content">{{CONTENT}}</main>
|
||||||
|
<div class="deck-bar">
|
||||||
|
<button type="button" id="prev">← 上一頁</button>
|
||||||
|
<button type="button" id="next">下一頁 →</button>
|
||||||
|
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||||
|
<span class="deck-pos" id="pos"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>{{BASE_JS}}</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
jsc.wrapTables(content);
|
||||||
|
var slides = jsc.splitBySection(content);
|
||||||
|
var at = 0;
|
||||||
|
var pos = document.getElementById('pos');
|
||||||
|
|
||||||
|
function show(i) {
|
||||||
|
if (!slides.length) return;
|
||||||
|
at = Math.max(0, Math.min(slides.length - 1, i));
|
||||||
|
slides.forEach(function (s, n) { s.classList.toggle('is-active', n === at); });
|
||||||
|
pos.textContent = (at + 1) + ' / ' + slides.length;
|
||||||
|
window.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('prev').addEventListener('click', function () { show(at - 1); });
|
||||||
|
document.getElementById('next').addEventListener('click', function () { show(at + 1); });
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'PageUp') show(at - 1);
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') show(at + 1);
|
||||||
|
});
|
||||||
|
show(0);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!-- 規格書:表格與程式碼區塊放大,表頭固定,適合 API 文件與交付規格 -->
|
||||||
|
<!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>
|
||||||
|
<style>{{BASE}}</style>
|
||||||
|
<style>{{STYLE}}</style>
|
||||||
|
<style>
|
||||||
|
.shell { max-width: 1120px; margin: 0 auto; padding: 0 1.25rem; }
|
||||||
|
.body-grid { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 2rem; }
|
||||||
|
.toc { position: sticky; top: 1.5rem; align-self: start; padding: 1.25rem 0; font-size: .88rem; }
|
||||||
|
.toc h2 { margin: 0 0 .5rem; font-size: .78rem; letter-spacing: .12em; color: var(--muted); text-transform: uppercase; }
|
||||||
|
.toc ul { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.toc a { color: var(--fg); text-decoration: none; display: block; padding: .15rem .4rem; }
|
||||||
|
.toc a:hover { color: var(--accent); }
|
||||||
|
.toc .toc-h3 a { padding-left: 1.1rem; color: var(--muted); }
|
||||||
|
.jsc-section { border-bottom: 1px solid var(--border); padding-bottom: 1.25rem; margin-bottom: 1.5rem; }
|
||||||
|
.jsc-section:last-child { border-bottom: 0; }
|
||||||
|
.content .table-wrap { max-height: 70vh; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius); }
|
||||||
|
.content .table-wrap table { border: 0; }
|
||||||
|
.content .table-wrap th { position: sticky; top: 0; z-index: 1; }
|
||||||
|
.content code { font-size: .92em; }
|
||||||
|
.content pre { font-size: .95rem; line-height: 1.6; }
|
||||||
|
/* 欄位表第一欄是欄位名,等寬字比較好對 */
|
||||||
|
.content td:first-child code { white-space: nowrap; }
|
||||||
|
@media (max-width: 860px) { .body-grid { grid-template-columns: minmax(0, 1fr); } .toc { position: static; } }
|
||||||
|
@media print { .toc { display: none; } .body-grid { display: block; } .content .table-wrap { max-height: none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="page-head">
|
||||||
|
<h1>{{TITLE}}</h1>
|
||||||
|
<p class="subtitle">{{SUBTITLE}}</p>
|
||||||
|
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||||
|
</header>
|
||||||
|
<div class="body-grid">
|
||||||
|
<nav class="toc" id="toc"><h2>章節</h2></nav>
|
||||||
|
<main class="content" id="content">{{CONTENT}}</main>
|
||||||
|
</div>
|
||||||
|
<footer class="page-foot">
|
||||||
|
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||||
|
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>{{BASE_JS}}</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
jsc.wrapTables(content);
|
||||||
|
jsc.buildToc(content, document.getElementById('toc'));
|
||||||
|
jsc.splitBySection(content);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!-- 時間軸:每個 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>
|
||||||
|
<style>{{BASE}}</style>
|
||||||
|
<style>{{STYLE}}</style>
|
||||||
|
<style>
|
||||||
|
.shell { max-width: 900px; margin: 0 auto; padding: 0 1.25rem; }
|
||||||
|
.content { position: relative; padding-left: 2rem; }
|
||||||
|
.content::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: .45rem;
|
||||||
|
top: .75rem;
|
||||||
|
bottom: .75rem;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
.jsc-section { position: relative; padding: 0 0 1.75rem; }
|
||||||
|
.jsc-section::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -1.9rem;
|
||||||
|
top: .95rem;
|
||||||
|
width: .85rem;
|
||||||
|
height: .85rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
border: 3px solid var(--bg);
|
||||||
|
}
|
||||||
|
.jsc-section h2 { margin-top: .5rem; font-size: 1.2rem; }
|
||||||
|
.jsc-section:last-child { padding-bottom: .5rem; }
|
||||||
|
@media print { .jsc-section { page-break-inside: avoid; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="page-head">
|
||||||
|
<h1>{{TITLE}}</h1>
|
||||||
|
<p class="subtitle">{{SUBTITLE}}</p>
|
||||||
|
<p class="meta">{{SOURCE}}<span class="generated">產生時間:{{GENERATED}}</span></p>
|
||||||
|
</header>
|
||||||
|
<main class="content" id="content">{{CONTENT}}</main>
|
||||||
|
<footer class="page-foot">
|
||||||
|
<span class="badge">{{LAYOUT}}/{{STYLE_NAME}}</span>
|
||||||
|
<span>本頁由 jsc-gitea:html-export 產生</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>{{BASE_JS}}</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
jsc.wrapTables(content);
|
||||||
|
jsc.splitBySection(content);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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); }
|
||||||
@@ -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; }
|
||||||
@@ -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; }
|
||||||
@@ -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); }
|
||||||
|
}
|
||||||
@@ -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); }
|
||||||
+46
-34
@@ -1,11 +1,15 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# check-wiki-rules — 驗證 wiki repo 解析與 hash-id 規則。
|
# check-wiki-rules — 驗證 wiki repo 解析與 hash-id 規則。
|
||||||
|
# 涵蓋全部九種頁面類型,每種三項:專用變數優先、退回共用變數、不得跨類型代用。
|
||||||
|
# 全部通過印 OK 並 exit 0;任一項不符印出差異並 exit 1。
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
gitea="$dir/gitea.sh"
|
gitea="$dir/gitea.sh"
|
||||||
hash_id="$dir/hash-id"
|
hash_id="$dir/hash-id"
|
||||||
|
|
||||||
|
TYPES='QUESTION PLAN ANALYZE DELIVER MAINTAIN REPO LOG LEARN ERROR'
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf '%s\n' "$1" >&2
|
printf '%s\n' "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -18,7 +22,7 @@ expect_eq() {
|
|||||||
[ "$got" = "$want" ] || fail "$label: want=$want got=$got"
|
[ "$got" = "$want" ] || fail "$label: want=$want got=$got"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_repo() {
|
check_repo() { # type label want [VAR=值...]
|
||||||
type=$1
|
type=$1
|
||||||
label=$2
|
label=$2
|
||||||
want=$3
|
want=$3
|
||||||
@@ -27,6 +31,19 @@ check_repo() {
|
|||||||
expect_eq "$got" "$want" "$label"
|
expect_eq "$got" "$want" "$label"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_no_repo() { # type label [VAR=值...]:期望 exit 3 且不印出任何 {owner}/{repo}
|
||||||
|
type=$1
|
||||||
|
label=$2
|
||||||
|
shift 2
|
||||||
|
if got=$(env -i PATH="${PATH:-/usr/bin:/bin}" "$@" "$gitea" wiki-repo "$type" 2>/dev/null); then
|
||||||
|
code=0
|
||||||
|
else
|
||||||
|
code=$?
|
||||||
|
fi
|
||||||
|
[ "$code" -eq 3 ] || fail "$label: want exit=3 got exit=$code output=$got"
|
||||||
|
[ -z "$got" ] || fail "$label: want no output got=$got"
|
||||||
|
}
|
||||||
|
|
||||||
check_hash() {
|
check_hash() {
|
||||||
input=$1
|
input=$1
|
||||||
want=$2
|
want=$2
|
||||||
@@ -34,47 +51,42 @@ check_hash() {
|
|||||||
expect_eq "$got" "$want" "hash-id $input"
|
expect_eq "$got" "$want" "hash-id $input"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_repo REPO 'REPO specific wins' 'records/REPO' \
|
# 其他八個類型的誘餌值。跨類型代用一旦發生,回傳的就會是 decoy/{別的類型}。
|
||||||
JSC_WIKI_REPO_REPO='records/REPO' \
|
decoys() { # $1=要排除的類型
|
||||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
for t in $TYPES; do
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
[ "$t" = "$1" ] || printf 'JSC_WIKI_REPO_%s=decoy/%s ' "$t" "$t"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
check_repo REPO 'REPO falls back to shared only' 'shared/wiki' \
|
for ty in $TYPES; do
|
||||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
# 1. 自己的變數贏過共用變數,也贏過其他類型的誘餌。
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
check_repo "$ty" "$ty specific wins" "own/$ty" \
|
||||||
|
$(decoys "$ty") "JSC_WIKI_REPO_$ty=own/$ty" JSC_WIKI_REPO='shared/wiki'
|
||||||
|
|
||||||
check_repo ANALYZE 'ANALYZE specific wins' 'knowledges/ANALYZE' \
|
# 2. 自己的變數未設定時,只退回共用變數。
|
||||||
JSC_WIKI_REPO_REPO='records/REPO' \
|
check_repo "$ty" "$ty falls back to shared only" 'shared/wiki' \
|
||||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
$(decoys "$ty") JSC_WIKI_REPO='shared/wiki'
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
|
||||||
|
|
||||||
check_repo ANALYZE 'ANALYZE falls back to shared only' 'shared/wiki' \
|
# 3. 自己的變數與共用變數都沒有時,exit 3,不借用別的類型。
|
||||||
JSC_WIKI_REPO_REPO='records/REPO' \
|
check_no_repo "$ty" "$ty never borrows another type" $(decoys "$ty")
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
done
|
||||||
|
|
||||||
check_repo LEARN 'LEARN specific wins' 'lessons/LEARN' \
|
# 未知類型 exit 2,與「設定不足」的 exit 3 分開。
|
||||||
JSC_WIKI_REPO_LEARN='lessons/LEARN' \
|
if got=$(env -i PATH="${PATH:-/usr/bin:/bin}" JSC_WIKI_REPO='shared/wiki' \
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
"$gitea" wiki-repo NOSUCH 2>/dev/null); then
|
||||||
|
code=0
|
||||||
check_repo LEARN 'LEARN falls back to shared only' 'shared/wiki' \
|
else
|
||||||
JSC_WIKI_REPO_LOG='records/LOG' \
|
code=$?
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
fi
|
||||||
|
[ "$code" -eq 2 ] || fail "unknown type: want exit=2 got exit=$code"
|
||||||
check_repo DELIVER 'DELIVER specific wins' 'handover/DELIVER' \
|
[ -z "$got" ] || fail "unknown type: want no output got=$got"
|
||||||
JSC_WIKI_REPO_DELIVER='handover/DELIVER' \
|
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
|
||||||
|
|
||||||
check_repo DELIVER 'DELIVER falls back to shared only' 'shared/wiki' \
|
|
||||||
JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \
|
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
|
||||||
|
|
||||||
check_repo ERROR 'ERROR uses its own repo' 'errors/wiki' \
|
|
||||||
JSC_WIKI_REPO_ERROR='errors/wiki' \
|
|
||||||
JSC_WIKI_REPO='shared/wiki'
|
|
||||||
|
|
||||||
|
# hash-id:首碼 0-9/A/B/C 改成 H 加前 7 碼;其餘首碼原樣輸出 8 碼大寫。
|
||||||
check_hash 'case-2' 'H5172CB7'
|
check_hash 'case-2' 'H5172CB7'
|
||||||
check_hash 'case-11' 'HA9A6662'
|
check_hash 'case-11' 'HA9A6662'
|
||||||
check_hash 'case-1' 'HB6EC7FD'
|
check_hash 'case-1' 'HB6EC7FD'
|
||||||
check_hash 'case-12' 'HCCA8D42'
|
check_hash 'case-12' 'HCCA8D42'
|
||||||
|
check_hash 'case-3' 'D3E5AA27'
|
||||||
|
check_hash 'case-7' 'D794C002'
|
||||||
|
|
||||||
printf '%s\n' 'OK'
|
printf '%s\n' 'OK'
|
||||||
|
|||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# gitea-link.sh — 解析 Gitea 的 wiki 頁或議題連結(供 jsc-gitea:wiki-to-issue、html-export 使用)。
|
||||||
|
#
|
||||||
|
# 為什麼要有這支腳本:兩支技能都以「一條連結」為唯一入口,沒有連結就中斷。連結長什麼樣、
|
||||||
|
# 哪一段是存取庫、哪一段是頁名,是固定的字串規則,交給模型每次自己拆,拆錯就寫到別的存取庫去。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# gitea-link.sh parse <url> 解析連結,印出可直接 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 <url>' >&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
|
||||||
+15
-2
@@ -19,6 +19,7 @@
|
|||||||
# gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index>
|
# gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index>
|
||||||
# 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併
|
# 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併
|
||||||
# gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁
|
# 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.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始)
|
||||||
# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN
|
# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN
|
||||||
# GITEA_TOKEN 未設定,或請求遇 401/403 時,自動退回 tea CLI 的登入 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
|
case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac
|
||||||
API="${HOST%/}/api/v1"
|
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:-}"
|
method="$1"; path="$2"; body_file="${3:-}"
|
||||||
|
ctype="${REQ_CONTENT_TYPE:-application/json}"
|
||||||
retried=0
|
retried=0
|
||||||
while :; do
|
while :; do
|
||||||
if [ -n "$body_file" ]; then
|
if [ -n "$body_file" ]; then
|
||||||
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
|
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")
|
--data-binary "@$body_file" "$API$path")
|
||||||
else
|
else
|
||||||
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
|
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
|
||||||
@@ -279,6 +283,15 @@ print(json.dumps(b))
|
|||||||
fi
|
fi
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
echo "OK $or" ;;
|
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)
|
api)
|
||||||
method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}"
|
method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}"
|
||||||
req "$method" "$path" $body ;;
|
req "$method" "$path" $body ;;
|
||||||
|
|||||||
Executable
+111
@@ -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"
|
||||||
Executable
+169
@@ -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
|
||||||
Executable
+157
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# issue.sh — Gitea 議題的讀取與建立(供 jsc-gitea:wiki-to-issue、html-export 使用)。
|
||||||
|
#
|
||||||
|
# 為什麼要有這支腳本:建議題要組 JSON、要把標籤名換成標籤 id、要把回應裡的編號與網址挑出來,
|
||||||
|
# 全是固定的輸入輸出。交給模型每次自己拼 curl,跳脫字元一錯就把議題內容寫壞,而議題是對外的。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name
|
||||||
|
# issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id(逗號分隔);有名字對不到就 exit 4
|
||||||
|
# issue.sh projects <owner>/<repo> 列出專案看板:id<TAB>title;站台沒有這個 API 就 exit 3
|
||||||
|
# issue.sh title <owner>/<repo> <index> 印出議題標題
|
||||||
|
# issue.sh body <owner>/<repo> <index> 印出議題正文(markdown)
|
||||||
|
# issue.sh labels-of <owner>/<repo> <index> 印出議題目前的標籤名,一行一個
|
||||||
|
# issue.sh create <owner>/<repo> <title> <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
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# repo-sync.sh — 把單一存取庫同步到本機。
|
||||||
|
# 用法: repo-sync.sh <owner>/<repo> [target-dir] # target-dir 預設為 <repo>
|
||||||
|
# 目錄不存在 → git clone(clone URL 取自 gitea.sh clone-url)
|
||||||
|
# 目錄已存在 → git fetch、切到基準分支、git pull --ff-only
|
||||||
|
# 有未提交變更 → 只回報 dirty 與基準分支,不動分支也不 pull
|
||||||
|
# 基準分支的唯一優先序(呼叫端不必再判斷,也不要自己再推導一次):
|
||||||
|
# 1. gitea.sh default-branch <owner>/<repo>(該分支要在遠端存在)
|
||||||
|
# 2. 查不到時依序取遠端的 develop、main、master
|
||||||
|
# 3. 都沒有 → failed no default branch
|
||||||
|
# 輸出: 恰好一行,cloned、updated、dirty {分支} 或 failed {原因}。
|
||||||
|
# 前三種 exit 0;failed exit 1。
|
||||||
|
# dirty 會把解析好的基準分支一起帶出來,呼叫端直接拿去當 PR 的 base。
|
||||||
|
set -u
|
||||||
|
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
gitea="$script_dir/gitea.sh"
|
||||||
|
|
||||||
|
fail() { # 原因壓成一行,避免呼叫端解析多行輸出
|
||||||
|
printf 'failed %s\n' "$(printf '%s' "$1" | tr '\n\r\t' ' ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
full="${1:-}"
|
||||||
|
[ -n "$full" ] || fail "usage: repo-sync.sh <owner>/<repo> [target-dir]"
|
||||||
|
case "$full" in
|
||||||
|
*/*/*|/*|*/) fail "not owner/repo: $full" ;;
|
||||||
|
*/*) ;;
|
||||||
|
*) fail "not owner/repo: $full" ;;
|
||||||
|
esac
|
||||||
|
repo="${full#*/}"
|
||||||
|
target="${2:-$repo}"
|
||||||
|
|
||||||
|
remote_has() { # 遠端是否有這個分支
|
||||||
|
git -C "$target" rev-parse --verify --quiet "refs/remotes/origin/$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -e "$target" ]; then
|
||||||
|
# clone URL 只收 stdout:把 stderr 併進來會讓任何雜訊變成網址的一部分
|
||||||
|
err=$(mktemp)
|
||||||
|
url=$("$gitea" clone-url "$full" 2>"$err") || url=''
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
reason=$(cat "$err"); rm -f "$err"
|
||||||
|
fail "clone-url failed for $full: ${reason:-no clone URL}"
|
||||||
|
fi
|
||||||
|
rm -f "$err"
|
||||||
|
out=$(git clone "$url" "$target" 2>&1) || fail "clone: $out"
|
||||||
|
printf 'cloned\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$target" rev-parse --git-dir >/dev/null 2>&1 || fail "$target is not a git repo"
|
||||||
|
|
||||||
|
dirty=0
|
||||||
|
[ -z "$(git -C "$target" status --porcelain 2>/dev/null)" ] || dirty=1
|
||||||
|
|
||||||
|
# dirty 的存取庫也要解析基準分支:呼叫端拿它當 PR 的 base,自己再推導一次就會
|
||||||
|
# 出現第二套優先序。fetch 只更新 refs,不動工作目錄,dirty 時照樣安全;但 dirty
|
||||||
|
# 時網路失敗不算致命,還是要把 dirty 回報出去,改用本機既有的 remote refs 判斷。
|
||||||
|
if ! out=$(git -C "$target" fetch --prune origin 2>&1); then
|
||||||
|
[ "$dirty" -eq 1 ] || fail "fetch: $out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch=$("$gitea" default-branch "$full" 2>/dev/null) || branch=''
|
||||||
|
if [ -z "$branch" ] || ! remote_has "$branch"; then
|
||||||
|
branch=''
|
||||||
|
for b in develop main master; do
|
||||||
|
if remote_has "$b"; then branch="$b"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -n "$branch" ] || fail "no default branch"
|
||||||
|
|
||||||
|
if [ "$dirty" -eq 1 ]; then
|
||||||
|
printf 'dirty %s\n' "$branch"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
out=$(git -C "$target" checkout "$branch" 2>&1) || fail "checkout $branch: $out"
|
||||||
|
out=$(git -C "$target" pull --ff-only origin "$branch" 2>&1) || fail "pull $branch: $out"
|
||||||
|
printf 'updated\n'
|
||||||
Reference in New Issue
Block a user