petari 0.3.0 → 0.4.1

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 (163) hide show
  1. package/README.md +30 -6
  2. package/dist/cli.js +4 -1
  3. package/dist/commands/show.js +239 -40
  4. package/dist/core/diff-html.js +272 -0
  5. package/dist/core/diff.js +177 -0
  6. package/dist/core/edit.js +83 -0
  7. package/dist/core/editor-html.js +301 -0
  8. package/dist/infra/browser.js +24 -0
  9. package/dist/infra/diff-server.js +437 -0
  10. package/package.json +7 -4
  11. package/vendor/monaco/LICENSE.txt +21 -0
  12. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  13. package/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  14. package/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  15. package/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  16. package/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  17. package/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  18. package/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  19. package/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  20. package/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  21. package/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  22. package/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  23. package/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  24. package/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  25. package/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  26. package/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  27. package/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  28. package/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  29. package/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  30. package/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  31. package/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  32. package/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  33. package/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  34. package/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  35. package/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  36. package/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  37. package/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  38. package/vendor/monaco/vs/editor/editor.main.css +1 -0
  39. package/vendor/monaco/vs/editor/editor.main.js +1 -0
  40. package/vendor/monaco/vs/editor/editor.worker.js +1 -0
  41. package/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  42. package/vendor/monaco/vs/editor.js +1 -0
  43. package/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  44. package/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  45. package/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  46. package/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  47. package/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  48. package/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  49. package/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  50. package/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  51. package/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  52. package/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  53. package/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  54. package/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  55. package/vendor/monaco/vs/index-049xrXrp.js +1 -0
  56. package/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  57. package/vendor/monaco/vs/index.js +1 -0
  58. package/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  59. package/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  60. package/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  61. package/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  62. package/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  63. package/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  64. package/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  65. package/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  66. package/vendor/monaco/vs/language/css/css.worker.js +66 -0
  67. package/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  68. package/vendor/monaco/vs/language/html/html.worker.js +443 -0
  69. package/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  70. package/vendor/monaco/vs/language/json/json.worker.js +17 -0
  71. package/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  72. package/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  73. package/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  74. package/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  75. package/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  76. package/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  77. package/vendor/monaco/vs/loader.js +1368 -0
  78. package/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  79. package/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  80. package/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  81. package/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  82. package/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  83. package/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  84. package/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  85. package/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  86. package/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  87. package/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  88. package/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  89. package/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  90. package/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  91. package/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  92. package/vendor/monaco/vs/nls/lang/cs.d.ts +1 -0
  93. package/vendor/monaco/vs/nls/lang/cs.js +9 -0
  94. package/vendor/monaco/vs/nls/lang/de.d.ts +1 -0
  95. package/vendor/monaco/vs/nls/lang/de.js +9 -0
  96. package/vendor/monaco/vs/nls/lang/es.d.ts +1 -0
  97. package/vendor/monaco/vs/nls/lang/es.js +9 -0
  98. package/vendor/monaco/vs/nls/lang/fr.d.ts +1 -0
  99. package/vendor/monaco/vs/nls/lang/fr.js +9 -0
  100. package/vendor/monaco/vs/nls/lang/it.d.ts +1 -0
  101. package/vendor/monaco/vs/nls/lang/it.js +9 -0
  102. package/vendor/monaco/vs/nls/lang/ja.d.ts +1 -0
  103. package/vendor/monaco/vs/nls/lang/ja.js +9 -0
  104. package/vendor/monaco/vs/nls/lang/ko.d.ts +1 -0
  105. package/vendor/monaco/vs/nls/lang/ko.js +9 -0
  106. package/vendor/monaco/vs/nls/lang/pl.d.ts +1 -0
  107. package/vendor/monaco/vs/nls/lang/pl.js +9 -0
  108. package/vendor/monaco/vs/nls/lang/pt-br.d.ts +1 -0
  109. package/vendor/monaco/vs/nls/lang/pt-br.js +9 -0
  110. package/vendor/monaco/vs/nls/lang/ru.d.ts +1 -0
  111. package/vendor/monaco/vs/nls/lang/ru.js +9 -0
  112. package/vendor/monaco/vs/nls/lang/tr.d.ts +1 -0
  113. package/vendor/monaco/vs/nls/lang/tr.js +9 -0
  114. package/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  115. package/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  116. package/vendor/monaco/vs/nls/lang/zh-tw.d.ts +1 -0
  117. package/vendor/monaco/vs/nls/lang/zh-tw.js +9 -0
  118. package/vendor/monaco/vs/nls.messages-loader.js +1 -0
  119. package/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  120. package/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  121. package/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  122. package/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  123. package/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  124. package/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  125. package/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  126. package/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  127. package/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  128. package/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  129. package/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  130. package/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  131. package/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  132. package/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  133. package/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  134. package/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  135. package/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  136. package/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  137. package/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  138. package/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  139. package/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  140. package/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  141. package/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  142. package/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  143. package/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  144. package/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  145. package/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  146. package/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  147. package/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  148. package/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  149. package/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  150. package/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  151. package/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  152. package/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  153. package/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  154. package/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  155. package/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  156. package/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  157. package/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  158. package/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  159. package/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  160. package/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  161. package/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  162. package/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  163. package/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * ブラウザ差分ビューの HTML 生成 (§4.3)。純粋な文字列生成のみ (I/O なし)。
3
+ *
4
+ * セキュリティ方針:
5
+ * - 補間箇所は全て escapeHtml を通す。ファイル内容は非信頼入力 (AI 出力) として扱い、
6
+ * </textarea> や <script> の注入を無効化する
7
+ * - JS は一切使わない (フォーム POST のみ)。外部リソース参照ゼロ・インライン CSS のみ。
8
+ * 未変更範囲の折りたたみも <details> (JS 不要) で行う
9
+ * - textarea の値はエスケープ以外の加工禁止。制御文字を除去すると無編集行が
10
+ * 「変更」扱いになり、保存時にユーザーが触っていない行の文字が消えるため。
11
+ * 読み取り専用の diff ペインのみ表示用に双方向制御文字等を除去する
12
+ * - フォームの action とリンクは相対パスのみ (URL のトークンを HTML 本文に埋めない)
13
+ */
14
+ import { intralineRanges } from "./diff.js";
15
+ /** 1 ファイルの diff 表示上限行数 (巨大ファイルでのメモリ・描画保護) */
16
+ export const MAX_SECTION_ROWS = 20_000;
17
+ /** 変更行の前後に見せる未変更行数と、折りたたみを発動する最小行数 */
18
+ const FOLD_CONTEXT = 3;
19
+ const FOLD_MIN = 10;
20
+ export function escapeHtml(s) {
21
+ return s
22
+ .replaceAll("&", "&amp;")
23
+ .replaceAll("<", "&lt;")
24
+ .replaceAll(">", "&gt;")
25
+ .replaceAll('"', "&quot;")
26
+ .replaceAll("'", "&#39;");
27
+ }
28
+ // 表示専用の無害化 (term.ts と同じ集合: C0/C1 制御と Trojan Source 双方向制御)。
29
+ // textarea には適用しないこと (上記セキュリティ方針)
30
+ const DISPLAY_CONTROL_CHARS = new RegExp("[\\u0000-\\u0008\\u000B-\\u001F\\u007F-\\u009F\\u061C\\u200E\\u200F\\u202A-\\u202E\\u2066-\\u2069]", "g");
31
+ function displayText(s) {
32
+ return escapeHtml(s.replace(DISPLAY_CONTROL_CHARS, ""));
33
+ }
34
+ /** 全ページ共通の CSP。サーバー応答ヘッダにも同じ値を使う (meta の form-action 等は
35
+ * 無視されるブラウザがあるため、サーバー配信時はヘッダ側が正) */
36
+ export const CSP = "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'";
37
+ const STYLE = `
38
+ :root{
39
+ --bg:#ffffff;--fg:#1f2328;--muted:#59636e;--border:#d1d9e0;--surface:#f6f8fa;
40
+ --accent:#0969da;--ok:#1a7f37;--err:#d1242f;--rewrite:#8250df;
41
+ --del-bg:#ffebe9;--del-hl:#ffb3ad;--ins-bg:#dafbe1;--ins-hl:#a5e8b7;
42
+ --radius:8px;
43
+ }
44
+ @media (prefers-color-scheme: dark){:root{
45
+ --bg:#0d1117;--fg:#e6edf3;--muted:#8d96a0;--border:#30363d;--surface:#161b22;
46
+ --accent:#4493f8;--ok:#3fb950;--err:#f85149;--rewrite:#ab7df8;
47
+ --del-bg:#3b1a1d;--del-hl:#792e34;--ins-bg:#122b1d;--ins-hl:#1c5b34;
48
+ }}
49
+ *{box-sizing:border-box}
50
+ body{margin:0;background:var(--bg);color:var(--fg);line-height:1.5;
51
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif}
52
+ code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
53
+ .wrap{max-width:1180px;margin:0 auto;padding:1.2rem 1.4rem 4rem}
54
+ header.page h1{font-size:1.25rem;margin:.2rem 0}
55
+ header.page .meta{color:var(--muted);font-size:.82rem;margin:.1rem 0 0}
56
+ nav.files{position:sticky;top:0;z-index:2;display:flex;flex-wrap:wrap;gap:.4rem;
57
+ padding:.6rem 0;margin:.4rem 0 .2rem;background:var(--bg);border-bottom:1px solid var(--border)}
58
+ nav.files a{font-family:ui-monospace,Menlo,monospace;font-size:.78rem;text-decoration:none;
59
+ color:var(--accent);background:var(--surface);border:1px solid var(--border);
60
+ border-radius:2em;padding:.18em .7em}
61
+ nav.files a:hover{border-color:var(--accent)}
62
+ section.file{border:1px solid var(--border);border-radius:var(--radius);margin:1rem 0;
63
+ overflow:hidden;background:var(--bg)}
64
+ .fhead{display:flex;align-items:center;gap:.6em;padding:.55rem .8rem;
65
+ background:var(--surface);border-bottom:1px solid var(--border)}
66
+ .fhead code{font-size:.88rem;font-weight:600;word-break:break-all}
67
+ .op{font-size:.68rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
68
+ padding:.12em .6em;border-radius:2em;flex-shrink:0}
69
+ .op-replace{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}
70
+ .op-create{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
71
+ .op-delete{color:var(--err);background:color-mix(in srgb,var(--err) 14%,transparent)}
72
+ .op-rewrite{color:var(--rewrite);background:color-mix(in srgb,var(--rewrite) 14%,transparent)}
73
+ .dlabels{display:grid;grid-template-columns:1fr 1fr;font-size:.74rem;color:var(--muted);
74
+ background:var(--surface);border-bottom:1px solid var(--border)}
75
+ .dlabels div{padding:.28rem .7rem}
76
+ .dlabels div+div{border-left:1px solid var(--border)}
77
+ table.diff{border-collapse:collapse;width:100%;table-layout:fixed;
78
+ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;line-height:1.5}
79
+ col.cno{width:3.4em}
80
+ .diff td{vertical-align:top;padding:0 .55em}
81
+ .diff td.no{text-align:right;color:var(--muted);user-select:none;background:var(--surface);
82
+ border-right:1px solid var(--border);padding:0 .5em}
83
+ .diff td.no.split{border-left:1px solid var(--border)}
84
+ .diff td.code{white-space:pre-wrap;word-break:break-all}
85
+ .diff td.code.del{background:var(--del-bg)}
86
+ .diff td.code.ins{background:var(--ins-bg)}
87
+ .code.del .hl{background:var(--del-hl);border-radius:2px}
88
+ .code.ins .hl{background:var(--ins-hl);border-radius:2px}
89
+ .diff td.code.empty{background:repeating-linear-gradient(45deg,var(--surface),
90
+ var(--surface) 4px,var(--bg) 4px,var(--bg) 9px)}
91
+ details.fold summary{cursor:pointer;list-style:none;text-align:center;font-size:.76rem;
92
+ color:var(--accent);background:var(--surface);padding:.28rem .6rem;
93
+ border-top:1px solid var(--border);border-bottom:1px solid var(--border);user-select:none}
94
+ details.fold summary::-webkit-details-marker{display:none}
95
+ details.fold summary:hover{text-decoration:underline}
96
+ details.fold[open] summary{border-bottom-style:dashed}
97
+ p.note{color:var(--muted);font-size:.84rem;margin:.6rem .8rem}
98
+ .banner{border-radius:6px;padding:.55rem .9rem;margin:.7rem 0;font-size:.88rem;border:1px solid}
99
+ .banner.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);
100
+ background:color-mix(in srgb,var(--ok) 10%,transparent)}
101
+ .banner.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);
102
+ background:color-mix(in srgb,var(--err) 10%,transparent)}
103
+ section.editor{margin:1rem 0}
104
+ textarea{width:100%;min-height:24rem;padding:.8rem;resize:vertical;
105
+ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;line-height:1.5;
106
+ color:var(--fg);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius)}
107
+ textarea:focus{outline:2px solid var(--accent);outline-offset:-1px}
108
+ button{font-size:.88rem;font-weight:600;padding:.45em 1.4em;border-radius:6px;cursor:pointer;
109
+ border:1px solid transparent;background:var(--accent);color:#fff}
110
+ button:hover{filter:brightness(1.1)}
111
+ button.secondary{background:var(--surface);color:var(--muted);border-color:var(--border)}
112
+ .actions{display:flex;align-items:center;gap:.8rem;margin:.7rem 0}
113
+ .footer-actions{display:flex;align-items:center;gap:1rem;margin:1.6rem 0 0;
114
+ padding-top:.9rem;border-top:1px solid var(--border)}
115
+ .footer-actions form{margin:0}
116
+ a.back{color:var(--accent);text-decoration:none;font-size:.88rem}
117
+ a.back:hover{text-decoration:underline}
118
+ ul.list{list-style:none;margin:.8rem 0;padding:0;border:1px solid var(--border);
119
+ border-radius:var(--radius);overflow:hidden}
120
+ ul.list li{display:flex;align-items:center;gap:.7em;padding:.6rem .9rem;background:var(--bg)}
121
+ ul.list li+li{border-top:1px solid var(--border)}
122
+ ul.list a{color:var(--accent);text-decoration:none;font-family:ui-monospace,Menlo,monospace;
123
+ font-size:.9rem;word-break:break-all}
124
+ ul.list a:hover{text-decoration:underline}
125
+ ul.list .note{color:var(--muted);font-size:.78rem;margin-left:auto;flex-shrink:0}
126
+ `.trim();
127
+ function page(title, bodyHtml) {
128
+ return `<!doctype html>
129
+ <html lang="ja">
130
+ <head>
131
+ <meta charset="utf-8">
132
+ <meta http-equiv="Content-Security-Policy" content="${CSP}">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1">
134
+ <title>${escapeHtml(title)}</title>
135
+ <style>${STYLE}</style>
136
+ </head>
137
+ <body>
138
+ <div class="wrap">
139
+ ${bodyHtml}
140
+ </div>
141
+ </body>
142
+ </html>
143
+ `;
144
+ }
145
+ function opBadge(op) {
146
+ return `<span class="op op-${op}">${op}</span>`;
147
+ }
148
+ /** 文字単位ハイライト付きでコードを描画する (範囲は元テキストのコードポイント index) */
149
+ function renderCode(text, ranges) {
150
+ if (ranges.length === 0)
151
+ return displayText(text);
152
+ const chars = Array.from(text);
153
+ const parts = [];
154
+ let pos = 0;
155
+ for (const r of ranges) {
156
+ if (r.start > pos)
157
+ parts.push(displayText(chars.slice(pos, r.start).join("")));
158
+ parts.push(`<span class="hl">${displayText(chars.slice(r.start, r.end).join(""))}</span>`);
159
+ pos = r.end;
160
+ }
161
+ if (pos < chars.length)
162
+ parts.push(displayText(chars.slice(pos).join("")));
163
+ return parts.join("");
164
+ }
165
+ const COLGROUP = `<colgroup><col class="cno"><col><col class="cno"><col></colgroup>`;
166
+ function renderRow(row) {
167
+ const intra = row.kind === "change" && row.left !== null && row.right !== null
168
+ ? intralineRanges(row.left.text, row.right.text)
169
+ : null;
170
+ const cell = (side, cls, ranges, split) => {
171
+ const noCls = split ? "no split" : "no";
172
+ return side === null
173
+ ? `<td class="${noCls}"></td><td class="code empty"></td>`
174
+ : `<td class="${noCls}">${side.no}</td><td class="code${cls}">${renderCode(side.text, ranges)}</td>`;
175
+ };
176
+ const delCls = row.kind === "del" || row.kind === "change" ? " del" : "";
177
+ const insCls = row.kind === "ins" || row.kind === "change" ? " ins" : "";
178
+ return `<tr>${cell(row.left, delCls, intra?.left ?? [], false)}${cell(row.right, insCls, intra?.right ?? [], true)}</tr>`;
179
+ }
180
+ /** 変更行の前後 FOLD_CONTEXT 行だけを見せ、長い未変更ランは折りたたみ対象にする */
181
+ function segmentRows(rows) {
182
+ const keep = new Array(rows.length).fill(false);
183
+ rows.forEach((r, i) => {
184
+ if (r.kind !== "same") {
185
+ const from = Math.max(0, i - FOLD_CONTEXT);
186
+ const to = Math.min(rows.length - 1, i + FOLD_CONTEXT);
187
+ for (let j = from; j <= to; j++)
188
+ keep[j] = true;
189
+ }
190
+ });
191
+ const segs = [];
192
+ let i = 0;
193
+ while (i < rows.length) {
194
+ const k = keep[i];
195
+ let j = i;
196
+ while (j < rows.length && keep[j] === k)
197
+ j++;
198
+ const chunk = rows.slice(i, j);
199
+ segs.push({ fold: !k && chunk.length >= FOLD_MIN, rows: chunk });
200
+ i = j;
201
+ }
202
+ return segs;
203
+ }
204
+ function renderDiffBlocks(rows, leftLabel, rightLabel) {
205
+ const shown = rows.slice(0, MAX_SECTION_ROWS);
206
+ const omitted = rows.length - shown.length;
207
+ const parts = [
208
+ `<div class="dlabels"><div>${escapeHtml(leftLabel)}</div><div>${escapeHtml(rightLabel)}</div></div>`,
209
+ ];
210
+ if (shown.every((r) => r.kind === "same") && shown.length > 0) {
211
+ parts.push(`<p class="note">変更はありません (全 ${shown.length} 行一致)</p>`);
212
+ }
213
+ for (const seg of segmentRows(shown)) {
214
+ const table = `<table class="diff">${COLGROUP}${seg.rows.map(renderRow).join("\n")}</table>`;
215
+ if (seg.fold) {
216
+ const from = seg.rows[0]?.left?.no ?? seg.rows[0]?.right?.no ?? 0;
217
+ parts.push(`<details class="fold"><summary>未変更の ${seg.rows.length} 行を表示 (${from} 行目〜)</summary>${table}</details>`);
218
+ }
219
+ else {
220
+ parts.push(table);
221
+ }
222
+ }
223
+ if (omitted > 0)
224
+ parts.push(`<p class="note">以降 ${omitted} 行を省略しました</p>`);
225
+ return parts.join("\n");
226
+ }
227
+ function renderFileSection(s, id) {
228
+ const inner = s.body.kind === "note"
229
+ ? `<p class="note">${escapeHtml(s.body.note)}</p>`
230
+ : renderDiffBlocks(s.body.rows, s.body.leftLabel, s.body.rightLabel);
231
+ const anchor = id === null ? "" : ` id="${id}"`;
232
+ return `<section class="file"${anchor}>
233
+ <div class="fhead">${opBadge(s.op)}<code>${escapeHtml(s.path)}</code></div>
234
+ ${inner}
235
+ </section>`;
236
+ }
237
+ /** 静的差分レポート (全ファイルを 1 ページに)。閲覧専用・自己完結 */
238
+ export function buildReportPage(title, generatedAt, sections) {
239
+ const nav = sections.length > 1
240
+ ? `<nav class="files">${sections
241
+ .map((s, i) => `<a href="#f${i}">${escapeHtml(s.path)}</a>`)
242
+ .join("")}</nav>`
243
+ : "";
244
+ const body = sections.map((s, i) => renderFileSection(s, `f${i}`)).join("\n");
245
+ return page(title, `<header class="page"><h1>${escapeHtml(title)}</h1>
246
+ <p class="meta">${escapeHtml(generatedAt)} ・ ${sections.length} ファイル</p></header>
247
+ ${nav}
248
+ ${body}`);
249
+ }
250
+ /** 編集サーバーのファイル一覧ページ */
251
+ export function buildIndexPage(title, items, idleMinutes) {
252
+ const list = items
253
+ .map((item, i) => {
254
+ const note = item.note === null ? "" : `<span class="note">${escapeHtml(item.note)}</span>`;
255
+ return `<li>${opBadge(item.op)}<a href="f/${i}">${escapeHtml(item.path)}</a>${note}</li>`;
256
+ })
257
+ .join("\n");
258
+ const body = `<header class="page"><h1>${escapeHtml(title)}</h1>
259
+ <p class="meta">無操作 ${idleMinutes} 分でサーバーは自動終了します。編集途中の内容は保存されません。</p></header>
260
+ <ul class="list">
261
+ ${list}
262
+ </ul>
263
+ <div class="footer-actions"><form method="post" action="quit"><button class="secondary">サーバーを終了</button></form></div>`;
264
+ return page(title, body);
265
+ }
266
+ /** エラー・終了などの単文ページ (409 / 404 / 終了しました 等) */
267
+ export function buildMessagePage(title, message, backHref) {
268
+ const back = backHref === null
269
+ ? ""
270
+ : `\n<div class="footer-actions"><a class="back" href="${escapeHtml(backHref)}">← 戻る</a></div>`;
271
+ return page(title, `<header class="page"><h1>${escapeHtml(title)}</h1></header>\n<p>${escapeHtml(message)}</p>${back}`);
272
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * 行単位の diff (§4.3 ブラウザ差分ビュー用)。
3
+ *
4
+ * 不変条件: どんな入力でも「ops を a に適用すると b が完全再現される」。
5
+ * Myers の探索を maxD で打ち切った場合は中間部を全削除+全挿入として返すため、
6
+ * diff の品質はマージ (core/edit.ts) の正しさに影響しない — 打ち切りは
7
+ * raw バイトを保持できる行が減るだけ。
8
+ */
9
+ const DEFAULT_MAX_D = 2000;
10
+ /** 行配列 a → b の差分を返す。インデックスはともに 0-based */
11
+ export function diffLines(a, b, maxD = DEFAULT_MAX_D) {
12
+ // 共通の前置き・後置きを先に確定する (パッチ適用前後の比較では大半が共通)
13
+ let pre = 0;
14
+ while (pre < a.length && pre < b.length && a[pre] === b[pre])
15
+ pre++;
16
+ let suf = 0;
17
+ while (suf < a.length - pre &&
18
+ suf < b.length - pre &&
19
+ a[a.length - 1 - suf] === b[b.length - 1 - suf]) {
20
+ suf++;
21
+ }
22
+ const ops = [];
23
+ for (let i = 0; i < pre; i++)
24
+ ops.push({ kind: "same", a: i, b: i });
25
+ for (const op of diffMiddle(a.slice(pre, a.length - suf), b.slice(pre, b.length - suf), maxD)) {
26
+ if (op.kind === "same")
27
+ ops.push({ kind: "same", a: op.a + pre, b: op.b + pre });
28
+ else if (op.kind === "del")
29
+ ops.push({ kind: "del", a: op.a + pre });
30
+ else
31
+ ops.push({ kind: "ins", b: op.b + pre });
32
+ }
33
+ for (let i = 0; i < suf; i++) {
34
+ ops.push({ kind: "same", a: a.length - suf + i, b: b.length - suf + i });
35
+ }
36
+ return ops;
37
+ }
38
+ /** 全削除 + 全挿入 (共通行の探索なし)。maxD 打ち切り時のフォールバック */
39
+ function fallbackAll(n, m) {
40
+ const ops = [];
41
+ for (let i = 0; i < n; i++)
42
+ ops.push({ kind: "del", a: i });
43
+ for (let j = 0; j < m; j++)
44
+ ops.push({ kind: "ins", b: j });
45
+ return ops;
46
+ }
47
+ /** Myers O(ND) 貪欲法。a と b は共通の前置き・後置きを持たない前提 */
48
+ function diffMiddle(a, b, maxD) {
49
+ const n = a.length;
50
+ const m = b.length;
51
+ if (n === 0 || m === 0)
52
+ return fallbackAll(n, m);
53
+ // |k| <= d <= cap なので V 配列は 2*cap+1 で足りる (スナップショット総量の上限も抑える)
54
+ const cap = Math.min(n + m, maxD);
55
+ const offset = cap;
56
+ const v = new Int32Array(2 * cap + 1);
57
+ const trace = [];
58
+ let found = -1;
59
+ outer: for (let d = 0; d <= cap; d++) {
60
+ trace.push(v.slice());
61
+ for (let k = -d; k <= d; k += 2) {
62
+ let x;
63
+ if (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1])) {
64
+ x = v[offset + k + 1];
65
+ }
66
+ else {
67
+ x = v[offset + k - 1] + 1;
68
+ }
69
+ let y = x - k;
70
+ while (x < n && y < m && a[x] === b[y]) {
71
+ x++;
72
+ y++;
73
+ }
74
+ v[offset + k] = x;
75
+ if (x >= n && y >= m) {
76
+ found = d;
77
+ break outer;
78
+ }
79
+ }
80
+ }
81
+ if (found === -1)
82
+ return fallbackAll(n, m);
83
+ // バックトラック (trace[d] は深さ d-1 終了時点の V)
84
+ const rev = [];
85
+ let x = n;
86
+ let y = m;
87
+ for (let d = found; d > 0; d--) {
88
+ const vp = trace[d];
89
+ const k = x - y;
90
+ const down = k === -d || (k !== d && vp[offset + k - 1] < vp[offset + k + 1]);
91
+ const prevK = down ? k + 1 : k - 1;
92
+ const prevX = vp[offset + prevK];
93
+ const prevY = prevX - prevK;
94
+ while (x > prevX && y > prevY) {
95
+ rev.push({ kind: "same", a: x - 1, b: y - 1 });
96
+ x--;
97
+ y--;
98
+ }
99
+ rev.push(down ? { kind: "ins", b: prevY } : { kind: "del", a: prevX });
100
+ x = prevX;
101
+ y = prevY;
102
+ }
103
+ while (x > 0 && y > 0) {
104
+ rev.push({ kind: "same", a: x - 1, b: y - 1 });
105
+ x--;
106
+ y--;
107
+ }
108
+ return rev.reverse();
109
+ }
110
+ /**
111
+ * change ペア行の文字単位ハイライト範囲を両側分返す (表示専用)。
112
+ * 長い行や乖離が大きい行は行全体の範囲にフォールバックする (品質のみ低下)。
113
+ */
114
+ export function intralineRanges(a, b) {
115
+ const A = Array.from(a);
116
+ const B = Array.from(b);
117
+ if (A.length > 1000 || B.length > 1000) {
118
+ return {
119
+ left: A.length > 0 ? [{ start: 0, end: A.length }] : [],
120
+ right: B.length > 0 ? [{ start: 0, end: B.length }] : [],
121
+ };
122
+ }
123
+ const push = (arr, i) => {
124
+ const last = arr[arr.length - 1];
125
+ if (last !== undefined && last.end === i)
126
+ last.end = i + 1;
127
+ else
128
+ arr.push({ start: i, end: i + 1 });
129
+ };
130
+ const left = [];
131
+ const right = [];
132
+ for (const op of diffLines(A, B, 500)) {
133
+ if (op.kind === "del")
134
+ push(left, op.a);
135
+ else if (op.kind === "ins")
136
+ push(right, op.b);
137
+ }
138
+ return { left, right };
139
+ }
140
+ /** ops を side-by-side 表示用の行に変換する (連続する del/ins は左右にペアで並べる) */
141
+ export function toSideBySideRows(a, b, ops) {
142
+ const rows = [];
143
+ let i = 0;
144
+ while (i < ops.length) {
145
+ const op = ops[i];
146
+ if (op.kind === "same") {
147
+ rows.push({
148
+ kind: "same",
149
+ left: { no: op.a + 1, text: a[op.a] },
150
+ right: { no: op.b + 1, text: b[op.b] },
151
+ });
152
+ i++;
153
+ continue;
154
+ }
155
+ const dels = [];
156
+ const inss = [];
157
+ while (i < ops.length && ops[i].kind !== "same") {
158
+ const o = ops[i];
159
+ if (o.kind === "del")
160
+ dels.push(o.a);
161
+ else if (o.kind === "ins")
162
+ inss.push(o.b);
163
+ i++;
164
+ }
165
+ const len = Math.max(dels.length, inss.length);
166
+ for (let r = 0; r < len; r++) {
167
+ const ai = dels[r];
168
+ const bi = inss[r];
169
+ rows.push({
170
+ kind: ai !== undefined && bi !== undefined ? "change" : ai !== undefined ? "del" : "ins",
171
+ left: ai !== undefined ? { no: ai + 1, text: a[ai] } : null,
172
+ right: bi !== undefined ? { no: bi + 1, text: b[bi] } : null,
173
+ });
174
+ }
175
+ }
176
+ return rows;
177
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ブラウザ編集の保存マージ (§4.3 + §8)。
3
+ * textarea で送信された全文を FileDocument にマージする。変更していない行は
4
+ * 元の DocLine (raw バイト・改行) をそのまま保持し、変更・追加行のみを
5
+ * 再エンコード対象 (raw: null) にする — apply の replace と同じ §8 の方式。
6
+ */
7
+ import { diffLines } from "./diff.js";
8
+ import { findUnencodable } from "./encoding.js";
9
+ /**
10
+ * textarea 送信テキストを行に分解する。フォーム送信の改行は CRLF に正規化されるため
11
+ * \r\n / \r / \n を全て区切りとして扱う。末尾の空セグメント = 末尾改行あり
12
+ * (decodeFile と同じ規約)。
13
+ */
14
+ export function parseEditedText(text) {
15
+ if (text === "")
16
+ return { lines: [], trailingNewline: false };
17
+ const segs = text.split(/\r\n|\r|\n/);
18
+ if (segs[segs.length - 1] === "")
19
+ return { lines: segs.slice(0, -1), trailingNewline: true };
20
+ return { lines: segs, trailingNewline: false };
21
+ }
22
+ /** doc を textarea 表示用の全文にする (\n 結合。parseEditedText と往復可能) */
23
+ export function renderTextareaValue(doc) {
24
+ const joined = doc.lines.map((l) => l.text).join("\n");
25
+ return doc.trailingNewline && doc.lines.length > 0 ? joined + "\n" : joined;
26
+ }
27
+ /**
28
+ * 編集結果を doc にマージする。行 diff で整列し、テキストが一致する行は元の
29
+ * DocLine をそのまま使う (raw バイト・実改行を保持)。
30
+ * 既知の限界 (許容済み): 同一テキストで raw バイトだけ異なる重複行 (Shift_JIS の
31
+ * NEC/IBM 拡張の重複マッピング等) は、他所の編集により diff の整列先が入れ替わり
32
+ * raw が交換されることがある。デコード結果は同一のため実害はない。
33
+ */
34
+ export function mergeEditedText(doc, editedText) {
35
+ const { lines: newTexts, trailingNewline } = parseEditedText(editedText);
36
+ const ops = diffLines(doc.lines.map((l) => l.text), newTexts);
37
+ const lines = [];
38
+ for (const op of ops) {
39
+ if (op.kind === "same")
40
+ lines.push(doc.lines[op.a]);
41
+ else if (op.kind === "ins")
42
+ lines.push({ text: newTexts[op.b], raw: null, eol: null });
43
+ }
44
+ return {
45
+ encoding: doc.encoding,
46
+ hasBom: doc.hasBom,
47
+ eol: doc.eol,
48
+ trailingNewline,
49
+ lines,
50
+ };
51
+ }
52
+ /**
53
+ * ブラウザ編集できない理由を返す (null = 編集可)。
54
+ * HTML パーサは textarea 内の CR/CRLF を LF に正規化し NUL を U+FFFD に置換するため、
55
+ * これらを含むファイルは「開いて保存しただけで壊れる」— 事前に編集を拒否する。
56
+ * (閲覧のみの diff 表示には影響しない)
57
+ */
58
+ export function browserEditBlockReason(doc) {
59
+ for (const line of doc.lines) {
60
+ if (line.text.includes("\r") || line.text.includes("\u0000")) {
61
+ return "CR のみの改行または NUL 文字を含むため、ブラウザでの編集はできません (HTML フォームが内容を変更してしまうため)";
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ /** マージ後の再エンコード対象行 (raw: null) から変換不能文字を集める (§8 の事前検証) */
67
+ export function findMergeUnencodable(doc) {
68
+ if (doc.encoding === "utf8")
69
+ return [];
70
+ const bad = [];
71
+ const seen = new Set();
72
+ for (const line of doc.lines) {
73
+ if (line.raw !== null)
74
+ continue;
75
+ for (const ch of findUnencodable(line.text, doc.encoding)) {
76
+ if (!seen.has(ch)) {
77
+ seen.add(ch);
78
+ bad.push(ch);
79
+ }
80
+ }
81
+ }
82
+ return bad;
83
+ }