@mnemosyne_os/affine-reader 0.1.0
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/LICENSE +21 -0
- package/NOTICE.md +42 -0
- package/README.md +89 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +79 -0
- package/dist/exportMarkdown.d.ts +46 -0
- package/dist/exportMarkdown.js +104 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +34 -0
- package/dist/locate.d.ts +24 -0
- package/dist/locate.js +91 -0
- package/dist/nodeSqlite.d.ts +20 -0
- package/dist/nodeSqlite.js +50 -0
- package/dist/read.d.ts +45 -0
- package/dist/read.js +197 -0
- package/dist/stage.d.ts +27 -0
- package/dist/stage.js +54 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +10 -0
- package/dist/vendor/affine/blocksuite-types.d.ts +25 -0
- package/dist/vendor/affine/blocksuite-types.js +14 -0
- package/dist/vendor/affine/delta-to-md/delta-converters.d.ts +39 -0
- package/dist/vendor/affine/delta-to-md/delta-converters.js +81 -0
- package/dist/vendor/affine/delta-to-md/delta-to-md.d.ts +1 -0
- package/dist/vendor/affine/delta-to-md/delta-to-md.js +132 -0
- package/dist/vendor/affine/delta-to-md/index.d.ts +2 -0
- package/dist/vendor/affine/delta-to-md/index.js +7 -0
- package/dist/vendor/affine/delta-to-md/utils/node.d.ts +13 -0
- package/dist/vendor/affine/delta-to-md/utils/node.js +59 -0
- package/dist/vendor/affine/delta-to-md/utils/url.d.ts +1 -0
- package/dist/vendor/affine/delta-to-md/utils/url.js +8 -0
- package/dist/vendor/affine/parser.d.ts +5 -0
- package/dist/vendor/affine/parser.js +386 -0
- package/dist/vendor/affine/types.d.ts +100 -0
- package/dist/vendor/affine/types.js +2 -0
- package/package.json +64 -0
package/dist/read.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an AFFiNE workspace database into documents.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline, measured end to end against AFFiNE 0.27.4 on 2026-09-06:
|
|
5
|
+
* storage.db (+ -wal) -> snapshot + updates -> Y.Doc -> AFFiNE's own parser -> Markdown
|
|
6
|
+
*
|
|
7
|
+
* 🚨 A snapshot ALONE is stale. A doc typed one minute earlier decoded to 2
|
|
8
|
+
* characters from its snapshot and 323 once its 16 `updates` rows were replayed.
|
|
9
|
+
* The onboarding docs, whose snapshots are current, give the SAME answer either
|
|
10
|
+
* way — so a test written on them passes while the reader loses everything the
|
|
11
|
+
* person just wrote. Every load here replays the updates.
|
|
12
|
+
*/
|
|
13
|
+
import * as Y from 'yjs';
|
|
14
|
+
import type { AffineSchema, BlobRef, ReadOptions, SqliteDatabase, WorkspaceContent } from './types';
|
|
15
|
+
/** Reads the schema the file actually has rather than trusting a directory shape. */
|
|
16
|
+
export declare function detectSchema(db: SqliteDatabase): AffineSchema;
|
|
17
|
+
/** The workspace id, which is ALSO the id of the root doc holding the doc index. */
|
|
18
|
+
export declare function readWorkspaceId(db: SqliteDatabase, schema: AffineSchema): string | null;
|
|
19
|
+
interface LoadResult {
|
|
20
|
+
doc: Y.Doc;
|
|
21
|
+
hadSnapshot: boolean;
|
|
22
|
+
updatesReplayed: number;
|
|
23
|
+
}
|
|
24
|
+
/** Snapshot first, then every update in order. Both halves, always. */
|
|
25
|
+
export declare function loadDoc(db: SqliteDatabase, schema: AffineSchema, docId: string | null): LoadResult;
|
|
26
|
+
/**
|
|
27
|
+
* The doc index, straight out of the root doc's `meta.pages`.
|
|
28
|
+
* (This is AFFiNE's `readAllDocsFromRootDoc`, which is pure yjs.)
|
|
29
|
+
*/
|
|
30
|
+
export declare function readDocIndex(rootDoc: Y.Doc): {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
trash: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
/** Blob metadata only — the bytes are streamed separately, a workspace can hold GBs. */
|
|
36
|
+
export declare function readBlobIndex(db: SqliteDatabase, schema: AffineSchema): BlobRef[];
|
|
37
|
+
/** Streams blob bytes one row at a time so a large workspace never lands in memory at once. */
|
|
38
|
+
export declare function forEachBlob(db: SqliteDatabase, schema: AffineSchema, visit: (blob: {
|
|
39
|
+
key: string;
|
|
40
|
+
mime: string;
|
|
41
|
+
data: Uint8Array;
|
|
42
|
+
}) => void): void;
|
|
43
|
+
/** Every doc of one workspace, rendered. Docs that fail are NAMED, never dropped. */
|
|
44
|
+
export declare function readWorkspace(db: SqliteDatabase, options?: ReadOptions): WorkspaceContent;
|
|
45
|
+
export {};
|
package/dist/read.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turning an AFFiNE workspace database into documents.
|
|
4
|
+
*
|
|
5
|
+
* The pipeline, measured end to end against AFFiNE 0.27.4 on 2026-09-06:
|
|
6
|
+
* storage.db (+ -wal) -> snapshot + updates -> Y.Doc -> AFFiNE's own parser -> Markdown
|
|
7
|
+
*
|
|
8
|
+
* 🚨 A snapshot ALONE is stale. A doc typed one minute earlier decoded to 2
|
|
9
|
+
* characters from its snapshot and 323 once its 16 `updates` rows were replayed.
|
|
10
|
+
* The onboarding docs, whose snapshots are current, give the SAME answer either
|
|
11
|
+
* way — so a test written on them passes while the reader loses everything the
|
|
12
|
+
* person just wrote. Every load here replays the updates.
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.detectSchema = detectSchema;
|
|
49
|
+
exports.readWorkspaceId = readWorkspaceId;
|
|
50
|
+
exports.loadDoc = loadDoc;
|
|
51
|
+
exports.readDocIndex = readDocIndex;
|
|
52
|
+
exports.readBlobIndex = readBlobIndex;
|
|
53
|
+
exports.forEachBlob = forEachBlob;
|
|
54
|
+
exports.readWorkspace = readWorkspace;
|
|
55
|
+
const Y = __importStar(require("yjs"));
|
|
56
|
+
const parser_1 = require("./vendor/affine/parser");
|
|
57
|
+
/** Reads the schema the file actually has rather than trusting a directory shape. */
|
|
58
|
+
function detectSchema(db) {
|
|
59
|
+
const tables = new Set(db
|
|
60
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
61
|
+
.all()
|
|
62
|
+
.map((row) => String(row.name)));
|
|
63
|
+
if (!tables.has('updates'))
|
|
64
|
+
throw new Error('Not an AFFiNE workspace database: no `updates` table');
|
|
65
|
+
return tables.has('snapshots') ? 'v2' : 'v1';
|
|
66
|
+
}
|
|
67
|
+
/** The workspace id, which is ALSO the id of the root doc holding the doc index. */
|
|
68
|
+
function readWorkspaceId(db, schema) {
|
|
69
|
+
if (schema === 'v2') {
|
|
70
|
+
const row = db.prepare('SELECT space_id FROM meta').get();
|
|
71
|
+
return row && typeof row.space_id === 'string' ? row.space_id : null;
|
|
72
|
+
}
|
|
73
|
+
// v1 has no `meta`. The root doc is the one whose updates carry no doc_id.
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function toBytes(value, what) {
|
|
77
|
+
if (value instanceof Uint8Array)
|
|
78
|
+
return value;
|
|
79
|
+
if (value instanceof ArrayBuffer)
|
|
80
|
+
return new Uint8Array(value);
|
|
81
|
+
throw new Error(`${what}: expected a BLOB, got ${typeof value}`);
|
|
82
|
+
}
|
|
83
|
+
/** Snapshot first, then every update in order. Both halves, always. */
|
|
84
|
+
function loadDoc(db, schema, docId) {
|
|
85
|
+
const doc = new Y.Doc();
|
|
86
|
+
let hadSnapshot = false;
|
|
87
|
+
if (schema === 'v2' && docId !== null) {
|
|
88
|
+
const snap = db.prepare('SELECT data FROM snapshots WHERE doc_id = ?').get(docId);
|
|
89
|
+
if (snap) {
|
|
90
|
+
Y.applyUpdate(doc, toBytes(snap.data, `snapshot of ${docId}`));
|
|
91
|
+
hadSnapshot = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const rows = schema === 'v2'
|
|
95
|
+
? db.prepare('SELECT data FROM updates WHERE doc_id = ? ORDER BY created_at').all(docId)
|
|
96
|
+
: docId === null
|
|
97
|
+
? db.prepare('SELECT data FROM updates WHERE doc_id IS NULL ORDER BY id').all()
|
|
98
|
+
: db
|
|
99
|
+
.prepare('SELECT data FROM updates WHERE doc_id = ? ORDER BY id')
|
|
100
|
+
.all(docId);
|
|
101
|
+
for (const row of rows)
|
|
102
|
+
Y.applyUpdate(doc, toBytes(row.data, `update of ${docId ?? 'root'}`));
|
|
103
|
+
return { doc, hadSnapshot, updatesReplayed: rows.length };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The doc index, straight out of the root doc's `meta.pages`.
|
|
107
|
+
* (This is AFFiNE's `readAllDocsFromRootDoc`, which is pure yjs.)
|
|
108
|
+
*/
|
|
109
|
+
function readDocIndex(rootDoc) {
|
|
110
|
+
const pages = rootDoc.getMap('meta').get('pages');
|
|
111
|
+
if (!(pages instanceof Y.Array))
|
|
112
|
+
return [];
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const page of pages) {
|
|
115
|
+
if (!(page instanceof Y.Map))
|
|
116
|
+
continue;
|
|
117
|
+
const id = page.get('id');
|
|
118
|
+
if (typeof id !== 'string')
|
|
119
|
+
continue;
|
|
120
|
+
const title = page.get('title');
|
|
121
|
+
out.push({
|
|
122
|
+
id,
|
|
123
|
+
title: typeof title === 'string' ? title : '',
|
|
124
|
+
trash: page.get('trash') === true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** Blob metadata only — the bytes are streamed separately, a workspace can hold GBs. */
|
|
130
|
+
function readBlobIndex(db, schema) {
|
|
131
|
+
const rows = schema === 'v2'
|
|
132
|
+
? db.prepare('SELECT key, mime, size FROM blobs WHERE deleted_at IS NULL').all()
|
|
133
|
+
: db.prepare('SELECT key, length(data) AS size FROM blobs').all();
|
|
134
|
+
return rows.map((row) => ({
|
|
135
|
+
key: String(row.key),
|
|
136
|
+
// v1 stored no mime. An unknown type is unknown — never guessed into image/png.
|
|
137
|
+
mime: typeof row.mime === 'string' && row.mime ? row.mime : 'application/octet-stream',
|
|
138
|
+
size: typeof row.size === 'number' ? row.size : null,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
/** Streams blob bytes one row at a time so a large workspace never lands in memory at once. */
|
|
142
|
+
function forEachBlob(db, schema, visit) {
|
|
143
|
+
const rows = schema === 'v2'
|
|
144
|
+
? db.prepare('SELECT key, mime, data FROM blobs WHERE deleted_at IS NULL').all()
|
|
145
|
+
: db.prepare('SELECT key, data FROM blobs').all();
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
visit({
|
|
148
|
+
key: String(row.key),
|
|
149
|
+
mime: typeof row.mime === 'string' && row.mime ? row.mime : 'application/octet-stream',
|
|
150
|
+
data: toBytes(row.data, `blob ${String(row.key)}`),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Every doc of one workspace, rendered. Docs that fail are NAMED, never dropped. */
|
|
155
|
+
function readWorkspace(db, options = {}) {
|
|
156
|
+
const schema = detectSchema(db);
|
|
157
|
+
const workspaceId = readWorkspaceId(db, schema) ?? '';
|
|
158
|
+
const root = loadDoc(db, schema, schema === 'v2' ? workspaceId : null).doc;
|
|
159
|
+
const index = readDocIndex(root);
|
|
160
|
+
const docs = [];
|
|
161
|
+
const skipped = [];
|
|
162
|
+
let trashed = 0;
|
|
163
|
+
for (const entry of index) {
|
|
164
|
+
if (entry.trash) {
|
|
165
|
+
trashed++;
|
|
166
|
+
if (!options.includeTrash)
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const loaded = loadDoc(db, schema, entry.id);
|
|
171
|
+
const ctx = {
|
|
172
|
+
workspaceId,
|
|
173
|
+
doc: loaded.doc,
|
|
174
|
+
buildBlobUrl: (key) => options.blobUrl?.(key) ?? `affine-blob:${key}`,
|
|
175
|
+
buildDocUrl: (docId) => options.docUrl?.(docId) ?? `affine-doc:${docId}`,
|
|
176
|
+
};
|
|
177
|
+
const parsed = (0, parser_1.parsePageDoc)(ctx);
|
|
178
|
+
docs.push({
|
|
179
|
+
id: entry.id,
|
|
180
|
+
// The index title is what AFFiNE shows in its sidebar; the parsed one comes
|
|
181
|
+
// from the page block. They can differ mid-edit — prefer the page block and
|
|
182
|
+
// fall back, rather than rendering a doc with no name at all.
|
|
183
|
+
title: parsed.title || entry.title,
|
|
184
|
+
markdown: parsed.md,
|
|
185
|
+
updatesReplayed: loaded.updatesReplayed,
|
|
186
|
+
hadSnapshot: loaded.hadSnapshot,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
skipped.push({
|
|
191
|
+
id: entry.id,
|
|
192
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { workspaceId, schema, docs, skipped, blobs: readBlobIndex(db, schema), trashed };
|
|
197
|
+
}
|
package/dist/stage.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taking a safe copy of a LIVE AFFiNE database before reading it.
|
|
3
|
+
*
|
|
4
|
+
* 🚨 The content is in the -wal, not in storage.db. Measured on a real 0.27.4
|
|
5
|
+
* install 2026-09-06: `storage.db` 4 KB, `storage.db-wal` 1.8 MB. A reader that
|
|
6
|
+
* copies storage.db alone opens an EMPTY workspace and has no way to notice —
|
|
7
|
+
* it does not error, it just returns nothing.
|
|
8
|
+
*
|
|
9
|
+
* â›” Nothing here ever writes into AFFiNE's directory. Opening the live file
|
|
10
|
+
* read-write could checkpoint or corrupt a database another process owns.
|
|
11
|
+
*/
|
|
12
|
+
/** The journal files that must travel with storage.db. Order matters: -wal first. */
|
|
13
|
+
export declare const SIDECAR_SUFFIXES: readonly ["-wal", "-shm"];
|
|
14
|
+
export interface StagedDatabase {
|
|
15
|
+
/** Path of the copy — this is what to open. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Which sidecars were actually present and copied. */
|
|
18
|
+
sidecars: string[];
|
|
19
|
+
/** Removes the copy. Safe to call twice. */
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Copy `dbPath` and its journal sidecars into `stagingDir`.
|
|
24
|
+
*
|
|
25
|
+
* `stagingDir` must be a directory this process owns — it is REMOVED by dispose().
|
|
26
|
+
*/
|
|
27
|
+
export declare function stageDatabase(dbPath: string, stagingDir: string): StagedDatabase;
|
package/dist/stage.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Taking a safe copy of a LIVE AFFiNE database before reading it.
|
|
4
|
+
*
|
|
5
|
+
* 🚨 The content is in the -wal, not in storage.db. Measured on a real 0.27.4
|
|
6
|
+
* install 2026-09-06: `storage.db` 4 KB, `storage.db-wal` 1.8 MB. A reader that
|
|
7
|
+
* copies storage.db alone opens an EMPTY workspace and has no way to notice —
|
|
8
|
+
* it does not error, it just returns nothing.
|
|
9
|
+
*
|
|
10
|
+
* â›” Nothing here ever writes into AFFiNE's directory. Opening the live file
|
|
11
|
+
* read-write could checkpoint or corrupt a database another process owns.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SIDECAR_SUFFIXES = void 0;
|
|
15
|
+
exports.stageDatabase = stageDatabase;
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
/** The journal files that must travel with storage.db. Order matters: -wal first. */
|
|
19
|
+
exports.SIDECAR_SUFFIXES = ['-wal', '-shm'];
|
|
20
|
+
/**
|
|
21
|
+
* Copy `dbPath` and its journal sidecars into `stagingDir`.
|
|
22
|
+
*
|
|
23
|
+
* `stagingDir` must be a directory this process owns — it is REMOVED by dispose().
|
|
24
|
+
*/
|
|
25
|
+
function stageDatabase(dbPath, stagingDir) {
|
|
26
|
+
if (!(0, node_fs_1.existsSync)(dbPath))
|
|
27
|
+
throw new Error(`AFFiNE database not found: ${dbPath}`);
|
|
28
|
+
// â›” A floor under the delete. dispose() removes this directory recursively, so
|
|
29
|
+
// it must be one we created and own. Handed an existing folder with anything in
|
|
30
|
+
// it, this would be a public API that erases whatever it was pointed at.
|
|
31
|
+
if ((0, node_fs_1.existsSync)(stagingDir) && (0, node_fs_1.readdirSync)(stagingDir).length > 0)
|
|
32
|
+
throw new Error(`Staging directory is not empty, refusing to use it: ${stagingDir}`);
|
|
33
|
+
(0, node_fs_1.mkdirSync)(stagingDir, { recursive: true });
|
|
34
|
+
const target = (0, node_path_1.join)(stagingDir, 'storage.db');
|
|
35
|
+
(0, node_fs_1.copyFileSync)(dbPath, target);
|
|
36
|
+
const sidecars = [];
|
|
37
|
+
for (const suffix of exports.SIDECAR_SUFFIXES) {
|
|
38
|
+
if ((0, node_fs_1.existsSync)(dbPath + suffix)) {
|
|
39
|
+
(0, node_fs_1.copyFileSync)(dbPath + suffix, target + suffix);
|
|
40
|
+
sidecars.push(suffix);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let disposed = false;
|
|
44
|
+
return {
|
|
45
|
+
path: target,
|
|
46
|
+
sidecars,
|
|
47
|
+
dispose() {
|
|
48
|
+
if (disposed)
|
|
49
|
+
return;
|
|
50
|
+
disposed = true;
|
|
51
|
+
(0, node_fs_1.rmSync)(stagingDir, { recursive: true, force: true });
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for @mnemosyne_os/affine-reader.
|
|
3
|
+
*
|
|
4
|
+
* The package brings NO SQLite driver of its own. Electron 31 ships Node 20, which
|
|
5
|
+
* has no `node:sqlite`, while a plain CLI on Node >= 22.5 does — so the driver is
|
|
6
|
+
* injected and the package stays honest in both. `openNodeSqlite()` is provided for
|
|
7
|
+
* the CLI case; the Electron host passes its own better-sqlite3 adapter.
|
|
8
|
+
*/
|
|
9
|
+
export interface SqliteStatement {
|
|
10
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
11
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface SqliteDatabase {
|
|
14
|
+
prepare(sql: string): SqliteStatement;
|
|
15
|
+
close(): void;
|
|
16
|
+
}
|
|
17
|
+
/** MUST open the file READ-ONLY. Two writers on one CRDT corrupt it. */
|
|
18
|
+
export type SqliteOpener = (path: string) => SqliteDatabase;
|
|
19
|
+
/** Which on-disk layout a workspace uses. `v1` is the pre-nbstore shape. */
|
|
20
|
+
export type AffineSchema = 'v1' | 'v2';
|
|
21
|
+
export interface WorkspaceRef {
|
|
22
|
+
/** Workspace id as AFFiNE names the directory. */
|
|
23
|
+
id: string;
|
|
24
|
+
/**
|
|
25
|
+
* `local` for a workspace that never left the machine; otherwise the escaped
|
|
26
|
+
* server name — a synced workspace keeps the SAME schema, only the folder differs.
|
|
27
|
+
*/
|
|
28
|
+
peer: string;
|
|
29
|
+
kind: 'workspace' | 'userspace';
|
|
30
|
+
/** Absolute path to `storage.db`. */
|
|
31
|
+
dbPath: string;
|
|
32
|
+
layout: AffineSchema;
|
|
33
|
+
}
|
|
34
|
+
export interface AffineDoc {
|
|
35
|
+
id: string;
|
|
36
|
+
/** Title as the ROOT doc records it. May be empty — that is a real AFFiNE state. */
|
|
37
|
+
title: string;
|
|
38
|
+
markdown: string;
|
|
39
|
+
/** How many `updates` rows were replayed on top of the snapshot. */
|
|
40
|
+
updatesReplayed: number;
|
|
41
|
+
/** False when the doc has no snapshot row yet — normal for a freshly created doc. */
|
|
42
|
+
hadSnapshot: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface SkippedDoc {
|
|
45
|
+
id: string;
|
|
46
|
+
/** Why this doc is not in `docs`. Never dropped silently. */
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
export interface BlobRef {
|
|
50
|
+
key: string;
|
|
51
|
+
mime: string;
|
|
52
|
+
/** Size AFFiNE recorded. `null` when the column held something unreadable. */
|
|
53
|
+
size: number | null;
|
|
54
|
+
}
|
|
55
|
+
export interface WorkspaceContent {
|
|
56
|
+
workspaceId: string;
|
|
57
|
+
schema: AffineSchema;
|
|
58
|
+
docs: AffineDoc[];
|
|
59
|
+
/** Docs found in the index but not rendered, each with its reason. */
|
|
60
|
+
skipped: SkippedDoc[];
|
|
61
|
+
blobs: BlobRef[];
|
|
62
|
+
/** Docs in the trash are counted, never rendered. */
|
|
63
|
+
trashed: number;
|
|
64
|
+
}
|
|
65
|
+
export interface ReadOptions {
|
|
66
|
+
/** Include docs AFFiNE has moved to the trash. Default false. */
|
|
67
|
+
includeTrash?: boolean;
|
|
68
|
+
/** How an image block's blob key becomes a link. Return null for "cannot point at it". */
|
|
69
|
+
blobUrl?: (key: string) => string | null;
|
|
70
|
+
/** How a link to another AFFiNE doc is rendered. */
|
|
71
|
+
docUrl?: (docId: string) => string;
|
|
72
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Public types for @mnemosyne_os/affine-reader.
|
|
4
|
+
*
|
|
5
|
+
* The package brings NO SQLite driver of its own. Electron 31 ships Node 20, which
|
|
6
|
+
* has no `node:sqlite`, while a plain CLI on Node >= 22.5 does — so the driver is
|
|
7
|
+
* injected and the package stays honest in both. `openNodeSqlite()` is provided for
|
|
8
|
+
* the CLI case; the Electron host passes its own better-sqlite3 adapter.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local stand-ins for the two types the vendored parser imported from
|
|
3
|
+
* `@blocksuite/affine/model`.
|
|
4
|
+
*
|
|
5
|
+
* Both were `import type` — erased at compilation, never present at runtime —
|
|
6
|
+
* so replacing them changes NO behaviour. They are declared here only so this
|
|
7
|
+
* package does not need `@blocksuite/affine` (MPL-2.0) on its resolution path
|
|
8
|
+
* just to typecheck. See ../../..//NOTICE.md.
|
|
9
|
+
*
|
|
10
|
+
* Shapes derive from how `parser.ts` actually reads them (a database block's
|
|
11
|
+
* `prop:columns` and `prop:cells`), not from BlockSuite's full model.
|
|
12
|
+
*/
|
|
13
|
+
/** One column of an `affine:database` block. */
|
|
14
|
+
export interface ColumnDataType {
|
|
15
|
+
id: string;
|
|
16
|
+
/** 'title' | 'select' | 'multi-select' | … — free-form, the parser switches on it. */
|
|
17
|
+
type: string;
|
|
18
|
+
name: string;
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/** One cell of an `affine:database` block. */
|
|
22
|
+
export interface CellDataType {
|
|
23
|
+
columnId?: string;
|
|
24
|
+
value?: unknown;
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local stand-ins for the two types the vendored parser imported from
|
|
4
|
+
* `@blocksuite/affine/model`.
|
|
5
|
+
*
|
|
6
|
+
* Both were `import type` — erased at compilation, never present at runtime —
|
|
7
|
+
* so replacing them changes NO behaviour. They are declared here only so this
|
|
8
|
+
* package does not need `@blocksuite/affine` (MPL-2.0) on its resolution path
|
|
9
|
+
* just to typecheck. See ../../..//NOTICE.md.
|
|
10
|
+
*
|
|
11
|
+
* Shapes derive from how `parser.ts` actually reads them (a database block's
|
|
12
|
+
* `prop:columns` and `prop:cells`), not from BlockSuite's full model.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Node } from './utils/node';
|
|
2
|
+
export interface InlineReference {
|
|
3
|
+
type: 'LinkedPage';
|
|
4
|
+
pageId: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
params?: {
|
|
7
|
+
mode: 'doc' | 'edgeless';
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface ConverterOptions {
|
|
11
|
+
convertInlineReferenceLink?: (reference: InlineReference) => {
|
|
12
|
+
title: string;
|
|
13
|
+
link: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare function getConverters(opts?: ConverterOptions): {
|
|
17
|
+
embed: {
|
|
18
|
+
image: (src: any) => void;
|
|
19
|
+
thematic_break: () => void;
|
|
20
|
+
};
|
|
21
|
+
inline: {
|
|
22
|
+
italic: () => string[];
|
|
23
|
+
bold: () => string[];
|
|
24
|
+
link: (url: any) => string[];
|
|
25
|
+
reference: (reference: InlineReference) => string[];
|
|
26
|
+
strike: () => string[];
|
|
27
|
+
code: () => string[];
|
|
28
|
+
};
|
|
29
|
+
block: {
|
|
30
|
+
header: ({ header }: {
|
|
31
|
+
header: any;
|
|
32
|
+
}) => void;
|
|
33
|
+
blockquote: () => void;
|
|
34
|
+
list: {
|
|
35
|
+
group: () => Node;
|
|
36
|
+
line: (attrs: any, group: any) => void;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConverters = getConverters;
|
|
4
|
+
// oxlint-disable
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
const node_1 = require("./utils/node");
|
|
7
|
+
const url_1 = require("./utils/url");
|
|
8
|
+
const defaultConvertInlineReferenceLink = (reference) => {
|
|
9
|
+
return {
|
|
10
|
+
title: reference.title || '',
|
|
11
|
+
link: [reference.type, reference.pageId, reference.params?.mode]
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.join(':'),
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
function getConverters(opts = {}) {
|
|
17
|
+
const { convertInlineReferenceLink = defaultConvertInlineReferenceLink } = opts;
|
|
18
|
+
return {
|
|
19
|
+
embed: {
|
|
20
|
+
image: function (src) {
|
|
21
|
+
this.append('(src) + ')');
|
|
22
|
+
},
|
|
23
|
+
// Not a default Quill feature, converts custom divider embed blot added when
|
|
24
|
+
// creating quill editor instance.
|
|
25
|
+
// See https://quilljs.com/guides/cloning-medium-with-parchment/#dividers
|
|
26
|
+
thematic_break: function () {
|
|
27
|
+
this.open = '\n---\n' + this.open;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
inline: {
|
|
31
|
+
italic: function () {
|
|
32
|
+
return ['_', '_'];
|
|
33
|
+
},
|
|
34
|
+
bold: function () {
|
|
35
|
+
return ['**', '**'];
|
|
36
|
+
},
|
|
37
|
+
link: function (url) {
|
|
38
|
+
return ['[', '](' + url + ')'];
|
|
39
|
+
},
|
|
40
|
+
reference: function (reference) {
|
|
41
|
+
const { title, link } = convertInlineReferenceLink(reference);
|
|
42
|
+
return ['[', `${title}](${link})`];
|
|
43
|
+
},
|
|
44
|
+
strike: function () {
|
|
45
|
+
return ['~~', '~~'];
|
|
46
|
+
},
|
|
47
|
+
code: function () {
|
|
48
|
+
return ['`', '`'];
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
block: {
|
|
52
|
+
header: function ({ header }) {
|
|
53
|
+
this.open = '#'.repeat(header) + ' ' + this.open;
|
|
54
|
+
},
|
|
55
|
+
blockquote: function () {
|
|
56
|
+
this.open = '> ' + this.open;
|
|
57
|
+
},
|
|
58
|
+
list: {
|
|
59
|
+
group: function () {
|
|
60
|
+
return new node_1.Node(['', '\n']);
|
|
61
|
+
},
|
|
62
|
+
line: function (attrs, group) {
|
|
63
|
+
if (attrs.list === 'bullet') {
|
|
64
|
+
this.open = '- ' + this.open;
|
|
65
|
+
}
|
|
66
|
+
else if (attrs.list === 'checked') {
|
|
67
|
+
this.open = '- [x] ' + this.open;
|
|
68
|
+
}
|
|
69
|
+
else if (attrs.list === 'unchecked') {
|
|
70
|
+
this.open = '- [ ] ' + this.open;
|
|
71
|
+
}
|
|
72
|
+
else if (attrs.list === 'ordered') {
|
|
73
|
+
group.count = group.count || 0;
|
|
74
|
+
var count = ++group.count;
|
|
75
|
+
this.open = count + '. ' + this.open;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const deltaToMd: (delta: any, converters: any) => string;
|