= T | ErrorResponse;
15 |
16 | /**
17 | * リストページハンドラーのパラメータ型定義
18 | */
19 | export interface ListPagesHandlerParams extends PaginationParams {
20 | cosenseSid: string;
21 | projectName: string;
22 | }
23 |
24 | /**
25 | * 検索ページハンドラーのパラメータ型定義
26 | */
27 | export interface SearchPagesHandlerParams extends PaginationParams {
28 | cosenseSid: string;
29 | projectName: string;
30 | query: string;
31 | }
32 |
33 | /**
34 | * ページ取得ハンドラーの入力パラメータ
35 | */
36 | export interface GetPageHandlerParams extends AuthParams {
37 | pageId: string;
38 | }
39 |
40 | /**
41 | * ページ作成ハンドラーの入力パラメータ
42 | */
43 | export interface CreatePageHandlerParams extends AuthParams {
44 | title: string;
45 | content: string;
46 | }
47 |
48 | /**
49 | * 各ハンドラーのレスポンス型
50 | */
51 | export type ListPagesHandlerResponse = HandlerResponse<{
52 | success: true;
53 | data: ListPagesResponse;
54 | }>;
55 |
56 | export type SearchPagesHandlerResponse = HandlerResponse<{
57 | success: true;
58 | data: SearchPagesResponse;
59 | }>;
60 |
61 | export type GetPageHandlerResponse = HandlerResponse<{
62 | success: true;
63 | data: ScrapboxPage;
64 | }>;
65 |
66 | export type CreatePageHandlerResponse = HandlerResponse<{
67 | success: true;
68 | data: CreatePageResponse;
69 | }>;
70 |
71 | /**
72 | * ハンドラー関数の型定義
73 | */
74 | export type RequestHandler = (params: P) => Promise;
75 |
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | // 基本的なページ型を定義
2 | export interface BasePage {
3 | title: string;
4 | created?: number;
5 | updated?: number;
6 | pin?: number;
7 | user?: {
8 | id: string;
9 | name: string;
10 | displayName: string;
11 | photo: string;
12 | };
13 | lastUpdateUser?: {
14 | id: string;
15 | name: string;
16 | displayName: string;
17 | photo: string;
18 | };
19 | lastAccessed?: number;
20 | accessed?: number;
21 | views?: number;
22 | linked?: number;
23 | }
24 |
25 | // 検索結果用の拡張ページ型
26 | interface ExtendedPage extends BasePage {
27 | words?: string[];
28 | lines?: string[];
29 | collaborators?: Array<{
30 | id: string;
31 | name: string;
32 | displayName: string;
33 | photo: string;
34 | }>;
35 | descriptions?: string[]; // 冒頭5行を追加
36 | }
37 |
38 | export interface PageMetadata {
39 | title: string;
40 | created?: number;
41 | updated?: number;
42 | pin?: number | boolean;
43 | user?: {
44 | id: string;
45 | name: string;
46 | displayName: string;
47 | photo: string;
48 | };
49 | lastUpdateUser?: {
50 | id: string;
51 | name: string;
52 | displayName: string;
53 | photo: string;
54 | };
55 | collaborators?: Array<{
56 | id: string;
57 | displayName: string;
58 | }>;
59 | words?: string[];
60 | lines?: string[];
61 | debug?: {
62 | warning?: string;
63 | error?: string;
64 | };
65 | }
66 |
67 | export interface FormatPageOptions {
68 | skip?: number;
69 | showSort?: boolean;
70 | showMatches?: boolean;
71 | sortValue?: string | null;
72 | showSnippet?: boolean;
73 | }
74 |
75 | export function formatYmd(date: Date): string {
76 | const y = date.getFullYear();
77 | const m = date.getMonth() + 1;
78 | const d = date.getDate();
79 | return `${y}/${m}/${d}`;
80 | }
81 |
82 | export function getSortDescription(sortMethod: string | undefined): string {
83 | const base = {
84 | updated: "Sorted by last updated",
85 | created: "Sorted by creation date",
86 | accessed: "Sorted by last accessed",
87 | linked: "Sorted by number of incoming links",
88 | views: "Sorted by view count",
89 | title: "Sorted by title"
90 | }[sortMethod || ''] || "Default order";
91 |
92 | return `${base}`;
93 | }
94 |
95 | export function getSortValue(page: ScrapboxPage, sortMethod: string | undefined): {
96 | value: number | string | null;
97 | formatted: string;
98 | } {
99 | switch (sortMethod) {
100 | case 'updated':
101 | return {
102 | value: page.updated || 0,
103 | formatted: page.updated ? formatYmd(new Date(page.updated * 1000)) : 'Not available'
104 | };
105 | case 'created':
106 | return {
107 | value: page.created || 0,
108 | formatted: page.created ? formatYmd(new Date(page.created * 1000)) : 'Not available'
109 | };
110 | case 'accessed':
111 | const accessTime = page.accessed || page.lastAccessed || 0;
112 | return {
113 | value: accessTime,
114 | formatted: accessTime ? formatYmd(new Date(accessTime * 1000)) : 'Not available'
115 | };
116 | case 'linked':
117 | return {
118 | value: page.linked || 0,
119 | formatted: String(page.linked || 0)
120 | };
121 | case 'views':
122 | return {
123 | value: page.views || 0,
124 | formatted: String(page.views || 0)
125 | };
126 | case 'title':
127 | return {
128 | value: page.title,
129 | formatted: page.title
130 | };
131 | default:
132 | return {
133 | value: null,
134 | formatted: 'Not specified'
135 | };
136 | }
137 | }
138 |
139 | export function formatPageOutput(
140 | page: ExtendedPage,
141 | index: number,
142 | options: {
143 | skip?: number,
144 | showSort?: boolean,
145 | sortValue?: string,
146 | showMatches?: boolean,
147 | showSnippet?: boolean,
148 | isSearchResult?: boolean, // 追加: 検索結果かどうかのフラグ
149 | showDescriptions?: boolean // 冒頭5行表示オプションを追加
150 | } = {}
151 | ): string {
152 | const lines = [
153 | `Page number: ${(options.skip || 0) + index + 1}`,
154 | `Title: ${page.title}`
155 | ];
156 |
157 | // 検索結果以外の場合のみ日付を表示
158 | if (!options.isSearchResult) {
159 | lines.push(
160 | `Created: ${formatYmd(new Date((page.created || 0) * 1000))}`,
161 | `Updated: ${formatYmd(new Date((page.updated || 0) * 1000))}`
162 | );
163 | }
164 |
165 | lines.push(`Pinned: ${page.pin ? 'Yes' : 'No'}`);
166 |
167 | if (options.showMatches && page.words) {
168 | lines.push(`Matched words: ${page.words.join(', ')}`);
169 | }
170 |
171 | if (options.showSort && options.sortValue) {
172 | lines.push(`Sort value: ${options.sortValue}`);
173 | }
174 |
175 | // 作成者の表示
176 | if (page.user) {
177 | lines.push(`Created user: ${page.user.displayName}`);
178 | }
179 |
180 | // 最終更新者の表示
181 | if (page.lastUpdateUser) {
182 | lines.push(`Last editor: ${page.lastUpdateUser.displayName}`);
183 | }
184 |
185 | if (page.collaborators && page.collaborators.length > 0) {
186 | const uniqueCollaborators = page.collaborators
187 | .filter(collab =>
188 | collab.id !== page.user?.id &&
189 | collab.id !== page.lastUpdateUser?.id
190 | )
191 | .map(collab => collab.displayName)
192 | .filter((value, index, self) => self.indexOf(value) === index);
193 |
194 | if (uniqueCollaborators.length > 0) {
195 | lines.push(`Other editors: ${uniqueCollaborators.join(', ')}`);
196 | }
197 | }
198 |
199 | if (options.showSnippet && page.lines) {
200 | lines.push('Snippet:');
201 | lines.push(page.lines.join('\n'));
202 | }
203 |
204 | if (options.showDescriptions && page.descriptions?.length) {
205 | lines.push('Description:');
206 | lines.push(page.descriptions.join('\n'));
207 | }
208 |
209 | return lines.join('\n');
210 | }
211 |
212 | // ScrapboxPageインターフェースをBasePageから継承
213 | export interface ScrapboxPage extends BasePage {}
214 |
--------------------------------------------------------------------------------
/src/utils/markdown-converter.ts:
--------------------------------------------------------------------------------
1 | import md2sb from 'md2sb';
2 |
3 | export async function convertMarkdownToScrapbox(markdown: string): Promise {
4 | // md2sbを直接呼び出し、エラーは上位に伝播させる
5 | return await md2sb.default(markdown);
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/sort.ts:
--------------------------------------------------------------------------------
1 | import type { ScrapboxPage } from '../cosense.js';
2 |
3 | export interface SortOptions {
4 | sort?: string;
5 | excludePinned?: boolean;
6 | }
7 |
8 | export function sortPages(pages: ScrapboxPage[], options: SortOptions = {}): ScrapboxPage[] {
9 | const { sort, excludePinned } = options;
10 |
11 | // ピン留めページのフィルタリング
12 | let filteredPages = excludePinned
13 | ? pages.filter(page => !page.pin)
14 | : pages;
15 |
16 | // ソート処理
17 | return [...filteredPages].sort((a, b) => {
18 | // ピン留めを考慮しないソート
19 | const compareValues = () => {
20 | switch (sort) {
21 | case 'updated':
22 | return (b.updated || 0) - (a.updated || 0);
23 | case 'created':
24 | return (b.created || 0) - (a.created || 0);
25 | case 'accessed':
26 | const aAccess = a.accessed || 0;
27 | const bAccess = b.accessed || 0;
28 | return bAccess - aAccess;
29 | case 'linked':
30 | return (b.linked || 0) - (a.linked || 0);
31 | case 'views':
32 | return (b.views || 0) - (a.views || 0);
33 | case 'title':
34 | return a.title.localeCompare(b.title);
35 | default:
36 | return (b.created || 0) - (a.created || 0);
37 | }
38 | };
39 |
40 | return compareValues();
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ES2020",
5 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
6 | "module": "node16",
7 | "moduleResolution": "nodenext",
8 | "outDir": "build",
9 | "rootDir": "src",
10 | "strict": true,
11 | "esModuleInterop": true,
12 | "skipLibCheck": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "allowJs": true,
15 | "declaration": true
16 | },
17 | "include": ["src/**/*"],
18 | "exclude": ["node_modules", "build"]
19 | }
20 |
--------------------------------------------------------------------------------