feat(gitea): 新增 HTML 匯出與範本風格設定
What:新增 html-export 與 html-style 兩支技能、tools/html-render.sh 與 tools/html-style.sh 兩支工具,以及六種版型乘五種風格的 HTML 範本。 Why:wiki 頁與議題要拿給 Gitea 以外的人看時,只能複製 markdown;不同類型的文件也該有各自的版面,不是每份都長一樣。 How:版型(report、slide、dashboard、spec、timeline、onepager)決定內容怎麼排,風格(minimal、corporate、dark、print、vivid)決定看起來長怎樣,兩者自由搭配。哪一種頁面套哪一組由設定決定:專案的 .jsc/html-styles 優先,其次 $JSC_HOME/html-styles.conf,對不到退 DEFAULT,再對不到才用內建的 report/minimal。產出是單一 HTML 檔,CSS 與腳本全部內嵌。 Who:需要把 wiki 頁或議題寄給客戶、主管或跨團隊同事的人。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -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); }
|
||||||
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
|
||||||
Reference in New Issue
Block a user