changelogkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. changelogkit/__init__.py +11 -0
  2. changelogkit/__main__.py +6 -0
  3. changelogkit/assets/about.html +37 -0
  4. changelogkit/assets/about.js +22 -0
  5. changelogkit/assets/dev.css +90 -0
  6. changelogkit/assets/dev.html +29 -0
  7. changelogkit/assets/dev.js +244 -0
  8. changelogkit/assets/file.css +51 -0
  9. changelogkit/assets/file.html +29 -0
  10. changelogkit/assets/file.js +101 -0
  11. changelogkit/assets/highlight.js +74 -0
  12. changelogkit/assets/i18n.js +10 -0
  13. changelogkit/assets/index.css +49 -0
  14. changelogkit/assets/index.html +56 -0
  15. changelogkit/assets/index.js +175 -0
  16. changelogkit/assets/markdown.js +144 -0
  17. changelogkit/assets/static_dev.html +37 -0
  18. changelogkit/assets/static_doc.html +30 -0
  19. changelogkit/assets/static_history.html +46 -0
  20. changelogkit/assets/theme.css +86 -0
  21. changelogkit/assets/theme.js +28 -0
  22. changelogkit/assets.py +55 -0
  23. changelogkit/buildinfo.py +71 -0
  24. changelogkit/cli.py +190 -0
  25. changelogkit/config.py +123 -0
  26. changelogkit/export.py +219 -0
  27. changelogkit/history.py +233 -0
  28. changelogkit/i18n.py +92 -0
  29. changelogkit/index.py +288 -0
  30. changelogkit/lang/en.json +140 -0
  31. changelogkit/lang/zh-TW.json +140 -0
  32. changelogkit/scanners/__init__.py +60 -0
  33. changelogkit/scanners/common.py +48 -0
  34. changelogkit/scanners/cpp.py +61 -0
  35. changelogkit/scanners/csharp.py +49 -0
  36. changelogkit/scanners/dart.py +49 -0
  37. changelogkit/scanners/go.py +55 -0
  38. changelogkit/scanners/java.py +49 -0
  39. changelogkit/scanners/javascript.py +46 -0
  40. changelogkit/scanners/kotlin.py +57 -0
  41. changelogkit/scanners/python.py +33 -0
  42. changelogkit/scanners/ruby.py +47 -0
  43. changelogkit/scanners/rust.py +67 -0
  44. changelogkit/scanners/swift.py +72 -0
  45. changelogkit/server.py +134 -0
  46. changelogkit-0.1.0.dist-info/METADATA +151 -0
  47. changelogkit-0.1.0.dist-info/RECORD +50 -0
  48. changelogkit-0.1.0.dist-info/WHEEL +4 -0
  49. changelogkit-0.1.0.dist-info/entry_points.txt +2 -0
  50. changelogkit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,11 @@
1
+ """changelogkit:本機變更歷史 GUI 與開發索引。
2
+
3
+ 通用工具,與任何特定 repo 解耦:
4
+
5
+ * 變更歷史 — 解析 ``Docs/ChangeHistory.md``(可設定路徑),提供網頁 GUI 與 CLI。
6
+ * 開發索引 — 唯讀索引 Git 近期改動(可展開 diff)、Swift 宣告定位、Markdown 文件。
7
+
8
+ 每個 repo 的差異由 repo 根目錄的 ``.changelog.toml`` 提供;未提供時用預設值。
9
+ """
10
+
11
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """``python -m changelogkit`` 的進入點(等同 CLI 指令 ``changelogkit``)。"""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="{{lang_html}}"><head>
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>{{t.about.title}}</title>
6
+ <script src="/assets/theme.js"></script>
7
+ <script src="/assets/i18n.js"></script>
8
+ <script>window.CK_I18N = {{i18n_json}};</script>
9
+ <link rel="stylesheet" href="/assets/theme.css">
10
+ <style>
11
+ main { max-width: 720px; margin: 0 auto; padding: 24px }
12
+ .card dl { display: grid; grid-template-columns: 120px 1fr; gap: 8px 16px; margin: 10px 0 0 }
13
+ .card dt { color: var(--mut); font-size: 13px }
14
+ .card dd { margin: 0; overflow-wrap: anywhere }
15
+ .card code { font-size: 13px }
16
+ </style>
17
+ </head><body>
18
+ <header class="appbar">
19
+ <h1>{{t.about.title}}</h1>
20
+ <a href="/">{{t.nav.history}}</a>
21
+ <a href="/development">{{t.nav.dev}}</a>
22
+ <button id="theme" aria-label="{{t.btn.theme}}" title="{{t.btn.theme}}">🌙</button>
23
+ </header>
24
+ <main>
25
+ <article class="card">
26
+ <h2>changelogkit</h2>
27
+ <p class="muted">{{t.about.intro}}</p>
28
+ <dl>
29
+ <dt>{{t.about.version}}</dt><dd id="version">…</dd>
30
+ <dt>{{t.about.commit}}</dt><dd id="commit">…</dd>
31
+ <dt>{{t.about.locales}}</dt><dd id="locales">…</dd>
32
+ <dt>{{t.about.home}}</dt><dd><a href="https://github.com/TwhomeGH/ChangeLogKit">github.com/TwhomeGH/ChangeLogKit</a></dd>
33
+ </dl>
34
+ </article>
35
+ </main>
36
+ <script src="/assets/about.js"></script>
37
+ </body></html>
@@ -0,0 +1,22 @@
1
+ /* 關於頁:向 /api/buildinfo 取版本與來源 commit(commit 連到 GitHub)。 */
2
+ const REPO = "https://github.com/TwhomeGH/ChangeLogKit";
3
+
4
+ fetch("/api/buildinfo")
5
+ .then((r) => r.json())
6
+ .then((b) => {
7
+ document.getElementById("version").textContent = b.version || CK_T("about.na");
8
+ /* UI 語系清單,* 標示目前生效者。 */
9
+ document.getElementById("locales").textContent =
10
+ (b.locales || []).map((l) => l + (l === b.locale ? " *" : "")).join(" ");
11
+ const raw = b.commit || "";
12
+ if (!raw) {
13
+ document.getElementById("commit").textContent = CK_T("about.unknown");
14
+ return;
15
+ }
16
+ const hash = raw.replace(/\+dirty$/, "");
17
+ document.getElementById("commit").innerHTML =
18
+ '<a href="' + REPO + "/commit/" + hash + '"><code>' + raw + "</code></a>";
19
+ })
20
+ .catch(() => {
21
+ document.getElementById("version").textContent = CK_T("about.load_failed");
22
+ });
@@ -0,0 +1,90 @@
1
+ /* 開發索引頁面版面。主題與共用元件在 theme.css。 */
2
+ main { max-width: 1100px; margin: auto; padding: 24px 28px 60px }
3
+ #hint { margin: 4px 0 0 }
4
+ #tabs { display: flex; gap: 8px; margin: 18px 0 6px; flex-wrap: wrap }
5
+ #tabs button { white-space: nowrap }
6
+ #tabs button[aria-pressed="true"] { background: var(--accent); color: var(--accent-fg); border-color: var(--accent) }
7
+ #message { padding: 6px 0 10px; font-size: 13px }
8
+ article.card h2 { margin: 0 0 6px; font-size: 19px; line-height: 1.35 }
9
+ article.card h2 a { color: var(--fg); text-decoration: none }
10
+ article.card h2 a:hover { color: var(--accent); text-decoration: underline }
11
+ .meta { font-size: 12.5px; color: var(--mut); margin-bottom: 8px }
12
+ .kind {
13
+ display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .03em;
14
+ padding: 1px 7px; border-radius: 6px; background: var(--code); color: var(--accent); margin-right: 6px;
15
+ }
16
+ .card dl { display: grid; grid-template-columns: 108px 1fr; gap: 6px 12px; margin: 10px 0 4px }
17
+ .card dt { color: var(--mut); font-size: 13px } .card dd { margin: 0 }
18
+ .card h3 { font-size: 13px; color: var(--mut); margin: 14px 0 6px; text-transform: none }
19
+ .doc { line-height: 1.6 } .doc p { margin: .3em 0 } .doc ul { margin: .3em 0; padding-left: 1.3em }
20
+ .files a { display: inline-block; margin: 0 12px 6px 0 }
21
+ .preview { font: 13px/1.6 ui-monospace, Consolas, "Courier New", monospace; color: var(--mut); white-space: pre-wrap; overflow-wrap: anywhere; margin: 4px 0 0 }
22
+ pre.blob { background: var(--code); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; overflow: auto; font-size: 12.5px; line-height: 1.55 }
23
+ .deps { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px; margin-top: 6px }
24
+ .deps code { font-size: 12.5px }
25
+ @media (max-width: 640px) {
26
+ main { padding: 16px 16px 48px }
27
+ .card dl { grid-template-columns: 1fr }
28
+ .card dt { font-weight: 700 }
29
+ }
30
+
31
+ /* 「API 宣告」篩選列。 */
32
+ #filters { margin: 4px 0 10px; display: flex; flex-direction: column; gap: 6px }
33
+ #filters .supported { font-size: 12.5px; color: var(--mut); overflow-wrap: anywhere }
34
+ #filters .chips { display: flex; flex-wrap: wrap; gap: 6px 8px }
35
+ #filters .chip.on { background: var(--accent); color: var(--accent-fg); border-color: var(--accent) }
36
+ #filters .kindfilter { font-size: 13px; color: var(--mut) }
37
+ #filters .kindfilter select { width: auto; margin-left: 6px }
38
+
39
+ /* 符號簽名列:等寬、可換行、深淺色一致。 */
40
+ .sig {
41
+ margin: 6px 0 10px; padding: 9px 12px; border-radius: 10px;
42
+ background: var(--code); border: 1px solid var(--line);
43
+ color: var(--fg); font-size: 12.5px; white-space: pre-wrap;
44
+ word-break: break-word; overflow-x: auto;
45
+ }
46
+
47
+ /* 近期改動:提交卡片與差異檢視。 */
48
+ .commits { display: flex; flex-direction: column; gap: 6px }
49
+ .commit { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--bg) }
50
+ .commit-head {
51
+ display: flex; flex-direction: column; gap: 3px; align-items: stretch; width: 100%;
52
+ text-align: left; background: transparent; border: 0; border-radius: 0; padding: 8px 12px;
53
+ }
54
+ .commit-head:hover { background: var(--line2) }
55
+ .commit-top { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap }
56
+ .commit-head .hash { color: var(--accent); font-size: 12.5px; background: none; padding: 0 }
57
+ .commit-head .when { color: var(--mut); font-size: 12px; white-space: nowrap }
58
+ .commit-head .subject { font-weight: 600; white-space: normal; overflow-wrap: anywhere }
59
+ .commit-files { display: flex; flex-wrap: wrap; gap: 4px 14px; padding: 7px 12px; border-top: 1px solid var(--line2) }
60
+ .commit-files a.cfile { font-size: 12px; color: var(--mut) }
61
+ .commit-files a.cfile:hover { color: var(--accent) }
62
+ .wt-wrap { margin: 6px 0 2px }
63
+ .commit .diff, .wt-wrap .diff { padding: 8px 12px 10px }
64
+
65
+ /* 統一分歧(unified diff):等寬、可橫向捲動,+/- 綠紅底色。 */
66
+ .dpre {
67
+ margin: 0; background: var(--code); border: 1px solid var(--line); border-radius: 10px;
68
+ overflow: auto; padding: 8px 0; white-space: pre;
69
+ font: 12.5px/1.55 ui-monospace, Consolas, "Courier New", monospace;
70
+ }
71
+ .dline { display: block; padding: 0 12px }
72
+ .dmark { display: inline-block; width: 1.3em; color: var(--mut); user-select: none }
73
+ .d-add { background: rgba(16, 185, 129, .14) }
74
+ .d-del { background: rgba(239, 68, 68, .13) }
75
+ .d-add .dmark { color: #059669 } .d-del .dmark { color: #dc2626 }
76
+ .d-hunk { color: #8b5cf6; background: rgba(139, 92, 246, .08) }
77
+ .d-file { color: var(--mut); font-weight: 600 }
78
+ .d-meta { color: var(--mut) }
79
+
80
+ /* 靜態匯出:<details class="commit">(動態版用 button.commit-head,故這些規則不衝突)。 */
81
+ details.commit > summary { padding: 8px 12px; cursor: pointer }
82
+ details.commit > summary::marker { color: var(--mut) }
83
+ details.commit > summary:hover { background: var(--line2) }
84
+ details.commit > summary .cbody { display: block }
85
+ details.commit > summary .commit-top { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap }
86
+ details.commit > summary .hash { color: var(--accent); font-size: 12.5px; background: none; padding: 0 }
87
+ details.commit > summary .when { color: var(--mut); font-size: 12px; white-space: nowrap }
88
+ details.commit > summary .subject { display: block; font-weight: 600; white-space: normal; overflow-wrap: anywhere }
89
+ details.commit > .commit-files { border-top: 1px solid var(--line2) }
90
+ details.commit > .dpre { margin: 8px 12px 10px }
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="{{lang_html}}"><head>
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>{{t.dev.title}}</title>
6
+ <script src="/assets/theme.js"></script>
7
+ <script src="/assets/i18n.js"></script>
8
+ <script>window.CK_I18N = {{i18n_json}};</script>
9
+ <link rel="stylesheet" href="/assets/theme.css">
10
+ <link rel="stylesheet" href="/assets/dev.css">
11
+ </head><body>
12
+ <header class="appbar">
13
+ <h1>{{t.dev.title}}</h1>
14
+ <a href="/">{{t.nav.history}}</a>
15
+ <a href="/about">{{t.nav.about}}</a>
16
+ <input id="search" aria-label="{{t.dev.search}}" placeholder="{{t.dev.search}}">
17
+ <select id="repo" aria-label="{{t.dev.repo_all}}"><option value="">{{t.dev.repo_all}}</option></select>
18
+ <button id="reload" aria-label="{{t.dev.reload}}" title="{{t.dev.reload}}">↻</button>
19
+ <button id="theme" aria-label="{{t.btn.theme}}" title="{{t.btn.theme}}">🌙</button>
20
+ </header>
21
+ <main>
22
+ <p class="muted" id="hint">{{t.dev.hint}}</p>
23
+ <nav id="tabs"></nav>
24
+ <div id="message" role="status" class="muted">{{t.dev.loading}}</div>
25
+ <div id="filters" hidden></div>
26
+ <section id="results"></section>
27
+ </main>
28
+ <script src="/assets/dev.js"></script>
29
+ </body></html>
@@ -0,0 +1,244 @@
1
+ /* 開發索引:功能/API 宣告/文件/近期改動。資料來自 /development/data。 */
2
+ const $ = (s) => document.querySelector(s);
3
+ let data = null, tab = "features", lang = "", kind = "";
4
+
5
+ const LABELS = {
6
+ features: "tab.features",
7
+ symbols: "tab.symbols",
8
+ documents: "tab.documents",
9
+ repos: "tab.repos",
10
+ };
11
+
12
+ function esc(s) {
13
+ return String(s ?? "").replace(/[&<>"']/g, (c) => (
14
+ { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]
15
+ ));
16
+ }
17
+
18
+ /* 行內 markdown(用於註釋、摘要):跳脫後套用 code/bold/link。 */
19
+ function mdInline(s) {
20
+ s = esc(s);
21
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
22
+ s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
23
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
24
+ return s;
25
+ }
26
+
27
+ /* 把相鄰 /// 註釋字串轉成簡單段落/條列。 */
28
+ function mdDoc(text) {
29
+ if (!text) return "";
30
+ const lines = text.split("\n");
31
+ let out = "", list = [];
32
+ const flush = () => { if (list.length) { out += "<ul>" + list.map((x) => "<li>" + mdInline(x) + "</li>").join("") + "</ul>"; list = []; } };
33
+ for (const l of lines) {
34
+ const m = l.match(/^\s*[-*]\s+(.*)$/);
35
+ if (m) { list.push(m[1]); continue; }
36
+ flush();
37
+ if (l.trim()) out += "<p>" + mdInline(l) + "</p>";
38
+ }
39
+ flush();
40
+ return out;
41
+ }
42
+
43
+ function kindOf(name) {
44
+ const m = name.match(/\b(func|struct|class|enum|protocol|actor)\b/);
45
+ return m ? m[1] : "decl";
46
+ }
47
+
48
+ function links(items) {
49
+ return items.map((p) => `<a href="/development/file?repo=app&path=${encodeURIComponent(p)}">${esc(p)}</a>`).join("");
50
+ }
51
+
52
+ /* ── 近期改動:提交卡片與 lazy 差異 ── */
53
+
54
+ function commitCard(repo, c) {
55
+ const files = (c.files || []).map((f) =>
56
+ `<a class="cfile" href="#" data-repo="${esc(repo)}" data-hash="${esc(c.hash)}" data-path="${esc(f)}">${esc(f)}</a>`
57
+ ).join("");
58
+ const when = (c.date || "").slice(0, 16).replace("T", " ");
59
+ return `<div class="commit" data-repo="${esc(repo)}" data-hash="${esc(c.hash)}">` +
60
+ `<button class="commit-head" type="button"><span class="commit-top"><code class="hash">${esc(c.hash)}</code>` +
61
+ `<span class="when">${esc(when)}</span></span>` +
62
+ `<span class="subject">${esc(c.subject)}</span></button>` +
63
+ `<div class="commit-files">${files || `<span class="muted">${CK_T("dev.no_files")}</span>`}</div>` +
64
+ `<div class="diff" hidden></div></div>`;
65
+ }
66
+
67
+ function diffLineClass(l) {
68
+ if (l.startsWith("@@")) return "d-hunk";
69
+ if (l.startsWith("+++") || l.startsWith("---") || l.startsWith("diff --git")) return "d-file";
70
+ if (l.startsWith("+")) return "d-add";
71
+ if (l.startsWith("-")) return "d-del";
72
+ if (/^(index |new file|deleted file|similarity|rename |old mode|new mode|Binary files)/.test(l) ||
73
+ l.startsWith("\\ No newline")) return "d-meta";
74
+ if (l.startsWith(" ")) return "d-ctx";
75
+ return "d-plain";
76
+ }
77
+
78
+ function renderDiff(j) {
79
+ if (j.empty) return '<p class="muted">' + CK_T("dev.diff_empty") + "</p>";
80
+ const hl = window.HL;
81
+ const swift = hl ? hl.swift : (t) => esc(t);
82
+ const esc2 = hl ? hl.esc : esc;
83
+ const state = { block: false };
84
+ const body = j.diff.split("\n").map((l) => {
85
+ const cls = diffLineClass(l);
86
+ if (cls === "d-file") state.block = false;
87
+ if (cls === "d-add" || cls === "d-del" || cls === "d-ctx") {
88
+ const mark = l[0] || " ";
89
+ return `<span class="dline ${cls}"><span class="dmark">${esc2(mark)}</span>${swift(l.slice(1), state)}</span>`;
90
+ }
91
+ return `<span class="dline ${cls}"><span class="dmark"> </span>${esc2(l)}</span>`;
92
+ }).join("");
93
+ return '<pre class="dpre">' + body + "</pre>" +
94
+ (j.truncated ? '<p class="muted">' + CK_T("dev.diff_truncated") + "</p>" : "");
95
+ }
96
+
97
+ const diffCache = new Map();
98
+ async function loadDiff(box, params) {
99
+ const qs = params.toString();
100
+ if (diffCache.has(qs)) { box.innerHTML = diffCache.get(qs); box.hidden = false; return; }
101
+ box.hidden = false;
102
+ box.innerHTML = '<p class="muted">' + CK_T("dev.diff_loading") + "</p>";
103
+ try {
104
+ const res = await fetch("/development/diff?" + qs);
105
+ const j = await res.json();
106
+ if (!res.ok) throw Error(j.error || res.statusText);
107
+ const html = renderDiff(j);
108
+ diffCache.set(qs, html);
109
+ box.innerHTML = html;
110
+ } catch (e) {
111
+ box.innerHTML = '<p class="empty">' + CK_T("dev.load_failed") + esc(e.message) + "</p>";
112
+ }
113
+ }
114
+
115
+ /* 「API 宣告」頁的篩選列:支援語言 + 語言 chips + 種類下拉。 */
116
+ function renderFilters() {
117
+ const box = $("#filters");
118
+ if (tab !== "symbols") { box.hidden = true; box.innerHTML = ""; return; }
119
+ box.hidden = false;
120
+ const symbols = data.symbols || [];
121
+ const langs = [...new Set(symbols.map((s) => s.lang).filter(Boolean))];
122
+ const kinds = [...new Set(symbols.map((s) => s.kind).filter(Boolean))].sort();
123
+ const chip = (value, label) =>
124
+ `<button class="chip${value === lang ? " on" : ""}" data-lang="${esc(value)}">${esc(label)}</button>`;
125
+ box.innerHTML =
126
+ `<div class="supported">${CK_T("filter.supported")}${esc((data.languages || []).join("、"))}</div>` +
127
+ `<div class="chips">${chip("", CK_T("filter.all_langs"))}${langs.map((l) => chip(l, l)).join("")}</div>` +
128
+ `<label class="kindfilter">${CK_T("filter.kind")} <select id="kindsel"><option value="">${CK_T("filter.all")}</option>` +
129
+ kinds.map((k) => `<option value="${esc(k)}"${k === kind ? " selected" : ""}>${esc(k)}</option>`).join("") +
130
+ `</select></label>`;
131
+ }
132
+
133
+ function render() {
134
+ if (!data) return;
135
+ document.querySelectorAll("#tabs button").forEach((b) =>
136
+ b.setAttribute("aria-pressed", b.dataset.key === tab));
137
+ renderFilters();
138
+ const q = $("#search").value.toLowerCase();
139
+ const repo = $("#repo").value;
140
+ const match = (r) => JSON.stringify(r).toLowerCase().includes(q) &&
141
+ (!repo || r.repo === repo || (r.id && tab === "features")) &&
142
+ (tab !== "symbols" || ((!lang || r.lang === lang) && (!kind || r.kind === kind)));
143
+
144
+ const rows = (data[tab] || []).filter(match);
145
+ const suffix = tab === "symbols" ? CK_T("dev.filter_hint") : "";
146
+ $("#message").textContent = CK_T("dev.result", rows.length) + suffix;
147
+
148
+ let out = "";
149
+ if (tab === "repos") {
150
+ const deps = (data.lockedPackages || []).map((p) =>
151
+ `<code>${esc(p.identity)}</code><code>${esc((p.revision || "").slice(0, 12))}</code>`).join("");
152
+ out += '<article class="card"><h2>' + CK_T("dev.locked") + '</h2><div class="deps">' + (deps || "<code>(無)</code><code></code>") + "</div>" +
153
+ '<p class=muted>' + CK_T("dev.locked_note") + "</p></article>";
154
+ }
155
+ for (const r of rows.slice(0, 250)) {
156
+ if (tab === "features") {
157
+ /* 通用渲染:保留鍵以外的所有欄位逐列列出;標籤可用該筆的 labels 對照,預設用 key。 */
158
+ const reserved = new Set(["id", "name", "summary", "files", "docs", "labels"]);
159
+ const labels = r.labels || {};
160
+ const fields = Object.keys(r).filter((k) => !reserved.has(k))
161
+ .map((k) => [labels[k] || k, Array.isArray(r[k]) ? r[k].join("、") : r[k]])
162
+ .map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join("");
163
+ out += `<article class="card"><h2>${esc(r.name)}</h2><div class="meta">${esc(r.id)}</div>` +
164
+ (r.summary ? `<p>${mdInline(r.summary)}</p>` : "") +
165
+ (fields ? `<dl>${fields}</dl>` : "") +
166
+ `<h3>${CK_T("dev.files")}</h3><div class="files">${links(r.files)}${links(r.docs)}</div></article>`;
167
+ } else if (tab === "symbols") {
168
+ out += `<article class="card"><h2><a href="${esc(r.url)}">${esc(r.name)}</a></h2>` +
169
+ `<div class="meta"><span class="kind">${esc(r.kind || kindOf(r.name))}</span>${esc(r.repo)} · ${esc(r.file)}:${r.line}</div>` +
170
+ (r.signature ? `<pre class="sig">${esc(r.signature)}</pre>` : "") +
171
+ `<div class="doc">${r.comment ? mdDoc(r.comment) : '<p class="muted">' + CK_T("dev.no_comment") + '</p>'}</div></article>`;
172
+ } else if (tab === "documents") {
173
+ out += `<article class="card"><h2><a href="${esc(r.url)}">${esc(r.name)}</a></h2>` +
174
+ `<div class="meta">${esc(r.repo)}</div><div class="preview">${esc(r.text.slice(0, 260))}${r.text.length > 260 ? " …" : ""}</div></article>`;
175
+ } else {
176
+ const list = (r.commits || []).map((c) => commitCard(r.name, c)).join("");
177
+ out += `<article class="card"><h2>${esc(r.name)}</h2><div class="meta">HEAD:${esc(r.head)}</div>` +
178
+ `<h3>${CK_T("dev.uncommitted")}</h3><pre class="blob">${esc(r.changes || CK_T("dev.clean"))}</pre>` +
179
+ `<div class="wt-wrap"><button class="wt-btn" type="button" data-repo="${esc(r.name)}">${CK_T("dev.worktree")}</button>` +
180
+ `<div class="diff" hidden></div></div>` +
181
+ `<h3>${CK_T("dev.commits20")}</h3><div class="commits">${list}</div></article>`;
182
+ }
183
+ }
184
+ if (rows.length > 250) out += '<p class="muted">' + CK_T("dev.more") + "</p>";
185
+ $("#results").innerHTML = out || '<p class="empty">' + CK_T("dev.no_data") + "</p>";
186
+ }
187
+
188
+ async function load() {
189
+ $("#message").textContent = CK_T("dev.loading");
190
+ diffCache.clear();
191
+ try {
192
+ const response = await fetch("/development/data");
193
+ const result = await response.json();
194
+ if (!response.ok) throw Error(result.error);
195
+ data = result;
196
+ const repos = [...new Set((data.symbols.concat(data.documents)).map((r) => r.repo))].sort();
197
+ const sel = $("#repo");
198
+ sel.innerHTML = '<option value="">' + CK_T("dev.repo_all") + '</option>' +
199
+ repos.map((r) => `<option value="${esc(r)}">${esc(r)}</option>`).join("");
200
+ render();
201
+ } catch (e) {
202
+ $("#message").textContent = CK_T("dev.load_failed") + e.message;
203
+ }
204
+ }
205
+
206
+ for (const [key, title] of Object.entries(LABELS)) {
207
+ const b = document.createElement("button");
208
+ b.textContent = CK_T(title); b.dataset.key = key;
209
+ b.onclick = () => { tab = key; render(); };
210
+ $("#tabs").append(b);
211
+ }
212
+ $("#search").oninput = render;
213
+ $("#repo").onchange = render;
214
+ $("#reload").onclick = load;
215
+
216
+ $("#filters").addEventListener("click", (e) => {
217
+ const chip = e.target.closest(".chip");
218
+ if (chip) { lang = chip.dataset.lang; render(); }
219
+ });
220
+ $("#filters").addEventListener("change", (e) => {
221
+ if (e.target.id === "kindsel") { kind = e.target.value; render(); }
222
+ });
223
+
224
+ /* 事件委派:提交標題、檔名、工作區差異鈕(#results 每次 render 會重建)。 */
225
+ $("#results").addEventListener("click", (e) => {
226
+ const file = e.target.closest("a.cfile");
227
+ if (file) {
228
+ e.preventDefault();
229
+ loadDiff(file.closest(".commit").querySelector(".diff"),
230
+ new URLSearchParams({ repo: file.dataset.repo, hash: file.dataset.hash, path: file.dataset.path }));
231
+ return;
232
+ }
233
+ const head = e.target.closest(".commit-head");
234
+ if (head) {
235
+ const box = head.parentElement.querySelector(".diff");
236
+ if (!box.hidden && box.innerHTML) { box.hidden = true; return; }
237
+ loadDiff(box, new URLSearchParams({ repo: head.parentElement.dataset.repo, hash: head.parentElement.dataset.hash }));
238
+ return;
239
+ }
240
+ const wt = e.target.closest(".wt-btn");
241
+ if (wt) loadDiff(wt.parentElement.querySelector(".diff"), new URLSearchParams({ repo: wt.dataset.repo, worktree: "1" }));
242
+ });
243
+
244
+ load();
@@ -0,0 +1,51 @@
1
+ /* 檔案檢視頁:行號、行跳轉、目標行高亮與語法上色。主題與共用元件在 theme.css。 */
2
+ .appbar .spacer { flex: 1 }
3
+ .appbar #jump { width: 92px }
4
+ .wrap { height: calc(100vh - 53px); overflow: auto }
5
+ #src {
6
+ margin: 0; padding: 6px 0 40px;
7
+ font: 13px/1.55 ui-monospace, Consolas, "Courier New", monospace;
8
+ white-space: pre; min-width: max-content;
9
+ }
10
+ #src .ln { display: block; padding: 0 16px 0 0 }
11
+ #src .ln:hover { background: var(--line2) }
12
+ #src .ln:target, #src .ln.hit { background: rgba(245, 158, 11, .24) }
13
+ #src .no {
14
+ color: var(--mut); text-decoration: none; display: inline-block;
15
+ width: 4.6em; text-align: right; margin-right: 14px; user-select: none;
16
+ }
17
+ #src .no:hover { color: var(--accent) }
18
+
19
+ @media (max-width: 640px) { .appbar #name { max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap } }
20
+
21
+ /* Markdown 排版檢視 */
22
+ .wrap > [hidden] { display: none }
23
+ .rendered { max-width: 920px; margin: 0 auto; padding: 22px 22px 64px; line-height: 1.75 }
24
+ .rendered h1, .rendered h2, .rendered h3, .rendered h4 { line-height: 1.3; margin: 1.4em 0 .5em }
25
+ .rendered h1 { font-size: 1.7em; border-bottom: 1px solid var(--line); padding-bottom: .3em }
26
+ .rendered h2 { font-size: 1.35em; border-bottom: 1px solid var(--line); padding-bottom: .25em }
27
+ .rendered h3 { font-size: 1.15em }
28
+ .rendered p, .rendered ul, .rendered ol, .rendered table, .rendered pre, .rendered blockquote, .rendered .alert { margin: .7em 0 }
29
+ .rendered ul, .rendered ol { padding-left: 1.6em }
30
+ .rendered li { margin: .2em 0 }
31
+ .rendered img { max-width: 100%; border-radius: 8px }
32
+ .rendered hr { border: 0; border-top: 1px solid var(--line); margin: 1.4em 0 }
33
+ .rendered table { border-collapse: collapse; width: 100%; display: block; overflow-x: auto }
34
+ .rendered th, .rendered td { border: 1px solid var(--line); padding: 6px 10px; text-align: left }
35
+ .rendered th { background: var(--code) }
36
+ .rendered blockquote { border-left: 3px solid var(--line); margin-left: 0; padding: .2em 0 .2em 1em; color: var(--mut) }
37
+
38
+ /* GitHub 提示框(> [!NOTE] 等) */
39
+ .rendered .alert { border: 1px solid var(--line); border-left-width: 4px; border-radius: 8px; padding: 10px 14px }
40
+ .rendered .alert-title { margin: 0 0 .3em; font-weight: 600; display: flex; align-items: center; gap: .4em }
41
+ .rendered .alert p:last-child { margin-bottom: 0 }
42
+ .alert-note { border-left-color: #3b82f6; background: rgba(59, 130, 246, .10) }
43
+ .alert-tip { border-left-color: #10b981; background: rgba(16, 185, 129, .10) }
44
+ .alert-important { border-left-color: #8b5cf6; background: rgba(139, 92, 246, .10) }
45
+ .alert-warning { border-left-color: #f59e0b; background: rgba(245, 158, 11, .12) }
46
+ .alert-caution { border-left-color: #ef4444; background: rgba(239, 68, 68, .10) }
47
+ .alert-note .alert-title { color: #3b82f6 }
48
+ .alert-tip .alert-title { color: #10b981 }
49
+ .alert-important .alert-title { color: #8b5cf6 }
50
+ .alert-warning .alert-title { color: #f59e0b }
51
+ .alert-caution .alert-title { color: #ef4444 }
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="{{lang_html}}"><head>
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>{{title}} — {{t.dev.title}}</title>
6
+ <script src="/assets/theme.js"></script>
7
+ <script src="/assets/i18n.js"></script>
8
+ <script>window.CK_I18N = {{i18n_json}};</script>
9
+ <link rel="stylesheet" href="/assets/theme.css">
10
+ <link rel="stylesheet" href="/assets/file.css">
11
+ </head><body>
12
+ <header class="appbar">
13
+ <a href="/development" id="back">{{t.file.back}}</a>
14
+ <strong id="name">{{title}}</strong>
15
+ <span class="muted">{{repo}}</span>
16
+ <span class="spacer"></span>
17
+ <button id="mode" hidden>{{t.file.source}}</button>
18
+ <input id="jump" type="number" min="1" placeholder="{{t.file.jump}}" aria-label="{{t.file.jump}}">
19
+ <button id="copy" title="{{t.file.copy}}">{{t.file.copy}}</button>
20
+ <button id="theme" aria-label="{{t.btn.theme}}" title="{{t.btn.theme}}">🌙</button>
21
+ </header>
22
+ <div class="wrap">
23
+ <pre id="src" data-repo="{{repo}}" data-path="{{path}}" data-line="{{line}}">{{content}}</pre>
24
+ <article id="rendered" class="rendered" hidden></article>
25
+ </div>
26
+ <script src="/assets/highlight.js"></script>
27
+ <script src="/assets/markdown.js"></script>
28
+ <script src="/assets/file.js"></script>
29
+ </body></html>
@@ -0,0 +1,101 @@
1
+ /* 檔案檢視:行號、行跳轉、目標行高亮;Swift 上色;Markdown 預設排版檢視(可切回原始碼)。 */
2
+ (function () {
3
+ const src = document.getElementById("src");
4
+ if (!src) return;
5
+ const rendered = document.getElementById("rendered");
6
+ const modeBtn = document.getElementById("mode");
7
+ const jump = document.getElementById("jump");
8
+ const isMarkdown = /\.(md|markdown)$/i.test(src.dataset.path || "");
9
+
10
+ // 1) 原始碼逐行上色(Markdown 也用於「原始碼」模式)。
11
+ const state = { block: false };
12
+ src.querySelectorAll(".code").forEach((el) => {
13
+ el.innerHTML = isMarkdown ? HL.markdownLine(el.textContent) : HL.swift(el.textContent, state);
14
+ });
15
+
16
+ const lines = src.querySelectorAll(".ln");
17
+ const total = lines.length;
18
+ const initial = parseInt(src.dataset.line || "0", 10);
19
+
20
+ function highlight(line) {
21
+ if (!(line >= 1 && line <= total)) return;
22
+ src.querySelectorAll(".ln.hit").forEach((el) => el.classList.remove("hit"));
23
+ const el = document.getElementById("L" + line);
24
+ if (el) { el.classList.add("hit"); el.scrollIntoView({ block: "center" }); }
25
+ }
26
+ function fromHash() {
27
+ const m = (location.hash || "").match(/^#L(\d+)$/);
28
+ return m ? parseInt(m[1], 10) : initial;
29
+ }
30
+ function go(line, push) {
31
+ if (!(line >= 1 && line <= total)) return;
32
+ if (push) history.replaceState(null, "", "#L" + line);
33
+ highlight(line);
34
+ }
35
+
36
+ src.addEventListener("click", (e) => {
37
+ const a = e.target.closest ? e.target.closest("a.no") : null;
38
+ const href = a && a.getAttribute("href");
39
+ if (href && href.indexOf("#L") === 0) { e.preventDefault(); go(parseInt(href.slice(2), 10), true); }
40
+ });
41
+
42
+ if (jump) {
43
+ jump.max = total;
44
+ jump.addEventListener("keydown", (e) => {
45
+ if (e.key === "Enter") { e.preventDefault(); go(parseInt(jump.value, 10), true); }
46
+ });
47
+ }
48
+
49
+ document.getElementById("copy")?.addEventListener("click", async (e) => {
50
+ const path = src.dataset.path || "";
51
+ try { await navigator.clipboard.writeText(path); e.currentTarget.textContent = CK_T("file.copied"); }
52
+ catch { e.currentTarget.textContent = path; }
53
+ setTimeout(() => { e.currentTarget.textContent = CK_T("file.copy"); }, 1500);
54
+ });
55
+
56
+ document.getElementById("back")?.addEventListener("click", (e) => {
57
+ if (history.length > 1) { e.preventDefault(); history.back(); }
58
+ });
59
+
60
+ window.addEventListener("hashchange", () => { if (!src.hidden) highlight(fromHash()); });
61
+
62
+ // 2) Markdown:排版檢視(預設)+可切回原始碼。
63
+ function resolveHref(href) {
64
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#") || href.startsWith("/")) return href;
65
+ const repo = src.dataset.repo || "app";
66
+ const [clean, ...rest] = href.split("#");
67
+ const base = (src.dataset.path || "").split("/").slice(0, -1);
68
+ for (const seg of clean.split("/")) {
69
+ if (seg === "" || seg === ".") continue;
70
+ if (seg === "..") { base.pop(); continue; }
71
+ base.push(seg);
72
+ }
73
+ const anchor = rest.length ? "#" + rest.join("#") : "";
74
+ return "/development/file?" + new URLSearchParams({ repo, path: base.join("/") }).toString() + anchor;
75
+ }
76
+
77
+ function setMode(mode) {
78
+ const renderedMode = mode === "rendered";
79
+ src.hidden = renderedMode;
80
+ if (rendered) rendered.hidden = !renderedMode;
81
+ if (jump) jump.hidden = renderedMode;
82
+ if (modeBtn) modeBtn.textContent = renderedMode ? CK_T("file.source") : CK_T("file.rendered");
83
+ }
84
+
85
+ if (isMarkdown && rendered && modeBtn) {
86
+ const raw = Array.from(src.querySelectorAll(".code")).map((el) => el.textContent).join("\n");
87
+ rendered.innerHTML = window.renderMarkdown(raw, resolveHref);
88
+ // 文件內的 #L 行號連結:切回原始碼模式再跳行(就地,不重載)。
89
+ rendered.addEventListener("click", (e) => {
90
+ const a = e.target.closest ? e.target.closest("a") : null;
91
+ const href = a && a.getAttribute("href");
92
+ if (href && href.startsWith("#L")) { e.preventDefault(); setMode("source"); go(parseInt(href.slice(2), 10), true); }
93
+ });
94
+ modeBtn.hidden = false;
95
+ let mode = "rendered";
96
+ setMode(mode);
97
+ modeBtn.addEventListener("click", () => { mode = mode === "rendered" ? "source" : "rendered"; setMode(mode); });
98
+ } else {
99
+ highlight(fromHash());
100
+ }
101
+ })();