marksites 0.2.5 → 0.2.7

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.
package/README.md CHANGED
@@ -29,7 +29,7 @@ const html = markdownToHtml(markdown, {
29
29
 
30
30
  左ファイルサイドバーは幅280pxとし、開閉アイコンは枠線なしで表示します。更新日時はヘッダーのファイルパス右隣へ細い縦線で区切って表示し、狭い画面では省略します。
31
31
 
32
- 画面上端には固定ヘッダーを表示します。濃色背景と白文字の`marksites`ロゴ、その右に現在のファイルパスを表示します。ファイルパスから開くポップオーバーは左ファイルサイドバーを閉じている場合だけ利用できます。ブラウザのタブ名は`marksites | <ファイル名>`形式です。右上の月・太陽アイコンでライト/ダークモードを切り替え、地球アイコンで生成UIの日本語/英語を切り替えます。Markdown本文、見出し、ファイル名、コメント本文は翻訳対象にしません。選択状態は`theme`と`lang` GETパラメータでページ間に引き継がれます。
32
+ 画面上端には固定ヘッダーを表示します。濃色背景と白文字の`marksites`ロゴ、その右に現在のファイルパスを表示します。ファイルパスから開くポップオーバーは左ファイルサイドバーを閉じている場合だけ利用できます。ブラウザのタブ名は`marksites | <ファイル名>`形式です。右上では差分アイコンで前回正常ビルドからの変更と最新版を切り替え、その右の月・太陽アイコンでライト/ダークモード、地球アイコンで生成UIの日本語/英語を切り替えます。差分がなければ差分アイコンは非活性です。差分表示の希望状態は`document-view=diff`として保持し、差分のないページを経由してもHTMLページ間で引き継ぎます。Markdown本文、見出し、ファイル名、コメント本文は翻訳対象にしません。テーマと言語の選択状態も`theme`と`lang` GETパラメータでページ間に引き継がれます。
33
33
 
34
34
  ```ts
35
35
  const html = markdownToHtml(markdown, {
@@ -46,6 +46,8 @@ const html = markdownToHtml(markdown, {
46
46
  言語名を指定したコードブロックは、`highlight.js` によって自動的にハイライトされます。
47
47
  コードブロックには、コードをクリップボードへコピーするボタンと、長い行の折り返しを切り替えるボタンも表示されます。
48
48
 
49
+ Markdownの表では、列境界をドラッグして列幅を変更できます。見出しをクリックすると、その列を昇順、降順、元の順序へ切り替えられます。列幅変更とソートはマウス、タッチ、キーボードで操作でき、すべて生成HTML内のJavaScriptだけで動作します。
50
+
49
51
  ````markdown
50
52
  ```typescript
51
53
  const greeting: string = "Hello";
@@ -93,7 +95,7 @@ npx marksites docs
93
95
 
94
96
  入力フォルダ配下の`.gitignore`を読み込み、Gitと同じパターン規則で除外されたファイルとフォルダを変換対象から外します。ネストした`.gitignore`と`!`による再包含にも対応します。また、`.marksites-build.json`を持つ既存の生成フォルダと、今回指定された出力フォルダは常に探索対象外です。
95
97
 
96
- 2回目以降はMarkdown本文、コメント、ファイル構成、レンダー設定のSHA-256を比較し、影響したHTMLだけを再生成します。管理情報は出力先直下の `.marksites-build.json`、文書ごとのメタデータは各HTMLに隣接する `.<名前>.json` に保存されます。このJSONは再変換時にも上書きされません。現在はコメント情報を格納し、将来は文書固有のほかの情報も管理できるファイルとして扱います。
98
+ 2回目以降はMarkdown本文、コメント、ファイル構成、レンダー設定のSHA-256を比較し、影響したHTMLだけを再生成します。前回正常ビルド時のMarkdownは出力先の`.marksites-history/`へ文書ごとに1世代だけ保持し、現在版とのブロック差分を生成HTMLへ埋め込みます。管理情報は出力先直下の `.marksites-build.json`、文書ごとのメタデータは各HTMLに隣接する `.<名前>.json` に保存されます。このJSONは再変換時にも上書きされません。現在はコメント情報を格納し、将来は文書固有のほかの情報も管理できるファイルとして扱います。
97
99
 
98
100
  ### コメントを編集する
99
101
 
@@ -101,6 +103,8 @@ npx marksites docs
101
103
 
102
104
  本文で文字列を選択すると、コピー操作の隣に`文字列置換`が表示されます。選択文字列を変換元として画面中央の管理ポップアップへ置換後の文字列を入力すると、表示中の本文にある同じ文字列をすべてブラウザ上で一時的に置換でき、アクセストークンなどを当てはめてからコピーできます。本文カード右上の`置換`ボタンから選択なしで新しいルールを追加することもでき、複数ルールを一覧で管理して個別にクリアできます。ポップアップは閉じるアイコンまたは範囲外のクリックで閉じます。置換箇所は枠付きでハイライトされます。置換値自体は管理ボタンへ表示しません。元のMarkdownと生成HTMLは変更されず、ページを再読み込みしても元に戻ります。
103
105
 
106
+ ポップアップ表示中は背後が薄く暗くなります。一覧のルールを個別にクリアしてもポップアップは閉じないため、そのまま続けて管理できます。
107
+
104
108
  右サイドバーでは目次と現在ページのコメント一覧をタブで切り替えられます。タブにはコメント件数が表示され、コメントを選ぶと本文の対象箇所へ移動します。一覧先頭の追加ボタンから、範囲を選択しない文書全体へのコメントも作成できます。ヘッダーは固定され、各パネルは独立してスクロールします。
105
109
 
106
110
  ```sh
@@ -9,6 +9,7 @@ import { buildBreadcrumbs, buildFileTree } from "./navigation.js";
9
9
  import { DEFAULT_OUTPUT_DIRECTORY, findMarkdownFiles, firstExistingPath, pathExists, toLegacyMetadataPaths, } from "./paths.js";
10
10
  import { GENERATOR_VERSION, OUTPUT_COMPATIBILITY_VERSION, contentHash, renderFingerprint, rewriteMarkdownLinks, } from "./rendering.js";
11
11
  import { prepareImageAssets } from "./assets.js";
12
+ import { readHistory, removeHistory, toHistoryPath, writeHistory, } from "./history.js";
12
13
  async function migrateLegacyMetadata(files, output) {
13
14
  let moved = 0;
14
15
  for (const file of files) {
@@ -35,6 +36,12 @@ async function loadSources(files) {
35
36
  file.modifiedAt = sourceStat.mtime.toISOString();
36
37
  }
37
38
  }
39
+ async function loadHistories(files, previous, output) {
40
+ for (const file of files) {
41
+ file.historyPath = toHistoryPath(file.relativePath);
42
+ file.previousSource = await readHistory(output, previous?.files[file.relativePath]?.history);
43
+ }
44
+ }
38
45
  async function prepareAssets(files, output) {
39
46
  for (const file of files) {
40
47
  const assets = await prepareImageAssets(file.source, file.sourcePath, file.outputPath, output);
@@ -78,6 +85,7 @@ async function moveRenamedDocuments(files, previous, output, removed) {
78
85
  await rm(oldHtml);
79
86
  deleted++;
80
87
  }
88
+ await removeHistory(output, oldData.history);
81
89
  warnAboutStaleLinks(files, old);
82
90
  removed.splice(removed.indexOf(old), 1);
83
91
  }
@@ -118,6 +126,7 @@ async function removeDeletedHtml(removed, previous, output) {
118
126
  if (await pathExists(join(output, ...info.annotations.split("/")))) {
119
127
  orphaned.push(info.annotations);
120
128
  }
129
+ await removeHistory(output, info.history);
121
130
  }
122
131
  return { deleted, orphaned };
123
132
  }
@@ -150,7 +159,7 @@ async function renderChangedFiles(files, input, output, previous, full, options)
150
159
  file.rewriteImages?.(token);
151
160
  },
152
161
  },
153
- }, file.annotations));
162
+ }, file.annotations, file.previousSource));
154
163
  converted++;
155
164
  options.onLog?.(`Converted ${file.relativePath} -> ${file.outputPath}`);
156
165
  }
@@ -166,6 +175,7 @@ async function renderChangedFiles(files, input, output, previous, full, options)
166
175
  annotations: file.metadataPath,
167
176
  assetHash: file.assetHash,
168
177
  assets: file.assetOutputs,
178
+ history: file.historyPath,
169
179
  };
170
180
  }
171
181
  return { converted, skipped, files: manifestFiles };
@@ -190,6 +200,7 @@ export async function convertDirectoryDetailed(input, outputArgument, options =
190
200
  console.warn(loaded.warning);
191
201
  const previous = loaded.manifest;
192
202
  await loadSources(files);
203
+ await loadHistories(files, previous, output);
193
204
  await prepareAssets(files, output);
194
205
  let annotationsMoved = await migrateLegacyMetadata(files, output);
195
206
  const currentPaths = new Set(files.map((file) => file.relativePath));
@@ -211,6 +222,9 @@ export async function convertDirectoryDetailed(input, outputArgument, options =
211
222
  previous.treeHash !== treeHash;
212
223
  const removedResult = await removeDeletedHtml(removed, previous, output);
213
224
  const rendered = await renderChangedFiles(files, input, output, previous, full, options);
225
+ for (const file of files) {
226
+ await writeHistory(output, file.historyPath, file.source);
227
+ }
214
228
  await removeUnusedAssets(previous, files, output);
215
229
  await writeManifest(manifestPath, {
216
230
  schemaVersion: 1,
@@ -0,0 +1,4 @@
1
+ export declare function toHistoryPath(documentPath: string): string;
2
+ export declare function readHistory(outputRoot: string, historyPath: string | undefined): Promise<string | undefined>;
3
+ export declare function writeHistory(outputRoot: string, historyPath: string, markdown: string): Promise<void>;
4
+ export declare function removeHistory(outputRoot: string, historyPath: string | undefined): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { atomicWriteFile } from "../utils/files.js";
5
+ const HISTORY_DIRECTORY = ".marksites-history";
6
+ const HISTORY_PATH = /^\.marksites-history\/[a-f0-9]{64}\.md$/;
7
+ function isHistoryPath(path) {
8
+ return path !== undefined && HISTORY_PATH.test(path);
9
+ }
10
+ export function toHistoryPath(documentPath) {
11
+ const id = createHash("sha256").update(documentPath).digest("hex");
12
+ return `${HISTORY_DIRECTORY}/${id}.md`;
13
+ }
14
+ export async function readHistory(outputRoot, historyPath) {
15
+ if (!isHistoryPath(historyPath))
16
+ return undefined;
17
+ try {
18
+ return await readFile(join(outputRoot, ...historyPath.split("/")), "utf8");
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return undefined;
23
+ throw error;
24
+ }
25
+ }
26
+ export async function writeHistory(outputRoot, historyPath, markdown) {
27
+ await atomicWriteFile(join(outputRoot, ...historyPath.split("/")), markdown);
28
+ }
29
+ export async function removeHistory(outputRoot, historyPath) {
30
+ if (!isHistoryPath(historyPath))
31
+ return;
32
+ await rm(join(outputRoot, ...historyPath.split("/")), { force: true });
33
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Token } from "marked";
2
2
  export declare const GENERATOR_VERSION: string;
3
- export declare const OUTPUT_COMPATIBILITY_VERSION = 7;
3
+ export declare const OUTPUT_COMPATIBILITY_VERSION = 9;
4
4
  export declare function contentHash(value: string | Buffer): string;
5
5
  export declare function rewriteMarkdownLinks(token: Token): void;
6
6
  export declare function renderFingerprint(): string;
@@ -4,7 +4,7 @@ import { emptyAnnotationDocument } from "../annotations/model.js";
4
4
  import { renderMarkdown } from "../markdown-to-html.js";
5
5
  const packageMetadata = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
6
6
  export const GENERATOR_VERSION = packageMetadata.version;
7
- export const OUTPUT_COMPATIBILITY_VERSION = 7;
7
+ export const OUTPUT_COMPATIBILITY_VERSION = 9;
8
8
  export function contentHash(value) {
9
9
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
10
10
  }
@@ -21,7 +21,7 @@ export function rewriteMarkdownLinks(token) {
21
21
  token.href = rewriteMarkdownHref(token.href);
22
22
  }
23
23
  export function renderFingerprint() {
24
- const representativeHtml = renderMarkdown("## Heading\n\n[Document](guide.md)\n\n![Image](image.png)\n\n```js\nconst value = 1;\n```\n", {
24
+ const representativeHtml = renderMarkdown("## Heading\n\n[Document](guide.md)\n\n![Image](image.png)\n\n| Name | Value |\n| --- | --- |\n| one | 1 |\n\n```js\nconst value = 1;\n```\n", {
25
25
  title: "marksites-render-fingerprint",
26
26
  modifiedAt: "2026-01-01T00:00:00.000Z",
27
27
  fileTree: {
@@ -5,6 +5,7 @@ import { renderMarkdown } from "../markdown-to-html.js";
5
5
  import { atomicWriteFile } from "../utils/files.js";
6
6
  import { DEFAULT_OUTPUT_DIRECTORY, firstExistingPath, isMarkdown, pathExists, } from "./paths.js";
7
7
  import { prepareImageAssets } from "./assets.js";
8
+ import { readHistory, toHistoryPath, writeHistory } from "./history.js";
8
9
  export async function convertFile(input, outputArgument) {
9
10
  if (!isMarkdown(input))
10
11
  throw new Error(`Input file must use .md or .markdown: ${input}`);
@@ -29,12 +30,16 @@ export async function convertFile(input, outputArgument) {
29
30
  ]);
30
31
  await mkdir(dirname(output), { recursive: true });
31
32
  const assets = await prepareImageAssets(markdown, input, basename(output), dirname(output));
33
+ const outputRoot = dirname(output);
34
+ const historyPath = toHistoryPath(basename(output));
35
+ const previousSource = await readHistory(outputRoot, historyPath);
32
36
  await atomicWriteFile(output, renderMarkdown(markdown, {
33
37
  title: basename(input, extname(input)),
34
38
  modifiedAt: sourceStat.mtime.toISOString(),
35
39
  markedOptions: {
36
40
  walkTokens: assets.rewrite,
37
41
  },
38
- }, annotations));
42
+ }, annotations, previousSource));
43
+ await writeHistory(outputRoot, historyPath, markdown);
39
44
  return output;
40
45
  }
@@ -12,6 +12,8 @@ export interface MarkdownFile {
12
12
  assetHash?: string;
13
13
  assetOutputs?: string[];
14
14
  rewriteImages?: (token: import("marked").Token) => void;
15
+ previousSource?: string;
16
+ historyPath?: string;
15
17
  }
16
18
  export interface ManifestFile {
17
19
  sourceHash: string;
@@ -21,6 +23,7 @@ export interface ManifestFile {
21
23
  annotations: string;
22
24
  assetHash?: string;
23
25
  assets?: string[];
26
+ history?: string;
24
27
  }
25
28
  export interface BuildManifest {
26
29
  schemaVersion: 1;
@@ -1,6 +1,7 @@
1
1
  import { type AnnotationDocument } from "../../annotations/model.js";
2
2
  import type { DocumentFeature } from "../types.js";
3
3
  export interface AnnotationsFeature extends DocumentFeature {
4
+ documentControl: string;
4
5
  panel: string;
5
6
  count: number;
6
7
  }
@@ -8,7 +8,14 @@ function safeJson(value) {
8
8
  }
9
9
  export function createAnnotationsFeature(data) {
10
10
  if (!data)
11
- return { markup: "", panel: "", styles: "", script: "", count: 0 };
11
+ return {
12
+ markup: "",
13
+ documentControl: "",
14
+ panel: "",
15
+ styles: "",
16
+ script: "",
17
+ count: 0,
18
+ };
12
19
  const markup = `<script id="marksites-annotations-data" type="application/json">${safeJson(data)}</script>
13
20
  <div class="selection-actions" id="selection-actions" hidden role="toolbar" aria-label="選択範囲の操作">
14
21
  <button type="button" data-selection-action="copy" data-tooltip="選択範囲をコピー">${renderCopyIcon()}<span data-copy-label>コピー</span></button>
@@ -23,8 +30,8 @@ export function createAnnotationsFeature(data) {
23
30
  <label><span>置換後の文字列</span><input type="text" name="replacement" autocomplete="off"></label>
24
31
  <p class="text-replace-error" data-replacement-error role="alert" hidden></p>
25
32
  <div class="text-replace-alert-actions"><button type="submit">置換</button><button type="button" data-cancel-replace>キャンセル</button></div>
26
- </form>
27
- <button type="button" class="document-replacement-menu" data-replacement-menu aria-expanded="false" title="この文書の文字列置換を管理" aria-label="この文書の文字列置換を管理">${renderEditIcon()}<span>置換</span><span class="text-replacement-count" data-replacement-count hidden>0</span></button>`;
33
+ </form>`;
34
+ const documentControl = `<button type="button" class="document-content-action document-replacement-menu" data-replacement-menu aria-expanded="false" title="この文書の文字列置換を管理" aria-label="この文書の文字列置換を管理">${renderEditIcon()}<span>置換</span><span class="text-replacement-count" data-replacement-count hidden>0</span></button>`;
28
35
  const panel = `<section class="annotations-panel sidebar-panel" id="sidebar-panel-comments" role="tabpanel" aria-labelledby="sidebar-tab-comments" hidden>
29
36
  <div class="annotations-actions"><button type="button" class="annotation-action-add" data-add-document-comment data-tooltip="文書全体にコメントを追加" disabled title="コメントを追加するにはmarksites serveを起動してください">${renderAddIcon()}<span>追加</span></button><button type="button" class="annotation-action-copy" data-copy-all-comments data-tooltip="有効なコメントだけをコピー" aria-label="有効なコメントをコピー">${renderCopyIcon()}<span data-copy-comments-label>コピー</span></button></div>
30
37
  <p class="annotations-status" id="annotations-status" role="status"></p>
@@ -38,7 +45,8 @@ export function createAnnotationsFeature(data) {
38
45
  .selection-actions button{display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;border-radius:4px;padding:5px 9px;color:#fff;background:transparent;font:12px/1 system-ui;cursor:pointer}
39
46
  .selection-actions button:hover:not(:disabled){background:#57606a}.selection-actions button:disabled{color:#8c959f;cursor:not-allowed}
40
47
  .selection-actions [data-tooltip],.annotations-panel [data-tooltip]{position:relative}.selection-actions [data-tooltip]::after,.annotations-panel [data-tooltip]::after{position:absolute;z-index:30;top:calc(100% + 6px);right:0;width:max-content;max-width:220px;padding:5px 7px;color:#fff;font-size:.6875rem;font-weight:400;line-height:1.3;white-space:normal;background:#24292f;border-radius:4px;box-shadow:0 3px 10px rgba(31,35,40,.2);content:attr(data-tooltip);opacity:0;pointer-events:none;transform:translateY(-2px);transition:opacity 120ms ease,transform 120ms ease}.selection-actions [data-tooltip]:hover::after,.selection-actions [data-tooltip]:focus-visible::after,.annotations-panel [data-tooltip]:hover::after,.annotations-panel [data-tooltip]:focus-visible::after{opacity:1;transform:translateY(0)}
41
- .markdown-content{position:relative}.document-replacement-menu{position:absolute;top:12px;right:12px;display:inline-flex;height:32px;align-items:center;justify-content:center;gap:6px;padding:0 9px;color:var(--fgColor-muted,#59636e);font:inherit;font-size:.75rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.document-replacement-menu:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.document-replacement-menu:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.document-replacement-menu svg{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round}.document-replacement-menu>span:not(.text-replacement-count){line-height:16px}.text-replace-alert{position:fixed;z-index:45;top:50%;left:50%;box-sizing:border-box;display:grid;width:min(440px,calc(100vw - 24px));max-height:calc(100vh - 24px);grid-template-columns:1fr 1fr;gap:10px;padding:12px;overflow-y:auto;color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;box-shadow:0 8px 24px rgba(31,35,40,.2);transform:translate(-50%,-50%)}.text-replace-alert-heading,.text-replacement-list{grid-column:1/-1}.text-replace-alert-heading{display:flex;align-items:center;justify-content:space-between}.text-replace-alert label{display:block;min-width:0}.text-replace-alert label span{display:block;margin-bottom:5px;color:var(--fgColor-muted,#59636e);font-size:.75rem;font-weight:600}.text-replace-alert input{box-sizing:border-box;width:100%;height:34px;padding:5px 8px;color:var(--fgColor-default,#1f2328);font:inherit;background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px}.text-replace-alert input:focus{border-color:var(--borderColor-accent-emphasis,#0969da);outline:2px solid var(--bgColor-accent-muted,#ddf4ff)}.text-replace-error{grid-column:1/-1;margin:0;color:var(--fgColor-danger,#d1242f);font-size:.75rem}.text-replace-error[hidden]{display:none}.text-replace-alert-actions{display:flex;grid-column:1/-1;justify-content:flex-end;gap:6px}.text-replace-alert button{height:34px;padding:0 10px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.8125rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.text-replace-alert button[type=submit]{color:#fff;background:var(--button-primary-bgColor-rest,#1f883d);border-color:transparent}.text-replace-alert .text-replace-alert-heading button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;padding:0;color:var(--fgColor-muted,#59636e);background:transparent;border:0;border-radius:4px}.text-replace-alert .text-replace-alert-heading button:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.text-replace-alert .text-replace-alert-heading button:focus-visible{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2);outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:1px}.text-replace-alert-heading button svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round}.text-replacement-list:empty{display:none}.text-replacement-rule{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4);font-size:.75rem}.text-replacement-rule code{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-replacement-rule button{height:28px;padding:0 8px}.text-replacement{padding:1px 2px;background:var(--bgColor-accent-muted,#ddf4ff);border:1px solid var(--borderColor-accent-emphasis,#0969da);border-radius:3px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.text-replacement-count{display:inline-flex;min-width:16px;height:16px;align-items:center;justify-content:center;margin-left:3px;padding:0 3px;color:#fff;font-size:.6875rem;line-height:16px;background:var(--fgColor-accent,#0969da);border-radius:999px}
48
+ .document-replacement-menu>span:not(.text-replacement-count){line-height:16px}.text-replace-alert{position:fixed;z-index:45;top:50%;left:50%;box-sizing:border-box;display:grid;width:min(440px,calc(100vw - 24px));max-height:calc(100vh - 24px);grid-template-columns:1fr 1fr;gap:10px;padding:12px;overflow-y:auto;color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;box-shadow:0 8px 24px rgba(31,35,40,.2);transform:translate(-50%,-50%)}.text-replace-alert-heading,.text-replacement-list{grid-column:1/-1}.text-replace-alert-heading{display:flex;align-items:center;justify-content:space-between}.text-replace-alert label{display:block;min-width:0}.text-replace-alert label span{display:block;margin-bottom:5px;color:var(--fgColor-muted,#59636e);font-size:.75rem;font-weight:600}.text-replace-alert input{box-sizing:border-box;width:100%;height:34px;padding:5px 8px;color:var(--fgColor-default,#1f2328);font:inherit;background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px}.text-replace-alert input:focus{border-color:var(--borderColor-accent-emphasis,#0969da);outline:2px solid var(--bgColor-accent-muted,#ddf4ff)}.text-replace-error{grid-column:1/-1;margin:0;color:var(--fgColor-danger,#d1242f);font-size:.75rem}.text-replace-error[hidden]{display:none}.text-replace-alert-actions{display:flex;grid-column:1/-1;justify-content:flex-end;gap:6px}.text-replace-alert button{height:34px;padding:0 10px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.8125rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.text-replace-alert button[type=submit]{color:#fff;background:var(--button-primary-bgColor-rest,#1f883d);border-color:transparent}.text-replace-alert .text-replace-alert-heading button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;padding:0;color:var(--fgColor-muted,#59636e);background:transparent;border:0;border-radius:4px}.text-replace-alert .text-replace-alert-heading button:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.text-replace-alert .text-replace-alert-heading button:focus-visible{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2);outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:1px}.text-replace-alert-heading button svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round}.text-replacement-list:empty{display:none}.text-replacement-rule{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4);font-size:.75rem}.text-replacement-rule code{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-replacement-rule button{height:28px;padding:0 8px}.text-replacement{padding:1px 2px;background:var(--bgColor-accent-muted,#ddf4ff);border:1px solid var(--borderColor-accent-emphasis,#0969da);border-radius:3px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.text-replacement-count{display:inline-flex;min-width:16px;height:16px;align-items:center;justify-content:center;margin-left:3px;padding:0 3px;color:#fff;font-size:.6875rem;line-height:16px;background:var(--fgColor-accent,#0969da);border-radius:999px}
49
+ .text-replace-alert:not([hidden]){box-shadow:0 0 0 100vmax rgba(31,35,40,.18),0 8px 24px rgba(31,35,40,.2)}
42
50
  .annotations-panel{color:var(--fgColor-default,#1f2328)}
43
51
  .annotations-actions{position:sticky;z-index:1;top:-12px;display:flex;gap:8px;margin:-12px -12px 0;padding:10px 12px;background:var(--bgColor-default,#fff);border-bottom:1px solid var(--borderColor-muted,#d8dee4)}.annotations-panel button{padding:6px 10px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.8125rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.annotations-panel button:hover:not(:disabled){background:var(--button-default-bgColor-hover,#eaeef2)}.annotations-panel button:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.annotations-panel button:disabled{color:var(--fgColor-muted,#818b98);cursor:not-allowed}.annotations-actions button{display:inline-flex;min-width:0;flex:1;align-items:center;justify-content:center;gap:5px}.annotations-panel .annotation-action-add:not(:disabled){color:#fff;background:var(--button-primary-bgColor-rest,#1f883d);border-color:var(--button-primary-borderColor-rest,#1f232826)}.annotations-panel .annotation-action-add:hover:not(:disabled){background:var(--button-primary-bgColor-hover,#1a7f37)}.annotations-panel .annotation-action-copy{color:var(--fgColor-muted,#59636e);background:transparent;border-color:var(--borderColor-muted,#d8dee4)}
44
52
  .annotations-empty{margin:0;padding:28px 12px 20px;color:var(--fgColor-muted,#59636e);font-size:.8125rem;line-height:1.6;text-align:center}
@@ -52,7 +60,7 @@ export function createAnnotationsFeature(data) {
52
60
  const script = `<script>(()=>{
53
61
  const dataElement=document.getElementById('marksites-annotations-data');if(!dataElement)return;
54
62
  let state=JSON.parse(dataElement.textContent||'{}'),editable=false,pendingSelection=null,editingId=null,replacementSequence=0,replacementRules=[];
55
- const content=document.querySelector('.markdown-content'),toolbar=document.getElementById('selection-actions'),replaceAlert=document.getElementById('text-replace-alert'),replacementError=document.querySelector('[data-replacement-error]'),replacementMenu=document.querySelector('[data-replacement-menu]'),replacementList=document.querySelector('[data-replacement-list]'),addDocumentComment=document.querySelector('[data-add-document-comment]'),copyAllComments=document.querySelector('[data-copy-all-comments]'),list=document.getElementById('annotations-list'),empty=document.getElementById('annotations-empty'),count=document.getElementById('annotation-count'),panelStatus=document.getElementById('annotations-status'),form=document.getElementById('annotation-form'),formHome=document.createComment('annotation-form-home');form.before(formHome);content.prepend(replacementMenu);
63
+ const content=document.querySelector('.markdown-content'),toolbar=document.getElementById('selection-actions'),replaceAlert=document.getElementById('text-replace-alert'),replacementError=document.querySelector('[data-replacement-error]'),replacementMenu=document.querySelector('[data-replacement-menu]'),replacementList=document.querySelector('[data-replacement-list]'),addDocumentComment=document.querySelector('[data-add-document-comment]'),copyAllComments=document.querySelector('[data-copy-all-comments]'),list=document.getElementById('annotations-list'),empty=document.getElementById('annotations-empty'),count=document.getElementById('annotation-count'),panelStatus=document.getElementById('annotations-status'),form=document.getElementById('annotation-form'),formHome=document.createComment('annotation-form-home');form.before(formHome);
56
64
  const excluded='button,textarea,input,.selection-actions,.annotations-panel,.file-tree,.table-of-contents,.code-block-actions';
57
65
  const groupParameter='comments',groupDefinitions=[['comments',''],['archived','アーカイブ']],validGroups=new Set(['archived']),openGroups=new Set((new URL(location.href).searchParams.get(groupParameter)||'').split(',').filter(key=>validGroups.has(key)));
58
66
  function syncGroupState(){const updateUrl=url=>{url.searchParams.delete(groupParameter);if(openGroups.size)url.searchParams.set(groupParameter,groupDefinitions.map(([key])=>key).filter(key=>openGroups.has(key)).join(','));return url};history.replaceState(null,'',updateUrl(new URL(location.href)));for(const link of document.querySelectorAll('a[href]')){const raw=link.getAttribute('href');if(!raw||raw.startsWith('#'))continue;const url=new URL(raw,location.href);if(url.protocol===location.protocol&&url.host===location.host&&url.pathname.endsWith('.html'))link.href=updateUrl(url).href}}
@@ -72,6 +80,7 @@ function closeReplacementMenu(){replaceAlert.hidden=true;replacementMenu.setAttr
72
80
  function replaceText(search,replacement){const id=++replacementSequence,walker=document.createTreeWalker(content,NodeFilter.SHOW_TEXT,{acceptNode:node=>node.parentElement?.closest(excluded+',.text-replacement')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}),nodes=[];let node,total=0;while(node=walker.nextNode())nodes.push(node);for(const text of nodes){const parts=text.data.split(search);if(parts.length===1)continue;const fragment=document.createDocumentFragment();parts.forEach((part,index)=>{fragment.append(document.createTextNode(part));if(index<parts.length-1){const mark=document.createElement('span');mark.className='text-replacement';mark.dataset.replacementId=String(id);mark.textContent=replacement;fragment.append(mark);total++}});text.replaceWith(fragment)}if(total)replacementRules.push({id,search,replacement,total});renderReplacementRules()}
73
81
  document.addEventListener('selectionchange',()=>{clearTimeout(window.__marksitesSelectionTimer);window.__marksitesSelectionTimer=setTimeout(()=>{const next=selectionData();if(!next){toolbar.hidden=true;return}pendingSelection=next;const r=getSelection().getRangeAt(0).getBoundingClientRect();toolbar.style.left=Math.max(8,Math.min(innerWidth-toolbar.offsetWidth-8,r.left))+'px';toolbar.style.top=Math.max(8,r.top-44)+'px';toolbar.hidden=false},80)});
74
82
  toolbar.addEventListener('mousedown',e=>e.preventDefault());toolbar.addEventListener('click',async e=>{const button=e.target.closest('button');if(!button||!pendingSelection)return;const label=button.querySelector('[data-copy-label]');try{if(button.dataset.selectionAction==='copy')await copy(pendingSelection.exact);if(button.dataset.selectionAction==='replace'){openReplacementMenu(pendingSelection.exact);toolbar.hidden=true}if(button.dataset.selectionAction==='ai')await copy('文書: '+state.document+'\\n見出し: '+(pendingSelection.headingId||'なし')+'\\n\\n> '+pendingSelection.exact.replace(/\\n/g,'\\n> '));if(button.dataset.selectionAction==='comment'&&editable){editingId=null;dispatchEvent(new Event('marksites:show-comments'));openForm()}if(label){label.textContent='コピーしました';setTimeout(()=>label.textContent=button.dataset.selectionAction==='ai'?'AI向けコピー':'コピー',1000)}}catch{if(label)label.textContent='コピー失敗'}});
83
+ replacementList.addEventListener('click',e=>{if(e.target.closest('[data-clear-replacement]'))e.stopPropagation()});
75
84
  replaceAlert.addEventListener('submit',e=>{e.preventDefault();const search=replaceAlert.search.value,replacement=replaceAlert.replacement.value;if(!search){setReplacementError('置換する文字列を入力してください。');replaceAlert.search.focus();return}if(!replacement){setReplacementError('置換後の文字列を入力してください。');replaceAlert.replacement.focus();return}const before=replacementRules.length;replaceText(search,replacement);if(replacementRules.length===before){setReplacementError('置換する文字列が本文内に見つかりません。');replaceAlert.search.focus();return}setReplacementError();replaceAlert.search.value='';replaceAlert.replacement.value='';replaceAlert.search.focus();getSelection()?.removeAllRanges()});replaceAlert.addEventListener('input',()=>setReplacementError());replaceAlert.querySelectorAll('[data-cancel-replace]').forEach(button=>button.addEventListener('click',closeReplacementMenu));replacementMenu.addEventListener('click',()=>{if(replaceAlert.hidden)openReplacementMenu();else closeReplacementMenu()});replacementList.addEventListener('click',e=>{const button=e.target.closest('[data-clear-replacement]');if(!button)return;const id=Number(button.dataset.clearReplacement),rule=replacementRules.find(item=>item.id===id);if(!rule)return;for(const mark of content.querySelectorAll('.text-replacement[data-replacement-id="'+id+'"]'))mark.replaceWith(document.createTextNode(rule.search));replacementRules=replacementRules.filter(item=>item.id!==id);renderReplacementRules()});
76
85
  document.addEventListener('click',e=>{if(replaceAlert.hidden||replaceAlert.contains(e.target)||replacementMenu.contains(e.target)||e.target.closest('[data-selection-action=replace]'))return;closeReplacementMenu()});
77
86
  addDocumentComment.addEventListener('click',()=>{if(!editable)return;editingId=null;pendingSelection={exact:'',prefix:'',suffix:'',headingId:null,startOffset:0,endOffset:0};openForm()});
@@ -93,5 +102,12 @@ async function mutate(method,path,body){body.document=state.document;const formS
93
102
  async function connect(){if(location.protocol==='file:')return;try{const controller=new AbortController();setTimeout(()=>controller.abort(),1500);const response=await fetch('/_marksites/api/v1/runtime',{cache:'no-store',signal:controller.signal});const runtime=await response.json();if(runtime.data&&runtime.data.service==='marksites'&&runtime.data.apiVersion===1&&runtime.data.projectId&&runtime.data.capabilities.includes('annotations:write')){editable=true;const selectionComment=toolbar.querySelector('[data-selection-action=comment]');selectionComment.disabled=false;selectionComment.title='選択範囲にコメントを追加';addDocumentComment.disabled=false;addDocumentComment.title='文書全体にコメントを追加';const latest=await fetch('/_marksites/api/v1/annotations?document='+encodeURIComponent(state.document),{cache:'no-store'}).then(r=>r.json());if(latest.data)state=latest.data;render()}}catch{}}
94
103
  render();syncGroupState();connect();
95
104
  })()</script>`;
96
- return { markup, panel, styles, script, count: countActiveAnnotations(data) };
105
+ return {
106
+ markup,
107
+ documentControl,
108
+ panel,
109
+ styles,
110
+ script,
111
+ count: countActiveAnnotations(data),
112
+ };
97
113
  }
@@ -0,0 +1,8 @@
1
+ import { type MarkedOptions } from "marked";
2
+ export interface DocumentDiffFeature {
3
+ content: string;
4
+ control: string;
5
+ styles: string;
6
+ hasChanges: boolean;
7
+ }
8
+ export declare function createDocumentDiffFeature(current: string, previous: string | undefined, markedOptions?: Omit<MarkedOptions, "async" | "renderer">): DocumentDiffFeature;