@dinoxx/dinox-cli 1.0.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/README.md +294 -0
- package/dist/auth/userInfo.d.ts +14 -0
- package/dist/auth/userInfo.js +115 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/cliTypes.d.ts +6 -0
- package/dist/cliTypes.js +1 -0
- package/dist/commands/auth/index.d.ts +2 -0
- package/dist/commands/auth/index.js +193 -0
- package/dist/commands/boxes/index.d.ts +2 -0
- package/dist/commands/boxes/index.js +107 -0
- package/dist/commands/boxes/repo.d.ts +21 -0
- package/dist/commands/boxes/repo.js +154 -0
- package/dist/commands/config/index.d.ts +2 -0
- package/dist/commands/config/index.js +67 -0
- package/dist/commands/info/index.d.ts +2 -0
- package/dist/commands/info/index.js +20 -0
- package/dist/commands/notes/index.d.ts +2 -0
- package/dist/commands/notes/index.js +271 -0
- package/dist/commands/notes/repo.d.ts +70 -0
- package/dist/commands/notes/repo.js +674 -0
- package/dist/commands/notes/searchTime.d.ts +9 -0
- package/dist/commands/notes/searchTime.js +85 -0
- package/dist/commands/prompt/index.d.ts +2 -0
- package/dist/commands/prompt/index.js +51 -0
- package/dist/commands/prompt/repo.d.ts +6 -0
- package/dist/commands/prompt/repo.js +18 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +68 -0
- package/dist/commands/tags/index.d.ts +2 -0
- package/dist/commands/tags/index.js +120 -0
- package/dist/commands/tags/repo.d.ts +14 -0
- package/dist/commands/tags/repo.js +247 -0
- package/dist/config/keys.d.ts +9 -0
- package/dist/config/keys.js +17 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +39 -0
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +56 -0
- package/dist/config/serviceEndpoints.d.ts +3 -0
- package/dist/config/serviceEndpoints.js +3 -0
- package/dist/config/store.d.ts +5 -0
- package/dist/config/store.js +87 -0
- package/dist/config/types.d.ts +51 -0
- package/dist/config/types.js +1 -0
- package/dist/dinox.d.ts +2 -0
- package/dist/dinox.js +50 -0
- package/dist/powersync/connector.d.ts +21 -0
- package/dist/powersync/connector.js +58 -0
- package/dist/powersync/runtime.d.ts +37 -0
- package/dist/powersync/runtime.js +107 -0
- package/dist/powersync/schema/content.d.ts +76 -0
- package/dist/powersync/schema/content.js +76 -0
- package/dist/powersync/schema/index.d.ts +371 -0
- package/dist/powersync/schema/index.js +35 -0
- package/dist/powersync/schema/local.d.ts +68 -0
- package/dist/powersync/schema/local.js +83 -0
- package/dist/powersync/schema/note.d.ts +34 -0
- package/dist/powersync/schema/note.js +34 -0
- package/dist/powersync/schema/notesExtras.d.ts +62 -0
- package/dist/powersync/schema/notesExtras.js +71 -0
- package/dist/powersync/schema/projects.d.ts +101 -0
- package/dist/powersync/schema/projects.js +101 -0
- package/dist/powersync/schema/tags.d.ts +37 -0
- package/dist/powersync/schema/tags.js +37 -0
- package/dist/powersync/tokenIndex.d.ts +17 -0
- package/dist/powersync/tokenIndex.js +202 -0
- package/dist/powersync/uploader.d.ts +7 -0
- package/dist/powersync/uploader.js +134 -0
- package/dist/utils/argValue.d.ts +1 -0
- package/dist/utils/argValue.js +17 -0
- package/dist/utils/errors.d.ts +10 -0
- package/dist/utils/errors.js +17 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.js +4 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +10 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +10 -0
- package/dist/utils/text.d.ts +1 -0
- package/dist/utils/text.js +35 -0
- package/dist/utils/time.d.ts +1 -0
- package/dist/utils/time.js +3 -0
- package/dist/utils/tiptapMarkdown.d.ts +6 -0
- package/dist/utils/tiptapMarkdown.js +149 -0
- package/dist/utils/tokenize.d.ts +1 -0
- package/dist/utils/tokenize.js +56 -0
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.js +21 -0
- package/package.json +63 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { nowIsoUtc } from '../utils/time.js';
|
|
3
|
+
import { stripMarkdown } from '../utils/text.js';
|
|
4
|
+
import { tokenizeWithJieba } from '../utils/tokenize.js';
|
|
5
|
+
const INDEX_STATE_ID = 'c_note';
|
|
6
|
+
const NOTE_LOCAL_FTS_TABLE = 'note_local_fts';
|
|
7
|
+
const DEFAULT_BATCH_SIZE = 300;
|
|
8
|
+
export async function syncNoteTokenIndex(db, options) {
|
|
9
|
+
const batchSize = Math.max(1, Math.trunc(options?.batchSize ?? DEFAULT_BATCH_SIZE));
|
|
10
|
+
const nowIso = options?.nowIso ?? nowIsoUtc;
|
|
11
|
+
const tokenize = options?.tokenize ?? tokenizeWithJieba;
|
|
12
|
+
await ensureFtsTable(db);
|
|
13
|
+
const state = await ensureIndexState(db, nowIso);
|
|
14
|
+
let cursorUpdatedAt = state.cursor_updated_at ?? '';
|
|
15
|
+
let cursorId = state.cursor_id ?? '';
|
|
16
|
+
const result = {
|
|
17
|
+
scanned: 0,
|
|
18
|
+
reindexed: 0,
|
|
19
|
+
skipped: 0,
|
|
20
|
+
removed: 0,
|
|
21
|
+
ftsRowsUpserted: 0,
|
|
22
|
+
cursor: {
|
|
23
|
+
updatedAt: cursorUpdatedAt,
|
|
24
|
+
id: cursorId,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
while (true) {
|
|
28
|
+
const rows = await db.getAll(`
|
|
29
|
+
SELECT
|
|
30
|
+
id,
|
|
31
|
+
title,
|
|
32
|
+
content_md,
|
|
33
|
+
content_text,
|
|
34
|
+
tags,
|
|
35
|
+
created_at,
|
|
36
|
+
updated_at,
|
|
37
|
+
is_del,
|
|
38
|
+
version
|
|
39
|
+
FROM c_note
|
|
40
|
+
WHERE
|
|
41
|
+
COALESCE(updated_at, created_at, '') > ?
|
|
42
|
+
OR (
|
|
43
|
+
COALESCE(updated_at, created_at, '') = ?
|
|
44
|
+
AND id > ?
|
|
45
|
+
)
|
|
46
|
+
ORDER BY COALESCE(updated_at, created_at, '') ASC, id ASC
|
|
47
|
+
LIMIT ?
|
|
48
|
+
`, [cursorUpdatedAt, cursorUpdatedAt, cursorId, batchSize]);
|
|
49
|
+
if (rows.length === 0) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
result.scanned += 1;
|
|
54
|
+
const sortKey = noteSortKey(row);
|
|
55
|
+
const bodySource = selectIndexBodyText(row);
|
|
56
|
+
const sourceHash = createSourceHash(row, bodySource);
|
|
57
|
+
const meta = await db.getOptional(`
|
|
58
|
+
SELECT source_hash
|
|
59
|
+
FROM note_local_index_meta
|
|
60
|
+
WHERE id = ?
|
|
61
|
+
`, [row.id]);
|
|
62
|
+
if (meta?.source_hash === sourceHash) {
|
|
63
|
+
result.skipped += 1;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const changed = await reindexOneNoteFts(db, row, sourceHash, bodySource, nowIso, tokenize);
|
|
67
|
+
if (changed.upserted) {
|
|
68
|
+
result.reindexed += 1;
|
|
69
|
+
result.ftsRowsUpserted += 1;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
result.removed += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
cursorUpdatedAt = sortKey;
|
|
76
|
+
cursorId = row.id;
|
|
77
|
+
}
|
|
78
|
+
await db.execute(`
|
|
79
|
+
UPDATE note_local_index_state
|
|
80
|
+
SET cursor_updated_at = ?, cursor_id = ?, updated_at = ?
|
|
81
|
+
WHERE id = ?
|
|
82
|
+
`, [cursorUpdatedAt, cursorId, nowIso(), INDEX_STATE_ID]);
|
|
83
|
+
}
|
|
84
|
+
result.cursor = {
|
|
85
|
+
updatedAt: cursorUpdatedAt,
|
|
86
|
+
id: cursorId,
|
|
87
|
+
};
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
async function ensureFtsTable(db) {
|
|
91
|
+
try {
|
|
92
|
+
await db.execute(`
|
|
93
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS ${NOTE_LOCAL_FTS_TABLE}
|
|
94
|
+
USING fts5(
|
|
95
|
+
note_id UNINDEXED,
|
|
96
|
+
title,
|
|
97
|
+
tags,
|
|
98
|
+
body,
|
|
99
|
+
tokenize = 'unicode61'
|
|
100
|
+
)
|
|
101
|
+
`, []);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
throw new Error(`Failed to initialize FTS5 table (${NOTE_LOCAL_FTS_TABLE}): ${message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function ensureIndexState(db, nowIso) {
|
|
109
|
+
const state = await db.getOptional(`
|
|
110
|
+
SELECT cursor_updated_at, cursor_id
|
|
111
|
+
FROM note_local_index_state
|
|
112
|
+
WHERE id = ?
|
|
113
|
+
`, [INDEX_STATE_ID]);
|
|
114
|
+
if (state) {
|
|
115
|
+
return state;
|
|
116
|
+
}
|
|
117
|
+
await db.execute(`
|
|
118
|
+
INSERT INTO note_local_index_state (id, cursor_updated_at, cursor_id, updated_at)
|
|
119
|
+
VALUES (?, ?, ?, ?)
|
|
120
|
+
`, [INDEX_STATE_ID, '', '', nowIso()]);
|
|
121
|
+
return {
|
|
122
|
+
cursor_updated_at: '',
|
|
123
|
+
cursor_id: '',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function reindexOneNoteFts(db, row, sourceHash, bodySource, nowIso, tokenize) {
|
|
127
|
+
const updatedAt = noteSortKey(row);
|
|
128
|
+
const indexedAt = nowIso();
|
|
129
|
+
const deleted = isDeleted(row.is_del);
|
|
130
|
+
await db.execute(`DELETE FROM ${NOTE_LOCAL_FTS_TABLE} WHERE note_id = ?`, [row.id]);
|
|
131
|
+
let upserted = false;
|
|
132
|
+
if (!deleted) {
|
|
133
|
+
const titleTokens = tokenize(row.title ?? '', 180).join(' ');
|
|
134
|
+
const tagTokens = tokenize(parseTags(row.tags).join(' '), 180).join(' ');
|
|
135
|
+
const bodyTokens = tokenize(bodySource, 800).join(' ');
|
|
136
|
+
await db.execute(`
|
|
137
|
+
INSERT INTO ${NOTE_LOCAL_FTS_TABLE} (note_id, title, tags, body)
|
|
138
|
+
VALUES (?, ?, ?, ?)
|
|
139
|
+
`, [row.id, titleTokens, tagTokens, bodyTokens]);
|
|
140
|
+
upserted = true;
|
|
141
|
+
}
|
|
142
|
+
const existingMeta = await db.getOptional(`
|
|
143
|
+
SELECT id
|
|
144
|
+
FROM note_local_index_meta
|
|
145
|
+
WHERE id = ?
|
|
146
|
+
`, [row.id]);
|
|
147
|
+
if (existingMeta) {
|
|
148
|
+
await db.execute(`
|
|
149
|
+
UPDATE note_local_index_meta
|
|
150
|
+
SET source_hash = ?, indexed_at = ?, note_updated_at = ?, is_del = ?
|
|
151
|
+
WHERE id = ?
|
|
152
|
+
`, [sourceHash, indexedAt, updatedAt, deleted ? 1 : 0, row.id]);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
await db.execute(`
|
|
156
|
+
INSERT INTO note_local_index_meta (id, source_hash, indexed_at, note_updated_at, is_del)
|
|
157
|
+
VALUES (?, ?, ?, ?, ?)
|
|
158
|
+
`, [row.id, sourceHash, indexedAt, updatedAt, deleted ? 1 : 0]);
|
|
159
|
+
}
|
|
160
|
+
return { upserted };
|
|
161
|
+
}
|
|
162
|
+
function parseTags(raw) {
|
|
163
|
+
if (!raw) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(raw);
|
|
168
|
+
if (!Array.isArray(parsed)) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
return parsed.map((value) => String(value).trim()).filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function noteSortKey(row) {
|
|
178
|
+
return row.updated_at ?? row.created_at ?? '';
|
|
179
|
+
}
|
|
180
|
+
function isDeleted(value) {
|
|
181
|
+
return typeof value === 'number' && value !== 0;
|
|
182
|
+
}
|
|
183
|
+
function selectIndexBodyText(row) {
|
|
184
|
+
const contentMd = row.content_md?.trim();
|
|
185
|
+
if (contentMd) {
|
|
186
|
+
const markdownText = stripMarkdown(contentMd);
|
|
187
|
+
if (markdownText) {
|
|
188
|
+
return markdownText;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return row.content_text?.trim() ?? '';
|
|
192
|
+
}
|
|
193
|
+
function createSourceHash(row, bodySource) {
|
|
194
|
+
const normalizedTags = parseTags(row.tags).join('\u0001');
|
|
195
|
+
const payload = JSON.stringify({
|
|
196
|
+
title: row.title ?? '',
|
|
197
|
+
bodySource,
|
|
198
|
+
tags: normalizedTags,
|
|
199
|
+
isDel: isDeleted(row.is_del) ? 1 : 0,
|
|
200
|
+
});
|
|
201
|
+
return crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
|
|
202
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { UpdateType } from '@powersync/common';
|
|
2
|
+
const FATAL_RESPONSE_CODES = [/^22...$/, /^23...$/, /^42501$/];
|
|
3
|
+
function shouldFallbackToLegacy(status) {
|
|
4
|
+
return status === 404 || status === 405;
|
|
5
|
+
}
|
|
6
|
+
function normalizeBaseUrl(baseUrl) {
|
|
7
|
+
return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
8
|
+
}
|
|
9
|
+
async function readErrorBody(response) {
|
|
10
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
11
|
+
const text = await response.text().catch(() => '');
|
|
12
|
+
if (contentType.includes('application/json')) {
|
|
13
|
+
try {
|
|
14
|
+
const json = JSON.parse(text);
|
|
15
|
+
const code = json?.error?.code ?? json?.code;
|
|
16
|
+
const message = json?.error?.message ?? json?.message;
|
|
17
|
+
return {
|
|
18
|
+
code: typeof code === 'string' ? code : undefined,
|
|
19
|
+
message: typeof message === 'string' ? message : undefined,
|
|
20
|
+
text,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { text };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { text };
|
|
28
|
+
}
|
|
29
|
+
function createUploadError(args) {
|
|
30
|
+
const details = args.message ?? args.text;
|
|
31
|
+
const message = details
|
|
32
|
+
? `PowerSync upload failed (${args.method} ${args.url}) [${args.status}]: ${details}`
|
|
33
|
+
: `PowerSync upload failed (${args.method} ${args.url}) [${args.status}]`;
|
|
34
|
+
const error = new Error(message);
|
|
35
|
+
error.name = 'PowerSyncUploadError';
|
|
36
|
+
error.code = args.code;
|
|
37
|
+
error.status = args.status;
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
function isFatalUploadError(error) {
|
|
41
|
+
if (!error || typeof error !== 'object') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const code = error.code;
|
|
45
|
+
return typeof code === 'string' && FATAL_RESPONSE_CODES.some(regex => regex.test(code));
|
|
46
|
+
}
|
|
47
|
+
async function dispatchMutation(options) {
|
|
48
|
+
const response = await fetch(options.url, {
|
|
49
|
+
method: options.method,
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: options.authorization,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(options.payload),
|
|
55
|
+
});
|
|
56
|
+
if (response.ok) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const body = await readErrorBody(response);
|
|
60
|
+
throw createUploadError({
|
|
61
|
+
status: response.status,
|
|
62
|
+
method: options.method,
|
|
63
|
+
url: options.url,
|
|
64
|
+
code: body.code,
|
|
65
|
+
message: body.message,
|
|
66
|
+
text: body.text,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function uploadTransaction(args) {
|
|
70
|
+
const baseUrl = normalizeBaseUrl(args.uploadBaseUrl);
|
|
71
|
+
const primaryUrl = `${baseUrl}${args.uploadV4Path}`;
|
|
72
|
+
const fallbackUrl = `${baseUrl}${args.uploadV2Path}`;
|
|
73
|
+
for (const op of args.transaction.crud) {
|
|
74
|
+
const record = { table: op.table, data: { ...(op.opData ?? {}), id: op.id } };
|
|
75
|
+
switch (op.op) {
|
|
76
|
+
case UpdateType.PUT:
|
|
77
|
+
try {
|
|
78
|
+
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PUT', payload: record });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (shouldFallbackToLegacy(error?.status)) {
|
|
82
|
+
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PUT', payload: record });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case UpdateType.PATCH:
|
|
90
|
+
try {
|
|
91
|
+
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PATCH', payload: record });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (shouldFallbackToLegacy(error?.status)) {
|
|
95
|
+
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PATCH', payload: record });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case UpdateType.DELETE:
|
|
103
|
+
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record });
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function createPowerSyncUploadHandler(options) {
|
|
111
|
+
return async (database) => {
|
|
112
|
+
const transaction = await database.getNextCrudTransaction();
|
|
113
|
+
if (!transaction) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await uploadTransaction({
|
|
118
|
+
transaction,
|
|
119
|
+
authorization: options.authorization,
|
|
120
|
+
uploadBaseUrl: options.uploadBaseUrl,
|
|
121
|
+
uploadV4Path: options.uploadV4Path,
|
|
122
|
+
uploadV2Path: options.uploadV2Path,
|
|
123
|
+
});
|
|
124
|
+
await transaction.complete();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (isFatalUploadError(error)) {
|
|
128
|
+
await transaction.complete();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readStringOrFile(value: string): Promise<string>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { DinoxError } from './errors.js';
|
|
3
|
+
export async function readStringOrFile(value) {
|
|
4
|
+
if (!value.startsWith('@')) {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
const filePath = value.slice(1);
|
|
8
|
+
if (!filePath) {
|
|
9
|
+
throw new DinoxError('Invalid @file syntax: expected @<path>');
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return await fs.readFile(filePath, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw new DinoxError(`Failed to read file: ${filePath}`, { cause: error });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class DinoxError extends Error {
|
|
2
|
+
readonly exitCode: number;
|
|
3
|
+
readonly details?: unknown;
|
|
4
|
+
constructor(message: string, options?: {
|
|
5
|
+
exitCode?: number;
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
details?: unknown;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export declare function isDinoxError(error: unknown): error is DinoxError;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class DinoxError extends Error {
|
|
2
|
+
exitCode;
|
|
3
|
+
details;
|
|
4
|
+
constructor(message, options) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'DinoxError';
|
|
7
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
8
|
+
this.details = options?.details;
|
|
9
|
+
if (options?.cause !== undefined) {
|
|
10
|
+
// Node 16+ supports Error.cause.
|
|
11
|
+
this.cause = options.cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isDinoxError(error) {
|
|
16
|
+
return Boolean(error) && typeof error === 'object' && error.name === 'DinoxError';
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateId(): string;
|
package/dist/utils/id.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redactAuthorization(authorization?: string): string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stripMarkdown(markdown: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import remarkGfm from 'remark-gfm';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkStringify from 'remark-stringify';
|
|
4
|
+
import stripMarkdownPlugin from 'strip-markdown';
|
|
5
|
+
import { unified } from 'unified';
|
|
6
|
+
const markdownProcessor = unified()
|
|
7
|
+
.use(remarkParse)
|
|
8
|
+
.use(remarkGfm)
|
|
9
|
+
.use(stripMarkdownPlugin)
|
|
10
|
+
.use(remarkStringify);
|
|
11
|
+
export function stripMarkdown(markdown) {
|
|
12
|
+
if (!markdown.trim()) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const plain = String(markdownProcessor.processSync(markdown).value);
|
|
17
|
+
return normalizeWhitespace(decodeHtmlEntities(plain));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return normalizeWhitespace(markdown);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function decodeHtmlEntities(value) {
|
|
24
|
+
return value
|
|
25
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
26
|
+
.replace(/&#([0-9]+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10)))
|
|
27
|
+
.replace(/&/g, '&')
|
|
28
|
+
.replace(/</g, '<')
|
|
29
|
+
.replace(/>/g, '>')
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/'/g, "'");
|
|
32
|
+
}
|
|
33
|
+
function normalizeWhitespace(value) {
|
|
34
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function nowIsoUtc(): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import { Image } from '@tiptap/extension-image';
|
|
3
|
+
import { ListItem } from '@tiptap/extension-list';
|
|
4
|
+
import { Markdown } from '@tiptap/markdown';
|
|
5
|
+
import { StarterKit } from '@tiptap/starter-kit';
|
|
6
|
+
import { TableKit } from '@tiptap/extension-table';
|
|
7
|
+
import { UniqueID, generateUniqueIds } from '@tiptap/extension-unique-id';
|
|
8
|
+
import { DinoxError } from './errors.js';
|
|
9
|
+
import { generateId } from './id.js';
|
|
10
|
+
const EMPTY_PARAGRAPH = { type: 'paragraph', content: [] };
|
|
11
|
+
const CONTENT_PREVIEW_MAX_LENGTH = 400;
|
|
12
|
+
const markdownExtension = Markdown.configure({
|
|
13
|
+
markedOptions: {
|
|
14
|
+
gfm: true,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const uniqueIdExtension = UniqueID.configure({
|
|
18
|
+
attributeName: 'id',
|
|
19
|
+
types: [
|
|
20
|
+
'heading',
|
|
21
|
+
'paragraph',
|
|
22
|
+
'blockquote',
|
|
23
|
+
'codeBlock',
|
|
24
|
+
'listItem',
|
|
25
|
+
'table',
|
|
26
|
+
'tableRow',
|
|
27
|
+
'tableCell',
|
|
28
|
+
'tableHeader',
|
|
29
|
+
],
|
|
30
|
+
generateID: () => generateId(),
|
|
31
|
+
updateDocument: false,
|
|
32
|
+
});
|
|
33
|
+
function ensureLeadingParagraph(content) {
|
|
34
|
+
if (content.length === 0) {
|
|
35
|
+
return [EMPTY_PARAGRAPH];
|
|
36
|
+
}
|
|
37
|
+
if (content[0]?.type === 'paragraph') {
|
|
38
|
+
return content;
|
|
39
|
+
}
|
|
40
|
+
return [EMPTY_PARAGRAPH, ...content];
|
|
41
|
+
}
|
|
42
|
+
// Tiptap markdown parser may produce listItem that starts with blockquote/list.
|
|
43
|
+
// ProseMirror schema requires paragraph as first child (paragraph block*).
|
|
44
|
+
const SafeListItem = ListItem.extend({
|
|
45
|
+
parseMarkdown: (token, helpers) => {
|
|
46
|
+
if (token.type !== 'list_item') {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
let content = [];
|
|
50
|
+
if (token.tokens && token.tokens.length > 0) {
|
|
51
|
+
const hasParagraphTokens = token.tokens.some((inner) => inner.type === 'paragraph');
|
|
52
|
+
if (hasParagraphTokens) {
|
|
53
|
+
content = helpers.parseChildren(token.tokens);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const firstToken = token.tokens[0];
|
|
57
|
+
if (firstToken?.type === 'text' && firstToken.tokens && firstToken.tokens.length > 0) {
|
|
58
|
+
content = [
|
|
59
|
+
{
|
|
60
|
+
type: 'paragraph',
|
|
61
|
+
content: helpers.parseInline(firstToken.tokens),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
if (token.tokens.length > 1) {
|
|
65
|
+
content.push(...helpers.parseChildren(token.tokens.slice(1)));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
content = helpers.parseChildren(token.tokens);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
type: 'listItem',
|
|
75
|
+
content: ensureLeadingParagraph(content),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const tiptapExtensions = [
|
|
80
|
+
StarterKit.configure({ listItem: false }),
|
|
81
|
+
SafeListItem,
|
|
82
|
+
Image,
|
|
83
|
+
TableKit,
|
|
84
|
+
markdownExtension,
|
|
85
|
+
uniqueIdExtension,
|
|
86
|
+
];
|
|
87
|
+
function sanitizeNode(node) {
|
|
88
|
+
if (!node || typeof node !== 'object') {
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
const next = { ...node };
|
|
92
|
+
if (Array.isArray(node.content)) {
|
|
93
|
+
next.content = node.content
|
|
94
|
+
.filter((child) => !!child && typeof child === 'object')
|
|
95
|
+
.map((child) => sanitizeNode(child));
|
|
96
|
+
}
|
|
97
|
+
if (next.type === 'listItem') {
|
|
98
|
+
const content = Array.isArray(next.content) ? next.content : [];
|
|
99
|
+
if (content.length === 0) {
|
|
100
|
+
next.content = [EMPTY_PARAGRAPH];
|
|
101
|
+
}
|
|
102
|
+
else if (content[0]?.type !== 'paragraph') {
|
|
103
|
+
next.content = [EMPTY_PARAGRAPH, ...content];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
function normalizeContentText(text) {
|
|
109
|
+
const normalized = (text ?? '').replace(/\r\n/g, '\n');
|
|
110
|
+
const lines = normalized
|
|
111
|
+
.split('\n')
|
|
112
|
+
.map((line) => line.trim())
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.filter((line) => !/^\[[^\]]+\]:\s*(?:<)?https?:\/\/\S+/i.test(line))
|
|
115
|
+
.filter((line) => !/^(https?:\/\/|www\.)\S+$/i.test(line));
|
|
116
|
+
const cleaned = lines
|
|
117
|
+
.join(' ')
|
|
118
|
+
.replace(/\[\^[^\]]+\]/g, '')
|
|
119
|
+
.replace(/\[(\d{1,4})\]/g, '')
|
|
120
|
+
.replace(/\bhttps?:\/\/\S+/gi, '')
|
|
121
|
+
.replace(/\bwww\.\S+/gi, '')
|
|
122
|
+
.replace(/\s{2,}/g, ' ')
|
|
123
|
+
.trim();
|
|
124
|
+
if (cleaned.length <= CONTENT_PREVIEW_MAX_LENGTH) {
|
|
125
|
+
return cleaned;
|
|
126
|
+
}
|
|
127
|
+
return cleaned.slice(0, CONTENT_PREVIEW_MAX_LENGTH);
|
|
128
|
+
}
|
|
129
|
+
export function convertMarkdownToNoteContent(markdown) {
|
|
130
|
+
let editor = null;
|
|
131
|
+
try {
|
|
132
|
+
editor = new Editor({
|
|
133
|
+
editable: false,
|
|
134
|
+
extensions: tiptapExtensions,
|
|
135
|
+
content: markdown,
|
|
136
|
+
contentType: 'markdown',
|
|
137
|
+
});
|
|
138
|
+
const sanitized = sanitizeNode(editor.getJSON());
|
|
139
|
+
const contentJson = generateUniqueIds(sanitized, tiptapExtensions);
|
|
140
|
+
const contentText = normalizeContentText(editor.getText());
|
|
141
|
+
return { contentJson, contentText };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new DinoxError('Failed to convert markdown to TipTap JSON', { cause: error });
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
editor?.destroy();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function tokenizeWithJieba(text: string, limit?: number): string[];
|