Compare commits

...

13 Commits

Author SHA1 Message Date
Jiaxin Peng
db66ef137c Update version to 2.7.0 2025-10-27 23:21:30 +00:00
Jiaxin Peng
a152201510 Merge pull request #64 from jxpeng98/callout-support
feat: callout support
2025-10-27 23:20:27 +00:00
Jiaxin Peng
abb699fe4d docs: add support for converting Obsidian callouts to Notion callout blocks 2025-10-27 23:17:55 +00:00
Jiaxin Peng
cf6c80df0f update yarn.lock 2025-10-27 23:14:43 +00:00
Jiaxin Peng
cf884de554 refactor: remove redundant debug logs from upload commands and base class 2025-10-27 23:12:25 +00:00
Jiaxin Peng
8eb4694342 refactor: removing redundant debug logs 2025-10-27 22:57:28 +00:00
Jiaxin Peng
6c0f6cea30 feat: enhance logging and error handling in upload commands and base class 2025-10-27 22:34:46 +00:00
Jiaxin Peng
8af4816f87 chore: update martian dependency version to 1.3.1 2025-10-27 20:44:23 +00:00
Jiaxin Peng
f6d24daa52 chore: bump martian dependency version to 1.3.0 2025-10-27 13:40:42 +00:00
Jiaxin Peng
da72846903 Update version to 2.6.0 2025-10-06 23:25:29 +01:00
Jiaxin Peng
8e08c4325d Update version to 2.6.0 2025-10-06 23:22:00 +01:00
Jiaxin Peng
ebb7a290e8 Merge pull request #61 from jxpeng98/unify-upload-functions
unify three databases with single upload file
2025-10-06 23:19:55 +01:00
Jiaxin Peng
24e68fc798 unify three databases with single upload file 2025-10-06 23:18:01 +01:00
19 changed files with 1434 additions and 1938 deletions

4
.gitignore vendored
View File

@@ -24,3 +24,7 @@ data.json
# local data
local-data
# vitepress
doc/.vitepress/dist
doc/.vitepress/cache

View File

@@ -1,15 +1,6 @@
## Fix
- Fix the bug that the plugin does not work in the mobile app.
- 修复了在移动端应用中插件无法工作的问题。
## Feature
- Both desktop and mobile apps support syncing long markdown files.
- 桌面端和移动端应用都支持长篇 markdown 文件同步。
## Issue
- Bullet list over 3 levels is not supported.
- 无法同步超过三个分支的列表。
- Better debugging information for upload failures.
- Support synchronising Obsidian callouts as Notion callout blocks.
- 优化控制台输出的调试信息。
- 支持将 Obsidian Callout 同步为 Notion Callout 区块。

View File

@@ -30,8 +30,20 @@
- [x] Support custom properties for Notion General database. 支持自定义属性
- [x] Support preview for database details in plugin settings. 支持预览数据库详情
- [x] Support edit for database details in plugin settings. 支持编辑数据库详情
- [x] Convert Obsidian callouts into Notion callout blocks so your `[!info]` style notes stay consistent after syncing.
- [ ] Support group upload with one click 支持一键多数据库上传
## Callout support
Obsidian callouts written with the standard syntax (for example `> [!warning]` or `> [!quote]`) are automatically rendered as Notion callout blocks during upload. The plugin keeps the chosen callout type, title, and body content so your callout styling survives the trip from Obsidian to Notion without extra configuration.
```markdown
> [!info] Tips
> Remember to update your front matter before syncing.
```
After syncing, the note will contain an equivalent Notion callout block with the same icon and text.
## How to use
If you want to use this plugin, you need to follow the following steps to set up the plugin. The steps can be divided into two parts: setting up the Notion API and setting up the plugin in Obsidian.

View File

@@ -1,7 +1,7 @@
{
"id": "share-to-notionnext",
"name": "Share to NotionNext",
"version": "2.5.0",
"version": "2.7.0",
"minAppVersion": "0.0.1",
"description": "Shares obsidian md file to notion with notion api for NotionNext web deploy, originally created by EasyChris/obsidian-to-notion.",
"author": "EasyChris, jxpeng98",

View File

@@ -1,6 +1,6 @@
{
"name": "share-to-notionnext",
"version": "2.5.0",
"version": "2.7.0",
"type": "module",
"description": "Share files to any Notion database using the Notion API, originally created by EasyChris/obsidian-to-notion.",
"main": "main.js",
@@ -10,7 +10,7 @@
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"author": "Jiaxin PENG",
"license": "GNU GPLv3",
"devDependencies": {
"@types/node": "^20.5.7",
@@ -24,7 +24,7 @@
"typescript": "5.2.2"
},
"dependencies": {
"@jxpeng98/martian": "^1.2.7",
"@jxpeng98/martian": "^1.3.1",
"https-proxy-agent": "^7.0.2",
"remark-math": "^6.0.0",
"yaml": "^2.3.4",

View File

@@ -1,7 +1,5 @@
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from "obsidian";
import { addIcons } from 'src/ui/icon';
import { Upload2NotionGeneral } from "src/upload/upload_general/Upload2NotionGeneral";
import { Upload2NotionNext } from "src/upload/upload_next/Upload2NotionNext";
import { i18nConfig } from "src/lang/I18n";
import ribbonCommands from "src/commands/NotionCommands";
import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs";
@@ -86,4 +84,3 @@ export default class ObsidianSyncNotionPlugin extends Plugin {

628
src/upload/Upload2Notion.ts Normal file
View File

@@ -0,0 +1,628 @@
import { App, TFile } from "obsidian";
import { markdownToBlocks } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
import { LIMITS, paragraph } from "@jxpeng98/martian";
import MyPlugin from "src/main";
import { DatabaseDetails } from "../ui/settingTabs";
import { updateYamlInfo } from "./updateYaml";
import { UploadBase, NotionPageResponse } from "./common/UploadBase";
export type DatasetType = "general" | "next" | "custom";
interface BaseSyncRequest {
dataset: DatasetType;
markdown: string;
nowFile: TFile;
app: App;
}
interface GeneralSyncRequest extends BaseSyncRequest {
dataset: "general";
title: string;
cover: string;
tags: string[];
}
interface NextSyncRequest extends BaseSyncRequest {
dataset: "next";
title: string;
emoji: string;
cover: string;
tags: string[];
type: string;
slug: string;
stats: string;
category: string;
summary: string;
password: string;
favicon: string;
datetime: string;
}
interface CustomSyncRequest extends BaseSyncRequest {
dataset: "custom";
cover: string;
customValues: Record<string, string>;
}
export type SyncRequest =
| GeneralSyncRequest
| NextSyncRequest
| CustomSyncRequest;
interface GeneralParams {
title: string;
cover?: string;
tags: string[];
childArr: any[];
notionId?: string;
}
interface NextParams {
title: string;
emoji: string;
cover?: string;
tags: string[];
type: string;
slug: string;
stats: string;
category: string;
summary: string;
password: string;
favicon: string;
datetime: string;
childArr: any[];
notionId?: string;
}
interface CustomParams {
cover?: string;
customValues: Record<string, string>;
childArr: any[];
notionId?: string;
}
export class Upload2Notion extends UploadBase {
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
}
async sync(request: SyncRequest): Promise<NotionPageResponse> {
const startedAt = Date.now();
let response: NotionPageResponse;
switch (request.dataset) {
case "general":
response = await this.handleGeneral(request);
break;
case "next":
response = await this.handleNext(request);
break;
case "custom":
response = await this.handleCustom(request);
break;
default:
throw new Error(`Unsupported dataset type: ${(request as any).dataset}`);
}
if (response.response && response.response.status === 200) {
await updateYamlInfo(
request.markdown,
request.nowFile,
response.data,
request.app,
this.plugin,
this.dbDetails,
);
} else {
console.log(`[Upload2Notion] Sync failed`, response.data);
}
return response;
}
private async handleGeneral(request: GeneralSyncRequest): Promise<NotionPageResponse> {
console.log(`[Upload2Notion] Handling general dataset`, {
cover: request.cover,
tags: request.tags,
});
const blocks = this.buildBlocks(request.markdown, {
notionLimits: {truncate: false},
});
const notionId = this.getNotionId(request.app, request.nowFile);
this.debugLog("Upload2Notion", "General dataset payload prepared", {
blockCount: blocks.length,
notionId: notionId ?? null,
tagCount: request.tags?.length ?? 0,
});
return this.upsertGeneral({
title: request.title,
cover: request.cover,
tags: request.tags || [],
childArr: blocks,
notionId,
});
}
private async handleNext(request: NextSyncRequest): Promise<NotionPageResponse> {
console.log(`[Upload2Notion] Handling next dataset`, {
type: request.type,
slug: request.slug,
category: request.category,
});
const blocks = this.buildBlocks(request.markdown, {
notionLimits: {truncate: false},
});
this.splitRichTextParagraphs(blocks);
const notionId = this.getNotionId(request.app, request.nowFile);
this.debugLog("Upload2Notion", "Next dataset payload prepared", {
blockCount: blocks.length,
notionId: notionId ?? null,
hasEmoji: !!request.emoji,
tags: request.tags || [],
});
return this.upsertNext({
title: request.title,
emoji: request.emoji,
cover: request.cover,
tags: request.tags || [],
type: request.type,
slug: request.slug,
stats: request.stats,
category: request.category,
summary: request.summary,
password: request.password,
favicon: request.favicon,
datetime: request.datetime,
childArr: blocks,
notionId,
});
}
private async handleCustom(request: CustomSyncRequest): Promise<NotionPageResponse> {
console.log(`[Upload2Notion] Handling custom dataset`, {
customKeys: Object.keys(request.customValues || {}),
});
const blocks = this.buildBlocks(request.markdown, {
strictImageUrls: true,
notionLimits: {truncate: false},
});
const notionId = this.getNotionId(request.app, request.nowFile);
this.debugLog("Upload2Notion", "Custom dataset payload prepared", {
blockCount: blocks.length,
notionId: notionId ?? null,
customValueKeys: Object.keys(request.customValues || {}),
});
return this.upsertCustom({
cover: request.cover,
customValues: request.customValues,
childArr: blocks,
notionId,
});
}
private buildBlocks(markdown: string, options: Record<string, unknown>): any[] {
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const content = yamlContent.__content;
const blocks = markdownToBlocks(content, options);
this.debugLog("Upload2Notion", "Converted markdown to blocks", {
blockCount: blocks.length,
firstBlockTypes: blocks.slice(0, 5).map((block: any) => block?.type),
options,
});
return blocks;
}
private getNotionId(app: App, nowFile: TFile): string | undefined {
const frontMatter = app.metadataCache.getFileCache(nowFile)?.frontmatter;
if (!frontMatter) {
this.debugLog("Upload2Notion", "No frontmatter found when resolving Notion ID", {
filePath: nowFile.path,
});
return undefined;
}
const {abName} = this.dbDetails;
const notionIDKey = `NotionID-${abName}`;
const notionId = frontMatter[notionIDKey];
return notionId ? String(notionId) : undefined;
}
private splitRichTextParagraphs(blocks: any[]): void {
blocks.forEach((block, index) => {
if (
block.type === "paragraph" &&
block.paragraph.rich_text.length > LIMITS.RICH_TEXT_ARRAYS
) {
console.log(`[Upload2Notion] Splitting rich text paragraph`, {
index,
length: block.paragraph.rich_text.length,
});
const paragraphChunks = this.chunkArray<any>(block.paragraph.rich_text, 100);
const newParagraphBlocks = paragraphChunks.map((chunk) => paragraph(chunk));
blocks.splice(index, 1, ...newParagraphBlocks);
}
});
}
private chunkArray<T>(items: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < items.length; i += size) {
result.push(items.slice(i, i + size));
}
return result;
}
private async upsertGeneral(params: GeneralParams): Promise<NotionPageResponse> {
const {firstChunk, extraChunks} = this.prepareBlocks(params.childArr);
const cover = params.notionId
? await this.resolveCoverForUpdate(params.cover)
: params.cover;
this.debugLog("Upload2Notion", "Upserting general page", {
title: params.title,
blockCount: params.childArr.length,
firstChunkSize: firstChunk.length,
extraChunkCount: extraChunks.length,
cover: cover ?? null,
notionIdExisting: params.notionId ?? null,
tagList: params.tags,
});
if (params.notionId) {
console.log(`[Upload2Notion] Deleting existing Notion page`, {
notionId: params.notionId,
});
await this.deletePage(params.notionId);
}
const body = this.buildGeneralBody({
title: params.title,
tags: params.tags,
firstChunk,
});
this.applyCover(body, cover);
console.log(body);
return this.submitPage(body, extraChunks);
}
private async upsertNext(params: NextParams): Promise<NotionPageResponse> {
const {firstChunk, extraChunks} = this.prepareBlocks(params.childArr);
const cover = params.notionId
? await this.resolveCoverForUpdate(params.cover)
: params.cover;
this.debugLog("Upload2Notion", "Upserting NotionNext page", {
title: params.title,
type: params.type,
slug: params.slug,
blockCount: params.childArr.length,
firstChunkSize: firstChunk.length,
extraChunkCount: extraChunks.length,
cover: cover ?? null,
hasEmoji: !!params.emoji,
notionIdExisting: params.notionId ?? null,
});
if (params.notionId) {
await this.deletePage(params.notionId);
}
const body = this.buildNextBody({
firstChunk,
title: params.title,
emoji: params.emoji,
tags: params.tags,
type: params.type,
slug: params.slug,
stats: params.stats,
category: params.category,
summary: params.summary,
password: params.password,
favicon: params.favicon,
datetime: params.datetime,
});
this.applyCover(body, cover);
console.log(body);
return this.submitPage(body, extraChunks);
}
private async upsertCustom(params: CustomParams): Promise<NotionPageResponse> {
const {firstChunk, extraChunks} = this.prepareBlocks(params.childArr);
const cover = params.notionId
? await this.resolveCoverForUpdate(params.cover)
: params.cover;
this.debugLog("Upload2Notion", "Upserting custom page", {
blockCount: params.childArr.length,
firstChunkSize: firstChunk.length,
extraChunkCount: extraChunks.length,
cover: cover ?? null,
notionIdExisting: params.notionId ?? null,
customValueKeys: Object.keys(params.customValues || {}),
});
if (params.notionId) {
await this.deletePage(params.notionId);
}
const body = this.buildCustomBody({
customValues: params.customValues,
firstChunk,
});
this.applyCover(body, cover);
console.log(body);
return this.submitPage(body, extraChunks);
}
private buildGeneralBody(params: {title: string; tags: string[]; firstChunk: any[]}): any {
const {
databaseID,
customTitleButton,
customTitleName,
tagButton,
} = this.dbDetails;
return {
parent: {
database_id: databaseID,
},
properties: {
[customTitleButton ? customTitleName : "title"]: {
title: [
{
text: {
content: params.title,
},
},
],
},
...(tagButton
? {
tags: {
multi_select:
params.tags && params.tags.length > 0
? params.tags.map((tag) => ({name: tag}))
: [],
},
}
: {}),
},
children: params.firstChunk,
};
}
private buildNextBody(params: {
firstChunk: any[];
title: string;
emoji: string;
tags: string[];
type: string;
slug: string;
stats: string;
category: string;
summary: string;
password: string;
favicon: string;
datetime: string;
}): any {
const {databaseID} = this.dbDetails;
const pageProperties: any = {
parent: {
database_id: databaseID,
},
properties: {
title: {
title: [
{
text: {
content: params.title,
},
},
],
},
type: {
select: {
name: params.type || "Post",
},
},
status: {
select: {
name: params.stats || "Draft",
},
},
category: {
select: {
name: params.category || "Obsidian",
},
},
password: {
rich_text: [
{
text: {
content: params.password || "",
},
},
],
},
icon: {
rich_text: [
{
text: {
content: params.favicon || "",
},
},
],
},
date: {
date: {
start: params.datetime || new Date().toISOString(),
},
},
},
};
if (params.tags && params.tags.length > 0) {
pageProperties.properties.tags = {
multi_select: params.tags.map((tag) => ({name: tag})),
};
}
if (params.emoji) {
pageProperties.icon = {
emoji: params.emoji,
};
}
if (params.slug) {
pageProperties.properties.slug = {
rich_text: [
{
text: {
content: params.slug,
},
},
],
};
}
if (params.summary) {
pageProperties.properties.summary = {
rich_text: [
{
text: {
content: params.summary,
},
},
],
};
}
return {
...pageProperties,
children: params.firstChunk,
};
}
private buildCustomBody(params: {
customValues: Record<string, string>;
firstChunk: any[];
}): any {
const {customProperties, databaseID} = this.dbDetails;
const properties: { [key: string]: any } = {};
if (customProperties) {
customProperties.forEach(({customName, customType}) => {
if (params.customValues[customName] !== undefined) {
const propertyValue = this.buildCustomProperty(customName, customType, params.customValues);
if (propertyValue !== undefined) {
properties[customName] = propertyValue;
}
}
});
}
return {
parent: {
database_id: databaseID,
},
properties,
children: params.firstChunk,
};
}
private buildCustomProperty(customName: string, customType: string, customValues: Record<string, any>) {
const value = customValues[customName] || "";
switch (customType) {
case "title":
return {
title: [
{
text: {
content: value,
},
},
],
};
case "rich_text":
return {
rich_text: [
{
text: {
content: value || "",
},
},
],
};
case "date":
return {
date: {
start: value || new Date().toISOString(),
},
};
case "number":
return {
number: Number(value),
};
case "phone_number":
return {
phone_number: value,
};
case "email":
return {
email: value,
};
case "url":
return {
url: value,
};
case "files":
return {
files: Array.isArray(value)
? value.map((url: string) => ({
name: url,
type: "external",
external: {
url,
},
}))
: [
{
name: value,
type: "external",
external: {
url: value,
},
},
],
};
case "checkbox":
return {
checkbox: Boolean(value) || false,
};
case "select":
return {
select: {
name: value,
},
};
case "multi_select":
return {
multi_select: Array.isArray(value)
? value.map((item: string) => ({name: item}))
: [{name: value}],
};
case "relation":
return {
relation: Array.isArray(value)
? value.map((item: string) => ({id: item}))
: [{id: value}],
};
default:
return undefined;
}
}
}

View File

@@ -0,0 +1,248 @@
import { Notice, requestUrl } from "obsidian";
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
import { i18nConfig } from "../../lang/I18n";
export interface NotionPageResponse {
response: any;
data: any;
}
interface PreparedBlocks {
firstChunk: any[];
extraChunks: any[][];
}
export abstract class UploadBase {
protected plugin: MyPlugin;
protected dbDetails: DatabaseDetails;
protected constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails;
}
async deletePage(notionID: string) {
const {notionAPI} = this.dbDetails;
return requestUrl({
url: `https://api.notion.com/v1/blocks/${notionID}`,
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: "",
throw: false,
}).catch((error) =>
this.handleRequestError(error, `Deleting Notion page ${notionID}`),
);
}
protected prepareBlocks(childArr: any[]): PreparedBlocks {
this.stripCodeAnnotations(childArr);
const childArrLength = childArr.length;
if (childArrLength <= 100) {
this.debugLog("UploadBase", "Blocks fit into a single request", {
totalBlocks: childArrLength,
});
return {
firstChunk: childArr,
extraChunks: [],
};
}
const extraChunks: any[][] = [];
for (let i = 100; i < childArr.length; i += 100) {
extraChunks.push(childArr.slice(i, i + 100));
}
this.debugLog("UploadBase", "Blocks split into multiple chunks", {
totalBlocks: childArrLength,
firstChunkSize: 100,
extraChunkCount: extraChunks.length,
lastChunkSize: extraChunks[extraChunks.length - 1].length,
});
return {
firstChunk: childArr.slice(0, 100),
extraChunks,
};
}
protected applyCover(body: any, cover?: string) {
if (cover) {
body.cover = {
type: "external",
external: {
url: cover,
},
};
} else if (!body.cover && this.plugin.settings.bannerUrl) {
body.cover = {
type: "external",
external: {
url: this.plugin.settings.bannerUrl,
},
};
}
this.debugLog("UploadBase", "Cover applied to payload", {
chosenCover: body.cover?.external?.url ?? null,
defaultBannerUsed: !cover && !!this.plugin.settings.bannerUrl,
});
}
protected async resolveCoverForUpdate(cover?: string): Promise<string | undefined> {
if (cover) {
this.debugLog("UploadBase", "Existing cover retained for update", {
cover,
});
return cover;
}
const databaseCover = await this.fetchDatabaseCover();
return databaseCover ?? undefined;
}
protected async submitPage(body: any, extraChunks: any[][]): Promise<NotionPageResponse> {
const {notionAPI} = this.dbDetails;
const startedAt = Date.now();
const response = await requestUrl({
url: `https://api.notion.com/v1/pages`,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(body),
throw: false,
}).catch((error) =>
this.handleRequestError(error, "Creating Notion page"),
);
const data = await response.json;
this.debugLog("UploadBase", "Page creation response received", {
status: response.status,
durationMs: Date.now() - startedAt,
notionUrl: data?.url ?? null,
pageId: data?.id ?? null,
});
if (response.status !== 200) {
new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
if (extraChunks.length > 0) {
await this.appendExtraBlocks(data.id, extraChunks);
}
}
return {response, data};
}
private stripCodeAnnotations(childArr: any[]) {
childArr.forEach((block: any) => {
if (block.type === "code") {
block.code.rich_text.forEach((item: any) => {
if (item.type === "text" && item.annotations) {
delete item.annotations;
}
});
}
});
}
private async appendExtraBlocks(pageId: string, extraChunks: any[][]) {
const {notionAPI} = this.dbDetails;
for (let i = 0; i < extraChunks.length; i++) {
const chunk = extraChunks[i];
const extraBlocks = {
children: chunk,
};
const extraResponse = await requestUrl({
url: `https://api.notion.com/v1/blocks/${pageId}/children`,
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(extraBlocks),
throw: false,
}).catch((error) =>
this.handleRequestError(error, `Appending blocks to page ${pageId}`),
);
const extraData: any = await extraResponse.json;
if (extraResponse.status !== 200) {
new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${extraData.message}`);
} else {
console.log(`${i18nConfig["ExtraBlockUploaded"]} to page: ${pageId}`);
if (i === extraChunks.length - 1) {
console.log(`${i18nConfig["BlockUploaded"]} to page: ${pageId}`);
new Notice(`${i18nConfig["BlockUploaded"]} page: ${pageId}`, 5000);
}
}
}
}
private async fetchDatabaseCover(): Promise<string | null> {
const {notionAPI, databaseID} = this.dbDetails;
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: "GET",
headers: {
Authorization: "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
throw: false,
}).catch((error) =>
this.handleRequestError(error, `Fetching database ${databaseID}`),
);
if (response.json.cover && response.json.cover.external) {
return response.json.cover.external.url;
}
return null;
}
protected debugLog(context: string, message: string, payload?: Record<string, unknown>): void {
if (payload) {
console.log(`[${context}] ${message}`, payload);
} else {
console.log(`[${context}] ${message}`);
}
}
protected maskValue(value?: string, visibleChars = 4): string | undefined {
if (!value) {
return value;
}
if (value.length <= visibleChars * 2) {
return `${value.slice(0, visibleChars)}***`;
}
return `${value.slice(0, visibleChars)}***${value.slice(-visibleChars)}`;
}
private handleRequestError(error: unknown, context: string): never {
const message = error instanceof Error && error.message
? error.message
: String(error);
console.error(`[UploadBase] ${context} failed`, {
message,
error,
});
throw new Error(
`${context} failed: ${message}. Please check your network connection or proxy settings.`,
);
}
}

View File

@@ -1,6 +1,6 @@
import { App, Notice } from "obsidian";
import { i18nConfig } from "../../lang/I18n";
import { DatabaseDetails } from "../../ui/settingTabs";
import {App, Notice} from "obsidian";
import {i18nConfig} from "../../lang/I18n";
import {DatabaseDetails} from "../../ui/settingTabs";
export async function getNowFileMarkdownContentCustom(
app: App,
@@ -24,6 +24,7 @@ export async function getNowFileMarkdownContentCustom(
.map(property => property.customName);
// Extract custom values from the front matter based on the names
// Only collect data 'Relation' should be handled separately in the function buildBodyString
customPropertyNames.forEach(propertyName => {
if (FileCache.frontmatter && FileCache.frontmatter[propertyName] !== undefined) {
customValues[propertyName] = FileCache.frontmatter[propertyName];
@@ -31,7 +32,8 @@ export async function getNowFileMarkdownContentCustom(
});
// Check if any of the customProperties has a customType of 'title'
const titleProperty = dbDetails.customProperties.find(property => property.customType === 'title');
const titleProperty = dbDetails.customProperties
.find(property => property.customType === 'title');
// If a 'title' type property exists, use the file's basename as its value
if (titleProperty) {

View File

@@ -1,13 +1,38 @@
import {i18nConfig} from "../lang/I18n";
import {App, Notice} from "obsidian";
import {Upload2NotionNext} from "./upload_next/Upload2NotionNext";
import {Upload2NotionGeneral} from "./upload_general/Upload2NotionGeneral";
import {Upload2NotionCustom} from "./upoload_custom/Upload2NotionCustom";
import {Upload2Notion} from "./Upload2Notion";
import type {NotionPageResponse} from "./common/UploadBase";
import {DatabaseDetails, PluginSettings} from "../ui/settingTabs";
import ObsidianSyncNotionPlugin from "../main";
import {getNowFileMarkdownContentNext} from "./upload_next/getMarkdownNext";
import {getNowFileMarkdownContentGeneral} from "./upload_general/getMarkdownGeneral";
import {getNowFileMarkdownContentCustom} from "./upoload_custom/getMarkdownCustom";
import {getNowFileMarkdownContentNext} from "./common/getMarkdownNext";
import {getNowFileMarkdownContentGeneral} from "./common/getMarkdownGeneral";
import {getNowFileMarkdownContentCustom} from "./common/getMarkdownCustom";
const SYNC_ERROR_NOTICE_DURATION = 8000;
function extractErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return String(error);
}
function notifySyncError(prefix: string, basename: string, error: unknown): void {
const errorMessage = extractErrorMessage(error);
console.error(`${prefix} Sync failed`, error);
new Notice(
`${i18nConfig["sync-fail"]} ${basename}. ${errorMessage}\n${i18nConfig["CheckConsole"]}`,
SYNC_ERROR_NOTICE_DURATION,
);
}
function logCommandDebug(context: string, message: string, payload?: Record<string, unknown>): void {
if (payload) {
console.log(`[${context}] ${message}`, payload);
} else {
console.log(`[${context}] ${message}`);
}
}
export async function uploadCommandNext(
plugin: ObsidianSyncNotionPlugin,
@@ -17,6 +42,7 @@ export async function uploadCommandNext(
) {
const {notionAPI, databaseID} = dbDetails;
console.log(`[uploadCommandNext] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path);
// Check if the user has set up the Notion API and database ID
if (notionAPI === "" || databaseID === "") {
@@ -39,21 +65,76 @@ export async function uploadCommandNext(
paword,
favicon,
datetime
} = await getNowFileMarkdownContentNext(app, settings)
} = await getNowFileMarkdownContentNext(app, settings);
logCommandDebug("uploadCommandNext", "Metadata extracted from markdown", {
hasMarkdown: !!markDownData,
markdownLength: markDownData?.length ?? 0,
tagCount: tags?.length ?? 0,
coverIncluded: !!cover,
hasEmoji: !!emoji,
type,
slug,
hasPassword: !!paword,
datetime,
});
if (markDownData) {
const {basename} = nowFile;
logCommandDebug("uploadCommandNext", "Preparing to upload", {
filename: basename,
filePath: nowFile.path,
});
const upload = new Upload2NotionNext(plugin, dbDetails);
const res = await upload.syncMarkdownToNotionNext(basename, emoji, cover, tags, type, slug, stats, category, summary, paword, favicon, datetime, markDownData, nowFile, this.app);
const upload = new Upload2Notion(plugin, dbDetails);
let res: NotionPageResponse;
try {
res = await upload.sync({
dataset: "next",
title: basename,
emoji: emoji || "",
cover: cover || "",
tags: tags || [],
type: type || "",
slug: slug || "",
stats: stats || "",
category: category || "",
summary: summary || "",
password: paword || "",
favicon: favicon || "",
datetime: datetime || "",
markdown: markDownData,
nowFile,
app,
});
} catch (error: unknown) {
notifySyncError("[uploadCommandNext]", basename, error);
logCommandDebug("uploadCommandNext", "Sync threw error", {
filename: basename,
error: extractErrorMessage(error),
});
return;
}
const {response} = res;
if (response.status === 200) {
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green";
console.log(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`);
logCommandDebug("uploadCommandNext", "Sync succeeded", {
filename: basename,
status: response.status,
pageId: res.data?.id ?? null,
pageUrl: res.data?.url ?? null,
});
} else {
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
console.log(`${i18nConfig["sync-fail"]} ${basename}`);
logCommandDebug("uploadCommandNext", "Sync failed with status", {
filename: basename,
status: response.status,
errorCode: res.data?.code ?? null,
errorStatus: res.data?.status ?? null,
});
}
}
@@ -68,6 +149,7 @@ export async function uploadCommandGeneral(
) {
const {notionAPI, databaseID} = dbDetails;
console.log(`[uploadCommandGeneral] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path);
// Check if the user has set up the Notion API and database ID
if (notionAPI === "" || databaseID === "") {
@@ -83,17 +165,53 @@ export async function uploadCommandGeneral(
if (markDownData) {
const {basename} = nowFile;
logCommandDebug("uploadCommandGeneral", "Preparing to upload", {
filename: basename,
markdownLength: markDownData.length,
tagCount: tags?.length ?? 0,
hasCover: !!cover,
});
const upload = new Upload2NotionGeneral(plugin, dbDetails);
const res = await upload.syncMarkdownToNotionGeneral(basename, cover, tags, markDownData, nowFile, this.app);
const upload = new Upload2Notion(plugin, dbDetails);
let res: NotionPageResponse;
try {
res = await upload.sync({
dataset: "general",
title: basename,
cover: cover || "",
tags: tags || [],
markdown: markDownData,
nowFile,
app,
});
} catch (error: unknown) {
notifySyncError("[uploadCommandGeneral]", basename, error);
logCommandDebug("uploadCommandGeneral", "Sync threw error", {
filename: basename,
error: extractErrorMessage(error),
});
return;
}
const {response} = res;
if (response.status === 200) {
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green";
console.log(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`);
logCommandDebug("uploadCommandGeneral", "Sync succeeded", {
filename: basename,
status: response.status,
pageId: res.data?.id ?? null,
pageUrl: res.data?.url ?? null,
});
} else {
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
console.log(`${i18nConfig["sync-fail"]} ${basename}`);
logCommandDebug("uploadCommandGeneral", "Sync failed with status", {
filename: basename,
status: response.status,
errorCode: res.data?.code ?? null,
errorStatus: res.data?.status ?? null,
});
}
}
@@ -107,7 +225,8 @@ export async function uploadCommandCustom(
app: App,
) {
const {notionAPI, databaseID} = settings;
const {notionAPI, databaseID} = dbDetails;
console.log(`[uploadCommandCustom] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path);
// Check if the user has set up the Notion API and database ID
if (notionAPI === "" || databaseID === "") {
@@ -123,18 +242,53 @@ export async function uploadCommandCustom(
if (markDownData) {
const {basename} = nowFile;
logCommandDebug("uploadCommandCustom", "Preparing to upload", {
filename: basename,
markdownLength: markDownData.length,
hasCover: !!cover,
customValueKeys: Object.keys(customValues || {}),
});
const upload = new Upload2NotionCustom(plugin, dbDetails);
const res = await upload.syncMarkdownToNotionCustom(cover, customValues, markDownData, nowFile, this.app);
const upload = new Upload2Notion(plugin, dbDetails);
let res: NotionPageResponse;
try {
res = await upload.sync({
dataset: "custom",
cover: cover || "",
customValues,
markdown: markDownData,
nowFile,
app,
});
} catch (error: unknown) {
notifySyncError("[uploadCommandCustom]", basename, error);
logCommandDebug("uploadCommandCustom", "Sync threw error", {
filename: basename,
error: extractErrorMessage(error),
});
return;
}
const {response} = res;
if (response.status === 200) {
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green";
console.log(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`);
logCommandDebug("uploadCommandCustom", "Sync succeeded", {
filename: basename,
status: response.status,
pageId: res.data?.id ?? null,
pageUrl: res.data?.url ?? null,
});
} else {
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
console.log(`${i18nConfig["sync-fail"]} ${basename}`);
logCommandDebug("uploadCommandCustom", "Sync failed with status", {
filename: basename,
status: response.status,
errorCode: res.data?.code ?? null,
errorStatus: res.data?.status ?? null,
});
}
}

View File

@@ -1,96 +0,0 @@
import { App, Notice, requestUrl, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
export class UploadBaseGeneral {
plugin: MyPlugin;
notion: Client;
agent: any;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails
}
async deletePage(notionID: string) {
const { notionAPI } = this.dbDetails
return requestUrl({
url: `https://api.notion.com/v1/blocks/${notionID}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + notionAPI,
'Notion-Version': '2022-06-28',
},
body: ''
});
}
async getDataBase(databaseID: string) {
const { notionAPI } = this.dbDetails
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + notionAPI,
'Notion-Version': '2022-06-28',
}
}
)
// Check if cover is present in the JSON response and then get the URL
if (response.json.cover && response.json.cover.external) {
return response.json.cover.external.url;
} else {
return null; // or some other default value, if you prefer
}
}
// async updateYamlInfo(yamlContent: string, nowFile: TFile, res: any, app: App, settings: any) {
// let {url, id} = res.json
// // replace www to notionID
// const {notionUser} = this.plugin.settings;
//
// if (notionUser !== "") {
// // replace url str "www" to notionID
// url = url.replace("www.notion.so", `${notionUser}.notion.site`)
// }
//
// await app.fileManager.processFrontMatter(nowFile, yamlContent => {
// if (yamlContent['notionID']) {
// delete yamlContent['notionID']
// }
// if (yamlContent['link']) {
// delete yamlContent['link']
// }
// // add new notionID and link
// yamlContent.notionID = id;
// yamlContent.link = url;
// });
//
// try {
// await navigator.clipboard.writeText(url)
// } catch (error) {
// new Notice(`复制链接失败,请手动复制${error}`)
// }
// // const __content = yamlContent.__content;
// // delete yamlContent.__content
// // const yamlhead = yaml.stringify(yamlContent)
// // // if yamlhead hava last \n remove it
// // const yamlhead_remove_n = yamlhead.replace(/\n$/, '')
// // // if __content have start \n remove it
// // const __content_remove_n = __content.replace(/^\n/, '')
// // const content = '---\n' +yamlhead_remove_n +'\n---\n' + __content_remove_n;
// // try {
// // await nowFile.vault.modify(nowFile, content)
// // } catch (error) {
// // new Notice(`write file error ${error}`)
// // }
// }
}

View File

@@ -1,269 +0,0 @@
import {App, Notice, TFile, requestUrl} from "obsidian";
import {markdownToBlocks} from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
import MyPlugin from "src/main";
import {DatabaseDetails, PluginSettings} from "../../ui/settingTabs";
import {UploadBaseGeneral} from "./BaseUpload2NotionGeneral";
import {updateYamlInfo} from "../updateYaml";
import {i18nConfig} from "../../lang/I18n";
interface CreatePageResponse {
response: any;
data: any;
}
export class Upload2NotionGeneral extends UploadBaseGeneral {
settings: PluginSettings;
dbDetails: DatabaseDetails;
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
this.dbDetails = dbDetails;
}
// 因为需要解析notion的block进行对比非常的麻烦
// 暂时就直接删除新建一个page
async updatePage(
notionID: string,
title: string,
cover: string,
tags: string[],
childArr: any,
) {
await this.deletePage(notionID);
const {databaseID} = this.dbDetails;
const databaseCover = await this.getDataBase(
databaseID,
);
if (cover == null) {
cover = databaseCover;
}
return await this.createPage(title, cover, tags, childArr);
}
async createPage(
title: string,
cover: string,
tags: string[],
childArr: any,
): Promise<CreatePageResponse> {
const {
databaseID,
customTitleButton,
customTitleName,
tagButton,
notionAPI
} = this.dbDetails;
// remove the annotations from the childArr if type is code block
childArr.forEach((block: any) => {
if (block.type === "code") {
block.code.rich_text.forEach((item: any) => {
if (item.type === "text" && item.annotations) {
delete item.annotations;
}
}
);
}
}
);
// check the length of the childArr and split it into chunks of 100
const childArrLength = childArr.length;
let extraArr: any[] = [];
let firstArr: any;
let pushCount = 0;
console.log(`Page includes ${childArrLength} blocks`)
if (childArrLength > 100) {
for (let i = 0; i < childArr.length; i += 100) {
if (i == 0) {
firstArr = childArr.slice(0, 100);
} else {
const chunk = childArr.slice(i, i + 100);
extraArr.push(chunk);
pushCount++;
}
}
} else {
firstArr = childArr;
}
const bodyString: any = {
parent: {
database_id: databaseID,
},
properties: {
[customTitleButton
? customTitleName
: "title"]: {
title: [
{
text: {
content: title,
},
},
],
},
...(tagButton
? {
tags: {
multi_select: tags && true ? tags.map((tag) => ({name: tag})) : [],
},
}
: {}),
},
children: firstArr,
};
if (cover) {
bodyString.cover = {
type: "external",
external: {
url: cover,
},
};
}
if (!bodyString.cover && this.plugin.settings.bannerUrl) {
bodyString.cover = {
type: "external",
external: {
url: this.plugin.settings.bannerUrl,
},
};
}
console.log(bodyString)
let response: any;
let data: any;
response = await requestUrl({
url: `https://api.notion.com/v1/pages`,
method: "POST",
headers: {
"Content-Type": "application/json",
// 'User-Agent': 'obsidian.md',
Authorization:
"Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(bodyString),
throw: false
});
data = await response.json;
// console.log(data)
// console.log(response.status)
if (response.status !== 200) {
new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
console.log(`Page created: ${data.url}`);
console.log(`Page ID: ${data.id}`);
}
//
// upload the rest of the blocks
if (pushCount > 0) {
for (let i = 0; i < pushCount; i++) {
const extraBlocks = {
children: extraArr[i],
};
console.log(extraBlocks)
const extraResponse = await requestUrl({
url: `https://api.notion.com/v1/blocks/${data.id}/children`,
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(extraBlocks),
});
const extraData: any = await extraResponse.json;
if (extraResponse.status !== 200) {
new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${extraData.message}`);
} else {
console.log(`${i18nConfig["ExtraBlockUploaded"]} to page: ${data.id}`);
if (i === pushCount - 1) {
console.log(`${i18nConfig["BlockUploaded"]} to page: ${data.id}`);
new Notice(`${i18nConfig["BlockUploaded"]} page: ${data.id}`, 5000);
}
}
}
}
return {
response,
data
}
}
async syncMarkdownToNotionGeneral(
title: string,
cover: string,
tags: string[],
markdown: string,
nowFile: TFile,
app: App,
): Promise<any> {
const options = {
notionLimits: {
truncate: false,
}
}
let res: any;
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const __content = yamlContent.__content;
const file2Block = markdownToBlocks(__content, options);
const frontMatter =
app.metadataCache.getFileCache(nowFile)?.frontmatter;
const {abName} = this.dbDetails
const notionIDKey = `NotionID-${abName}`;
const notionID = frontMatter ? frontMatter[notionIDKey] : null;
if (notionID) {
res = await this.updatePage(
notionID,
title,
cover,
tags,
file2Block,
);
} else {
res = await this.createPage(title, cover, tags, file2Block);
}
let {response, data} = res;
// console.log(response)
if (response && response.status === 200) {
await updateYamlInfo(
markdown,
nowFile,
data,
app,
this.plugin,
this.dbDetails,
);
}
return res;
}
}

View File

@@ -1,96 +0,0 @@
import { App, Notice, requestUrl, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
export class UploadBaseNext {
plugin: MyPlugin;
notion: Client;
agent: any;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails
}
async deletePage(notionID: string) {
const { notionAPI } = this.dbDetails
return requestUrl({
url: `https://api.notion.com/v1/blocks/${notionID}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + notionAPI,
'Notion-Version': '2022-06-28',
},
body: ''
});
}
async getDataBase(databaseID: string) {
const { notionAPI } = this.dbDetails
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + notionAPI,
'Notion-Version': '2022-06-28',
}
}
)
// Check if cover is present in the JSON response and then get the URL
if (response.json.cover && response.json.cover.external) {
return response.json.cover.external.url;
} else {
return null; // or some other default value, if you prefer
}
}
// async updateYamlInfo(yamlContent: string, nowFile: TFile, res: any, app: App, settings: any) {
// let {url, id} = res.json
// // replace www to notionID
// const { notionUser} = this.plugin.settings;
//
// if (notionUser !== "") {
// // replace url str "www" to notionID
// url = url.replace("www.notion.so", `${notionUser}.notion.site`)
// }
//
// await app.fileManager.processFrontMatter(nowFile, yamlContent => {
// if (yamlContent['notionID']) {
// delete yamlContent['notionID']
// }
// if (yamlContent['link']) {
// delete yamlContent['link']
// }
// // add new notionID and link
// yamlContent.notionID = id;
// yamlContent.link = url;
// });
//
// try {
// await navigator.clipboard.writeText(url)
// } catch (error) {
// new Notice(`复制链接失败,请手动复制${error}`)
// }
// // const __content = yamlContent.__content;
// // delete yamlContent.__content
// // const yamlhead = yaml.stringify(yamlContent)
// // // if yamlhead hava last \n remove it
// // const yamlhead_remove_n = yamlhead.replace(/\n$/, '')
// // // if __content have start \n remove it
// // const __content_remove_n = __content.replace(/^\n/, '')
// // const content = '---\n' +yamlhead_remove_n +'\n---\n' + __content_remove_n;
// // try {
// // await nowFile.vault.modify(nowFile, content)
// // } catch (error) {
// // new Notice(`write file error ${error}`)
// // }
// }
}

View File

@@ -1,420 +0,0 @@
import {UploadBaseNext} from "./BaseUpload2NotionNext";
import {App, Notice, TFile, requestUrl} from "obsidian";
import {markdownToBlocks} from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
import MyPlugin from "src/main";
import {DatabaseDetails, PluginSettings} from "../../ui/settingTabs";
import {updateYamlInfo} from "../updateYaml";
import {LIMITS, paragraph} from "@jxpeng98/martian/src/notion";
import {i18nConfig} from "../../lang/I18n";
interface CreatePageResponse {
response: any;
data: any;
}
export class Upload2NotionNext extends UploadBaseNext {
settings: PluginSettings;
dbDetails: DatabaseDetails;
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
this.dbDetails = dbDetails;
}
// 因为需要解析notion的block进行对比非常的麻烦
// 暂时就直接删除新建一个page
async updatePage(
notionID: string,
title: string,
emoji: string,
cover: string,
tags: string[],
type: string,
slug: string,
stats: string,
category: string,
summary: string,
paword: string,
favicon: string,
datetime: string,
childArr: any,
) {
await this.deletePage(notionID);
const {databaseID} = this.dbDetails;
const databaseCover = await this.getDataBase(databaseID);
if (cover == null) {
cover = databaseCover;
}
return await this.createPage(
title,
emoji,
cover,
tags,
type,
slug,
stats,
category,
summary,
paword,
favicon,
datetime,
childArr,
);
}
async createPage(
title: string,
emoji: string,
cover: string,
tags: string[],
type: string,
slug: string,
stats: string,
category: string,
summary: string,
pawrod: string,
favicon: string,
datetime: string,
childArr: any,
): Promise<CreatePageResponse> {
const {databaseID, notionAPI} = this.dbDetails;
// remove the annotations from the childArr if type is code block
childArr.forEach((block: any) => {
if (block.type === "code") {
block.code.rich_text.forEach((item: any) => {
if (item.type === "text" && item.annotations) {
delete item.annotations;
}
}
);
}
}
);
// check the length of the childArr
// if it is too long, split it into multiple pages
const childArrLength = childArr.length;
let extraArr: any[] = [];
let firstArr: any;
let pushCount = 0;
console.log(`Page includes ${childArrLength} blocks`)
if (childArrLength > 100) {
for (let i = 0; i < childArr.length; i += 100) {
if (i == 0) {
firstArr = childArr.slice(0, 100);
} else {
const chunk = childArr.slice(i, i + 100);
extraArr.push(chunk);
pushCount++;
}
}
} else {
firstArr = childArr;
}
const pageProperties: any = {
parent: {
database_id: databaseID,
},
properties: {
title: {
title: [
{
text: {
content: title,
},
},
],
},
type: {
select: {
name: type || "Post",
},
},
status: {
select: {
name: stats || "Draft",
},
},
category: {
select: {
name: category || "Obsidian",
},
},
password: {
rich_text: [
{
text: {
content: pawrod || "",
},
},
],
},
icon: {
rich_text: [
{
text: {
content: favicon || "",
},
},
],
},
date: {
date: {
start: datetime || new Date().toISOString(),
},
},
},
};
// add tags
if (tags) {
pageProperties.properties.tags = {
multi_select: tags.map((tag) => {
return {name: tag};
}),
};
}
// add title icon
if (emoji) {
pageProperties.icon = {
emoji: emoji,
};
}
// add slug
if (slug) {
pageProperties.properties.slug = {
rich_text: [
{
text: {
content: slug,
},
},
],
};
}
// check if summary is available
if (summary) {
pageProperties.properties.summary = {
rich_text: [
{
text: {
content: summary,
},
},
],
};
}
if (cover) {
pageProperties.cover = {
type: "external",
external: {
url: cover,
},
};
}
if (!pageProperties.cover && this.plugin.settings.bannerUrl) {
pageProperties.cover = {
type: "external",
external: {
url: this.plugin.settings.bannerUrl,
},
};
}
const bodyString: any = {
...pageProperties,
children: firstArr,
};
console.log(bodyString);
let response: any;
let data: any;
response = await requestUrl({
url: `https://api.notion.com/v1/pages`,
method: "POST",
headers: {
"Content-Type": "application/json",
// 'User-Agent': 'obsidian.md',
Authorization:
"Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(bodyString),
throw: false
});
data = await response.json;
// console.log(data)
// console.log(response.status)
if (response.status !== 200) {
new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
console.log(`Page created: ${data.url}`);
console.log(`Page ID: ${data.id}`);
}
//
// upload the rest of the blocks
if (pushCount > 0) {
for (let i = 0; i < pushCount; i++) {
const extraBlocks = {
children: extraArr[i],
};
console.log(extraBlocks)
const extraResponse = await requestUrl({
url: `https://api.notion.com/v1/blocks/${data.id}/children`,
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(extraBlocks),
});
const extraData: any = await extraResponse.json;
if (extraResponse.status !== 200) {
new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${extraData.message}`);
} else {
console.log(`${i18nConfig["ExtraBlockUploaded"]} to page: ${data.id}`);
if (i === pushCount - 1) {
console.log(`${i18nConfig["BlockUploaded"]} to page: ${data.id}`);
new Notice(`${i18nConfig["BlockUploaded"]} page: ${data.id}`, 5000);
}
}
}
}
return {
response,
data
};
}
async syncMarkdownToNotionNext(
title: string,
emoji: string,
cover: string,
tags: string[],
type: string,
slug: string,
stats: string,
category: string,
summary: string,
paword: string,
favicon: string,
datetime: string,
markdown: string,
nowFile: TFile,
app: App,
): Promise<any> {
const options = {
notionLimits: {
truncate: false,
},
};
let res: any;
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const __content = yamlContent.__content;
const file2Block = markdownToBlocks(__content, options);
const frontmatter =
app.metadataCache.getFileCache(nowFile)?.frontmatter;
const {abName} = this.dbDetails;
const notionIDKey = `NotionID-${abName}`;
const notionID = frontmatter ? frontmatter[notionIDKey] : null;
// increase the limits
// Motivated by https://github.com/tryfabric/martian/issues/51
file2Block.forEach((block, index) => {
if (
block.type === "paragraph" &&
block.paragraph.rich_text.length > LIMITS.RICH_TEXT_ARRAYS
) {
const newParagraphBlocks: any[] = [];
const chunk: any = [];
const richTextChunks = chunk(block.paragraph.rich_text, 100);
richTextChunks.forEach((chunk: any) => {
newParagraphBlocks.push(paragraph(chunk));
});
file2Block.splice(index, 1, ...newParagraphBlocks);
}
});
if (notionID) {
res = await this.updatePage(
notionID,
title,
emoji,
cover,
tags,
type,
slug,
stats,
category,
summary,
paword,
favicon,
datetime,
file2Block,
);
} else {
res = await this.createPage(
title,
emoji,
cover,
tags,
type,
slug,
stats,
category,
summary,
paword,
favicon,
datetime,
file2Block,
);
}
let {response, data} = res;
// console.log(response)
if (response && response.status === 200) {
await updateYamlInfo(
markdown,
nowFile,
data,
app,
this.plugin,
this.dbDetails,
);
}
return res;
}
}

View File

@@ -1,53 +0,0 @@
import { App, Notice, requestUrl, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
export class UploadBaseCustom {
plugin: MyPlugin;
notion: Client;
agent: any;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails
}
async deletePage(notionID: string) {
const { notionAPI } = this.dbDetails
return requestUrl({
url: `https://api.notion.com/v1/blocks/${notionID}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + notionAPI,
'Notion-Version': '2022-06-28',
},
body: ''
});
}
async getDataBase(databaseID: string) {
const { notionAPI } = this.dbDetails
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + notionAPI,
'Notion-Version': '2022-06-28',
}
}
)
// Check if cover is present in the JSON response and then get the URL
if (response.json.cover && response.json.cover.external) {
return response.json.cover.external.url;
} else {
return null; // or some other default value, if you prefer
}
}
}

View File

@@ -1,348 +0,0 @@
import {App, Notice, TFile, requestUrl} from "obsidian";
import {markdownToBlocks} from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
import MyPlugin from "src/main";
import {DatabaseDetails, PluginSettings} from "../../ui/settingTabs";
import {updateYamlInfo} from "../updateYaml";
import {UploadBaseCustom} from "./BaseUpload2NotionCustom";
import {i18nConfig} from "../../lang/I18n";
interface CreatePageResponse {
response: any;
data: any;
}
export class Upload2NotionCustom extends UploadBaseCustom {
settings: PluginSettings;
dbDetails: DatabaseDetails;
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
this.dbDetails = dbDetails;
}
// 因为需要解析notion的block进行对比非常的麻烦
// 暂时就直接删除新建一个page
async updatePage(
notionID: string,
cover: string,
customValues: Record<string, string>,
childArr: any,
) {
await this.deletePage(notionID);
const {databaseID} = this.dbDetails;
const databaseCover = await this.getDataBase(
databaseID
);
if (cover == null) {
cover = databaseCover;
}
return await this.createPage(cover, customValues, childArr);
}
async createPage(
cover: string,
customValues: Record<string, string>,
childArr: any,
): Promise<CreatePageResponse> {
const {
databaseID,
customProperties,
notionAPI
} = this.dbDetails;
// remove the annotations from the childArr if type is code block
childArr.forEach((block: any) => {
if (block.type === "code") {
block.code.rich_text.forEach((item: any) => {
if (item.type === "text" && item.annotations) {
delete item.annotations;
}
}
);
}
}
);
// check the length of the childArr and split it into chunks of 100
const childArrLength = childArr.length;
let extraArr: any[] = [];
let firstArr: any;
let pushCount = 0;
console.log(`Page includes ${childArrLength} blocks`)
if (childArrLength > 100) {
for (let i = 0; i < childArr.length; i += 100) {
if (i == 0) {
firstArr = childArr.slice(0, 100);
} else {
const chunk = childArr.slice(i, i + 100);
extraArr.push(chunk);
pushCount++;
}
}
} else {
firstArr = childArr;
}
const bodyString: any = this.buildBodyString(customProperties, customValues, firstArr);
if (cover) {
bodyString.cover = {
type: "external",
external: {
url: cover,
},
};
}
if (!bodyString.cover && this.plugin.settings.bannerUrl) {
bodyString.cover = {
type: "external",
external: {
url: this.plugin.settings.bannerUrl,
},
};
}
console.log(bodyString)
let response: any;
let data: any;
response = await requestUrl({
url: `https://api.notion.com/v1/pages`,
method: "POST",
headers: {
"Content-Type": "application/json",
// 'User-Agent': 'obsidian.md',
Authorization:
"Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(bodyString),
throw: false
});
data = await response.json;
// console.log(data)
// console.log(response.status)
if (response.status !== 200) {
new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
console.log(`Page created: ${data.url}`);
console.log(`Page ID: ${data.id}`);
}
//
// upload the rest of the blocks
if (pushCount > 0) {
for (let i = 0; i < pushCount; i++) {
const extraBlocks = {
children: extraArr[i],
};
console.log(extraBlocks)
const extraResponse = await requestUrl({
url: `https://api.notion.com/v1/blocks/${data.id}/children`,
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(extraBlocks),
});
const extraData: any = await extraResponse.json;
if (extraResponse.status !== 200) {
new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
console.log(`Error message: \n ${extraData.message}`);
} else {
console.log(`${i18nConfig["ExtraBlockUploaded"]} to page: ${data.id}`);
if (i === pushCount - 1) {
console.log(`${i18nConfig["BlockUploaded"]} to page: ${data.id}`);
new Notice(`${i18nConfig["BlockUploaded"]} page: ${data.id}`, 5000);
}
}
}
}
return {
response, // for status code
data // for id and url
}
}
async syncMarkdownToNotionCustom(
cover: string,
customValues: Record<string, string>,
markdown: string,
nowFile: TFile,
app: App,
): Promise<any> {
const options = {
strictImageUrls: true,
notionLimits: {
truncate: false,
}
}
let res: any;
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const __content = yamlContent.__content;
const file2Block = markdownToBlocks(__content, options);
const frontMatter =
app.metadataCache.getFileCache(nowFile)?.frontmatter;
const {abName} = this.dbDetails
const notionIDKey = `NotionID-${abName}`;
const notionID = frontMatter ? frontMatter[notionIDKey] : null;
if (notionID) {
res = await this.updatePage(
notionID,
cover,
customValues,
file2Block,
);
} else {
res = await this.createPage(cover, customValues, file2Block);
}
let {response, data} = res;
// console.log(response)
if (response && response.status === 200) {
await updateYamlInfo(
markdown,
nowFile,
data,
app,
this.plugin,
this.dbDetails,
);
}
return res;
}
private buildPropertyObject(customName: string, customType: string, customValues: Record<string, any>) {
const value = customValues[customName] || '';
switch (customType) {
case "title":
return {
title: [
{
text: {
content: value,
},
},
],
};
case "rich_text":
return {
rich_text: [
{
text: {
content: value || '',
},
},
],
};
case "date":
return {
date: {
start: value || new Date().toISOString(),
},
};
case "number":
return {
number: Number(value),
};
case "phone_number":
return {
phone_number: value,
};
case "email":
return {
email: value,
};
case "url":
return {
url: value,
};
case "files":
return {
files: Array.isArray(value) ? value.map(url => ({
name: url,
type: "external",
external: {
url: url,
},
})) : [
{
name: value,
type: "external",
external: {
url: value,
},
},
],
};
case "checkbox":
return {
checkbox: Boolean(value) || false,
};
case "select":
return {
select: {
name: value,
},
};
case "multi_select":
return {
multi_select: Array.isArray(value) ? value.map(item => ({name: item})) : [{name: value}],
};
}
}
private buildBodyString(
customProperties: { customName: string; customType: string }[],
customValues: Record<string, string>,
childArr: any,
) {
const properties: { [key: string]: any } = {};
// Only include custom properties that have values
customProperties.forEach(({customName, customType}) => {
if (customValues[customName] !== undefined) {
properties[customName] = this.buildPropertyObject(customName, customType, customValues);
}
}
);
// console.log(properties)
return {
parent: {
database_id: this.dbDetails.databaseID,
},
properties,
children: childArr,
};
}
}

972
yarn.lock

File diff suppressed because it is too large Load Diff