diff --git a/browser/websocket/patch.test.ts b/browser/websocket/diffToChanges.test.ts
similarity index 98%
rename from browser/websocket/patch.test.ts
rename to browser/websocket/diffToChanges.test.ts
index 5eeeb85..c1efa18 100644
--- a/browser/websocket/patch.test.ts
+++ b/browser/websocket/diffToChanges.test.ts
@@ -1,5 +1,5 @@
///
-import { diffToChanges } from "./patch.ts";
+import { diffToChanges } from "./diffToChanges.ts";
import { assertEquals } from "../../deps/testing.ts";
Deno.test("diffToChanges()", async ({ step }) => {
diff --git a/browser/websocket/patch.ts b/browser/websocket/diffToChanges.ts
similarity index 95%
rename from browser/websocket/patch.ts
rename to browser/websocket/diffToChanges.ts
index 37d63e7..84f7f38 100644
--- a/browser/websocket/patch.ts
+++ b/browser/websocket/diffToChanges.ts
@@ -11,7 +11,7 @@ type Options = {
userId: string;
};
export function* diffToChanges(
- left: Omit[],
+ left: Pick[],
right: string[],
{ userId }: Options,
): Generator {
diff --git a/browser/websocket/makeChanges.ts b/browser/websocket/makeChanges.ts
new file mode 100644
index 0000000..46392b4
--- /dev/null
+++ b/browser/websocket/makeChanges.ts
@@ -0,0 +1,136 @@
+import { diffToChanges } from "./diffToChanges.ts";
+import type { Line } from "../../deps/scrapbox.ts";
+import {
+ Block,
+ convertToBlock,
+ Node,
+ packRows,
+ parseToRows,
+} from "../../deps/scrapbox.ts";
+import type { Change } from "../../deps/socket.ts";
+import { toTitleLc } from "../../title.ts";
+
+export interface HeadData {
+ commitId: string;
+ persistent: boolean;
+ image: string | null;
+ linksLc: string[];
+ lines: Line[];
+}
+export interface Init {
+ userId: string;
+ head: HeadData;
+}
+export function makeChanges(
+ left: Pick[],
+ right: string[],
+ { userId, head }: Init,
+) {
+ // 本文の差分
+ const changes: Change[] = [...diffToChanges(left, right, { userId })];
+
+ // titleの差分を入れる
+ // 空ページの場合もタイトル変更commitを入れる
+ if (left[0].text !== right[0] || !head.persistent) {
+ changes.push({ title: right[0] });
+ }
+
+ // descriptionsの差分を入れる
+ const leftDescriptions = left.slice(1, 6).map((line) => line.text);
+ const rightDescriptions = right.slice(1, 6);
+ if (leftDescriptions.join("") !== rightDescriptions.join("")) {
+ changes.push({ descriptions: rightDescriptions });
+ }
+
+ // リンクと画像の差分を入れる
+ const [linksLc, image] = findLinksAndImage(right.join("\n"));
+ if (
+ head.linksLc.length !== linksLc.length ||
+ !head.linksLc.every((link) => linksLc.includes(link))
+ ) {
+ changes.push({ links: linksLc });
+ }
+ if (head.image !== image) {
+ changes.push({ image });
+ }
+
+ return changes;
+}
+
+/** テキストに含まれる全てのリンクと最初の画像を探す */
+function findLinksAndImage(text: string): [string[], string | null] {
+ const rows = parseToRows(text);
+ const blocks = packRows(rows, { hasTitle: true }).flatMap((pack) => {
+ switch (pack.type) {
+ case "codeBlock":
+ case "title":
+ return [];
+ case "line":
+ case "table":
+ return [convertToBlock(pack)];
+ }
+ });
+
+ const linksLc = [] as string[];
+ let image: string | null = null;
+
+ const lookup = (node: Node) => {
+ switch (node.type) {
+ case "hashTag":
+ linksLc.push(toTitleLc(node.href));
+ return;
+ case "link": {
+ if (node.pathType !== "relative") return;
+ linksLc.push(toTitleLc(node.href));
+ return;
+ }
+ case "image":
+ case "strongImage": {
+ image ??= node.src.endsWith("/thumb/1000")
+ ? node.src.replace(/\/thumb\/1000$/, "/raw")
+ : node.src;
+ return;
+ }
+ case "strong":
+ case "quote":
+ case "decoration": {
+ for (const n of node.nodes) {
+ lookup(n);
+ }
+ return;
+ }
+ default:
+ return;
+ }
+ };
+ for (const node of blocksToNodes(blocks)) {
+ lookup(node);
+ }
+
+ return [linksLc, image];
+}
+
+function* blocksToNodes(blocks: Iterable) {
+ for (const block of blocks) {
+ switch (block.type) {
+ case "codeBlock":
+ case "title":
+ continue;
+ case "line":
+ for (const node of block.nodes) {
+ yield node;
+ }
+ continue;
+ case "table": {
+ for (const row of block.cells) {
+ for (const nodes of row) {
+ for (const node of nodes) {
+ yield node;
+ }
+ }
+ }
+ continue;
+ }
+ }
+ }
+}
diff --git a/browser/websocket/room.ts b/browser/websocket/room.ts
index 04be068..7d46458 100644
--- a/browser/websocket/room.ts
+++ b/browser/websocket/room.ts
@@ -1,12 +1,8 @@
-import {
- Change,
- CommitNotification,
- socketIO,
- wrap,
-} from "../../deps/socket.ts";
+import { CommitNotification, socketIO, wrap } from "../../deps/socket.ts";
import { getProjectId, getUserId } from "./id.ts";
-import { diffToChanges } from "./patch.ts";
import { applyCommit } from "./applyCommit.ts";
+import { toTitleLc } from "../../title.ts";
+import { makeChanges } from "./makeChanges.ts";
import type { Line } from "../../deps/scrapbox.ts";
import { ensureEditablePage, pushCommit } from "./_fetch.ts";
export type { CommitNotification };
@@ -48,9 +44,13 @@ export async function joinPageRoom(
]);
// 接続したページの情報
- let parentId = page.commitId;
- let created = page.persistent;
- let lines = page.lines;
+ let head = {
+ persistent: page.persistent,
+ lines: page.lines,
+ image: page.image,
+ commitId: page.commitId,
+ linksLc: page.links.map((link) => toTitleLc(link)),
+ };
const pageId = page.id;
const io = await socketIO();
@@ -63,55 +63,36 @@ export async function joinPageRoom(
// subscribe the latest commit
(async () => {
for await (const { id, changes } of response("commit")) {
- parentId = id;
- lines = applyCommit(lines, changes, { updated: id, userId });
+ head.commitId = id;
+ head.lines = applyCommit(head.lines, changes, { updated: id, userId });
}
})();
return {
patch: async (update: (before: Line[]) => string[] | Promise) => {
- const tryPush = async () => {
- const pending = update(lines);
- const newLines = pending instanceof Promise ? await pending : pending;
- const changes: Change[] = [
- ...diffToChanges(lines, newLines, { userId }),
- ];
-
- // 変更後のlinesを計算する
- const changedLines = applyCommit(lines, changes, {
- userId,
- });
- // タイトルの変更チェック
- // 空ページの場合もタイトル変更commitを入れる
- if (lines[0].text !== changedLines[0].text || !created) {
- changes.push({ title: changedLines[0].text });
- }
- // サムネイルの変更チェック
- const oldDescriptions = lines.slice(1, 6).map((line) => line.text);
- const newDescriptions = changedLines.slice(1, 6).map((lines) =>
- lines.text
- );
- if (oldDescriptions.join("\n") !== newDescriptions.join("\n")) {
- changes.push({ descriptions: newDescriptions });
- }
-
- // pushする
- const { commitId } = await pushCommit(request, changes, {
- parentId,
- projectId,
- pageId,
- userId,
- });
-
- // pushに成功したら、localにも変更を反映する
- parentId = commitId;
- created = true;
- lines = changedLines;
- };
-
for (let i = 0; i < 3; i++) {
try {
- await tryPush();
+ const pending = update(head.lines);
+ const newLines = pending instanceof Promise ? await pending : pending;
+ const changes = makeChanges(head.lines, newLines, {
+ userId,
+ head,
+ });
+
+ const { commitId } = await pushCommit(request, changes, {
+ parentId: head.commitId,
+ projectId,
+ pageId,
+ userId,
+ });
+
+ // pushに成功したら、localにも変更を反映する
+ head.commitId = commitId;
+ head.persistent = true;
+ head.lines = applyCommit(head.lines, changes, {
+ updated: commitId,
+ userId,
+ });
break;
} catch (_e: unknown) {
if (i === 2) {
@@ -122,9 +103,13 @@ export async function joinPageRoom(
);
try {
const page = await ensureEditablePage(project, title);
- parentId = page.commitId;
- created = page.persistent;
- lines = page.lines;
+ head = {
+ persistent: page.persistent,
+ lines: page.lines,
+ image: page.image,
+ commitId: page.commitId,
+ linksLc: page.links.map((link) => toTitleLc(link)),
+ };
} catch (e: unknown) {
throw e;
}
diff --git a/browser/websocket/shortcuts.ts b/browser/websocket/shortcuts.ts
index bdc5a09..e475512 100644
--- a/browser/websocket/shortcuts.ts
+++ b/browser/websocket/shortcuts.ts
@@ -1,9 +1,9 @@
-import { Change, socketIO, wrap } from "../../deps/socket.ts";
+import { socketIO, wrap } from "../../deps/socket.ts";
import { getProjectId, getUserId } from "./id.ts";
-import { diffToChanges } from "./patch.ts";
-import { applyCommit } from "./applyCommit.ts";
+import { makeChanges } from "./makeChanges.ts";
import { pinNumber } from "./pin.ts";
import type { Line } from "../../deps/scrapbox.ts";
+import { toTitleLc } from "../../title.ts";
import { ensureEditablePage, pushCommit, pushWithRetry } from "./_fetch.ts";
/** 指定したページを削除する
@@ -68,53 +68,32 @@ export async function patch(
getUserId(),
]);
- let persistent = page.persistent;
- let lines = page.lines;
- let parentId = page.commitId;
+ let head = {
+ persistent: page.persistent,
+ lines: page.lines,
+ image: page.image,
+ commitId: page.commitId,
+ linksLc: page.links.map((link) => toTitleLc(link)),
+ };
const pageId = page.id;
const io = await socketIO();
try {
const { request } = wrap(io);
- const tryPush = async () => {
- const pending = update(lines);
- const newLines = pending instanceof Promise ? await pending : pending;
- const changes: Change[] = [
- ...diffToChanges(lines, newLines, { userId }),
- ];
-
- // 変更後のlinesを計算する
- const changedLines = applyCommit(lines, changes, {
- userId,
- });
- // タイトルの変更チェック
- // 空ページの場合もタイトル変更commitを入れる
- if (lines[0].text !== changedLines[0].text || !persistent) {
- changes.push({ title: changedLines[0].text });
- }
- // サムネイルの変更チェック
- const oldDescriptions = lines.slice(1, 6).map((line) => line.text);
- const newDescriptions = changedLines.slice(1, 6).map((lines) =>
- lines.text
- );
- if (oldDescriptions.join("\n") !== newDescriptions.join("\n")) {
- changes.push({ descriptions: newDescriptions });
- }
-
- // pushする
- await pushCommit(request, changes, {
- parentId,
- projectId,
- pageId,
- userId,
- });
- };
-
// 3回retryする
for (let i = 0; i < 3; i++) {
try {
- await tryPush();
+ const pending = update(head.lines);
+ const newLines = pending instanceof Promise ? await pending : pending;
+ const changes = makeChanges(head.lines, newLines, { userId, head });
+
+ await pushCommit(request, changes, {
+ parentId: head.commitId,
+ projectId,
+ pageId,
+ userId,
+ });
break;
} catch (_e: unknown) {
if (i === 2) {
@@ -125,9 +104,13 @@ export async function patch(
);
try {
const page = await ensureEditablePage(project, title);
- parentId = page.commitId;
- persistent = page.persistent;
- lines = page.lines;
+ head = {
+ persistent: page.persistent,
+ lines: page.lines,
+ image: page.image,
+ commitId: page.commitId,
+ linksLc: page.links.map((link) => toTitleLc(link)),
+ };
} catch (e: unknown) {
throw e;
}
diff --git a/deps/scrapbox.ts b/deps/scrapbox.ts
index 34f115b..a522627 100644
--- a/deps/scrapbox.ts
+++ b/deps/scrapbox.ts
@@ -14,4 +14,5 @@ export type {
Scrapbox,
} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts";
import type { Page } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts";
+export * from "https://esm.sh/@progfay/scrapbox-parser@7.2.0";
export type Line = Page["lines"][0];
diff --git a/deps/socket.ts b/deps/socket.ts
index 4ff127a..e9e8cf3 100644
--- a/deps/socket.ts
+++ b/deps/socket.ts
@@ -9,8 +9,8 @@ export type {
ProjectUpdatesStreamCommit,
ProjectUpdatesStreamEvent,
UpdateCommit,
-} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-websocket/0.1.3/mod.ts";
+} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-websocket/0.1.4/mod.ts";
export {
socketIO,
wrap,
-} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-websocket/0.1.3/mod.ts";
+} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-websocket/0.1.4/mod.ts";