gatsby-source-notion-churnotion 1.2.2 → 1.2.4
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/dist/createSchemaCustomization.js +10 -1
- package/dist/util/blocks/blockProcessor.d.ts +2 -5
- package/dist/util/blocks/blockProcessorRegistry.js +2 -0
- package/dist/util/blocks/index.d.ts +1 -0
- package/dist/util/blocks/index.js +1 -0
- package/dist/util/blocks/structureBlockProcessor.js +0 -4
- package/dist/util/blocks/tableOfContentsBlockProcessor.d.ts +6 -0
- package/dist/util/blocks/tableOfContentsBlockProcessor.js +23 -0
- package/dist/util/processor.d.ts +2 -5
- package/dist/util/processor.js +7 -1
- package/dist/util/tableOfContent.d.ts +3 -5
- package/dist/util/tableOfContent.js +69 -11
- package/dist/util/tocHelper.d.ts +27 -0
- package/dist/util/tocHelper.js +114 -0
- package/package.json +1 -1
@@ -133,7 +133,7 @@ const createSchemaCustomization = ({ actions, schema }) => {
|
|
133
133
|
version: Int
|
134
134
|
description: String
|
135
135
|
slug: String
|
136
|
-
tableOfContents: [
|
136
|
+
tableOfContents: [TocEntry]
|
137
137
|
category_list: [${constants_1.NODE_TYPE.Category}]
|
138
138
|
url: String!
|
139
139
|
thumbnail: File @link(by: "id", from: "thumbnail")
|
@@ -160,6 +160,15 @@ const createSchemaCustomization = ({ actions, schema }) => {
|
|
160
160
|
type ${constants_1.NODE_TYPE.RelatedPost} implements Node {
|
161
161
|
posts: [${constants_1.NODE_TYPE.Post}] @link(by: "id")
|
162
162
|
}
|
163
|
+
|
164
|
+
type TocEntry {
|
165
|
+
type: String!
|
166
|
+
hash: String!
|
167
|
+
title: String!
|
168
|
+
parentHash: String
|
169
|
+
level: Int
|
170
|
+
contextTitle: String
|
171
|
+
}
|
163
172
|
`,
|
164
173
|
]);
|
165
174
|
};
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { BaseContentBlock } from "notion-types";
|
2
2
|
import { Actions, GatsbyCache, Reporter } from "gatsby";
|
3
3
|
import { CustomImageBlock } from "../../types";
|
4
|
+
import { TocEntry } from "../tocHelper";
|
4
5
|
export interface BlockProcessorContext {
|
5
6
|
actions: Actions;
|
6
7
|
getCache: (this: void, id: string) => GatsbyCache;
|
@@ -12,11 +13,7 @@ export interface ProcessBlockResult {
|
|
12
13
|
thumbnail?: string | null;
|
13
14
|
plainText?: string;
|
14
15
|
updatedBlock?: BaseContentBlock;
|
15
|
-
tableOfContents?:
|
16
|
-
type: string;
|
17
|
-
hash: string;
|
18
|
-
title: string;
|
19
|
-
}[];
|
16
|
+
tableOfContents?: TocEntry[];
|
20
17
|
}
|
21
18
|
export declare abstract class BlockProcessor {
|
22
19
|
protected context: BlockProcessorContext;
|
@@ -5,6 +5,7 @@ const textBlockProcessor_1 = require("./textBlockProcessor");
|
|
5
5
|
const imageBlockProcessor_1 = require("./imageBlockProcessor");
|
6
6
|
const mediaBlockProcessor_1 = require("./mediaBlockProcessor");
|
7
7
|
const structureBlockProcessor_1 = require("./structureBlockProcessor");
|
8
|
+
const tableOfContentsBlockProcessor_1 = require("./tableOfContentsBlockProcessor");
|
8
9
|
class BlockProcessorRegistry {
|
9
10
|
processors = [];
|
10
11
|
context;
|
@@ -17,6 +18,7 @@ class BlockProcessorRegistry {
|
|
17
18
|
this.registerProcessor(new textBlockProcessor_1.TextBlockProcessor(this.context));
|
18
19
|
this.registerProcessor(new imageBlockProcessor_1.ImageBlockProcessor(this.context));
|
19
20
|
this.registerProcessor(new mediaBlockProcessor_1.MediaBlockProcessor(this.context));
|
21
|
+
this.registerProcessor(new tableOfContentsBlockProcessor_1.TableOfContentsBlockProcessor(this.context));
|
20
22
|
this.registerProcessor(new structureBlockProcessor_1.StructureBlockProcessor(this.context));
|
21
23
|
}
|
22
24
|
registerProcessor(processor) {
|
@@ -20,3 +20,4 @@ __exportStar(require("./imageBlockProcessor"), exports);
|
|
20
20
|
__exportStar(require("./mediaBlockProcessor"), exports);
|
21
21
|
__exportStar(require("./structureBlockProcessor"), exports);
|
22
22
|
__exportStar(require("./textBlockProcessor"), exports);
|
23
|
+
__exportStar(require("./tableOfContentsBlockProcessor"), exports);
|
@@ -11,7 +11,6 @@ class StructureBlockProcessor extends blockProcessor_1.BlockProcessor {
|
|
11
11
|
"table_row",
|
12
12
|
"divider",
|
13
13
|
"breadcrumb",
|
14
|
-
"table_of_contents",
|
15
14
|
"equation",
|
16
15
|
"synced_block",
|
17
16
|
"template",
|
@@ -44,9 +43,6 @@ class StructureBlockProcessor extends blockProcessor_1.BlockProcessor {
|
|
44
43
|
case "breadcrumb":
|
45
44
|
reporter.info(`Processing breadcrumb block`);
|
46
45
|
break;
|
47
|
-
case "table_of_contents":
|
48
|
-
reporter.info(`Processing table_of_contents block`);
|
49
|
-
break;
|
50
46
|
case "equation":
|
51
47
|
reporter.info(`Processing equation block: ${JSON.stringify(block.equation?.expression)}`);
|
52
48
|
break;
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { BaseContentBlock } from "notion-types";
|
2
|
+
import { BlockProcessor, ProcessBlockResult } from "./blockProcessor";
|
3
|
+
export declare class TableOfContentsBlockProcessor extends BlockProcessor {
|
4
|
+
canProcess(block: BaseContentBlock): boolean;
|
5
|
+
process(block: BaseContentBlock): Promise<ProcessBlockResult>;
|
6
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.TableOfContentsBlockProcessor = void 0;
|
4
|
+
const blockProcessor_1 = require("./blockProcessor");
|
5
|
+
class TableOfContentsBlockProcessor extends blockProcessor_1.BlockProcessor {
|
6
|
+
canProcess(block) {
|
7
|
+
return block.type === "table_of_contents";
|
8
|
+
}
|
9
|
+
async process(block) {
|
10
|
+
const { reporter } = this.context;
|
11
|
+
reporter.info(`Processing table_of_contents block with id: ${block.id}`);
|
12
|
+
// Table of contents blocks don't have any content by themselves
|
13
|
+
// They are used as placeholders, and the actual TOC data is generated separately
|
14
|
+
// and passed through the GraphQL schema
|
15
|
+
return {
|
16
|
+
plainText: "",
|
17
|
+
updatedBlock: block,
|
18
|
+
// We don't return tableOfContents from here because that's handled
|
19
|
+
// at a higher level in the processor.ts file
|
20
|
+
};
|
21
|
+
}
|
22
|
+
}
|
23
|
+
exports.TableOfContentsBlockProcessor = TableOfContentsBlockProcessor;
|
package/dist/util/processor.d.ts
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
import { Actions, GatsbyCache, Reporter } from "gatsby";
|
2
2
|
import { BaseContentBlock } from "notion-types";
|
3
|
-
|
4
|
-
|
5
|
-
hash: string;
|
6
|
-
title: string;
|
7
|
-
}[], BaseContentBlock[], string]>;
|
3
|
+
import { TocEntry } from "./tocHelper";
|
4
|
+
export declare const processor: (blocks: BaseContentBlock[], actions: Actions, getCache: (this: void, id: string) => GatsbyCache, createNodeId: (this: void, input: string) => string, reporter: Reporter, cache: GatsbyCache) => Promise<[string | null, TocEntry[], BaseContentBlock[], string]>;
|
package/dist/util/processor.js
CHANGED
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.processor = void 0;
|
4
4
|
const metadataProcessor_1 = require("./metadataProcessor");
|
5
5
|
const blocks_1 = require("./blocks");
|
6
|
+
const tocHelper_1 = require("./tocHelper");
|
7
|
+
const tableOfContent_1 = require("./tableOfContent");
|
6
8
|
const processor = async (blocks, actions, getCache, createNodeId, reporter, cache) => {
|
7
9
|
const { thumbnail, tableOfContents, updatedBlocks, rawText } = await processBlocksForContent(blocks, actions, getCache, createNodeId, reporter, cache);
|
8
10
|
await (0, metadataProcessor_1.processMetadata)(blocks, actions, createNodeId, reporter, cache);
|
@@ -17,6 +19,8 @@ const processBlocksForContent = async (blocks, actions, getCache, createNodeId,
|
|
17
19
|
reporter,
|
18
20
|
cache,
|
19
21
|
};
|
22
|
+
// 목차 컨텍스트 초기화
|
23
|
+
(0, tableOfContent_1.resetTocContext)();
|
20
24
|
// 블록 프로세서 레지스트리 생성
|
21
25
|
const processorRegistry = new blocks_1.BlockProcessorRegistry(context);
|
22
26
|
const tableOfContents = [];
|
@@ -42,6 +46,8 @@ const processBlocksForContent = async (blocks, actions, getCache, createNodeId,
|
|
42
46
|
}
|
43
47
|
return result;
|
44
48
|
}));
|
49
|
+
// 목차 최적화
|
50
|
+
const processedToc = (0, tocHelper_1.optimizeTocArray)(tableOfContents, reporter);
|
45
51
|
// 업데이트된 블록 적용
|
46
52
|
processResults.forEach((result, index) => {
|
47
53
|
if (result.updatedBlock) {
|
@@ -51,5 +57,5 @@ const processBlocksForContent = async (blocks, actions, getCache, createNodeId,
|
|
51
57
|
updatedBlocks[index] = blocks[index];
|
52
58
|
}
|
53
59
|
});
|
54
|
-
return { thumbnail, tableOfContents, updatedBlocks, rawText };
|
60
|
+
return { thumbnail, tableOfContents: processedToc, updatedBlocks, rawText };
|
55
61
|
};
|
@@ -1,6 +1,4 @@
|
|
1
1
|
import { BaseContentBlock } from "notion-types";
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
title: string;
|
6
|
-
}[]) => Promise<void>;
|
2
|
+
import { TocEntry } from "./tocHelper";
|
3
|
+
export declare const resetTocContext: () => void;
|
4
|
+
export declare const processTableOfContents: (block: BaseContentBlock, tableOfContents: TocEntry[]) => Promise<void>;
|
@@ -1,22 +1,80 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.processTableOfContents = void 0;
|
3
|
+
exports.processTableOfContents = exports.resetTocContext = void 0;
|
4
4
|
const crypto_1 = require("crypto");
|
5
|
+
// Hash 생성 시 사용할 캐시
|
6
|
+
const hashCache = new Map();
|
7
|
+
// 헤딩 레벨 트래킹을 위한 변수들
|
8
|
+
let lastH1Hash = "";
|
9
|
+
let lastH2Hash = "";
|
10
|
+
let lastH1Title = "";
|
11
|
+
let lastH2Title = "";
|
12
|
+
const resetTocContext = () => {
|
13
|
+
lastH1Hash = "";
|
14
|
+
lastH2Hash = "";
|
15
|
+
lastH1Title = "";
|
16
|
+
lastH2Title = "";
|
17
|
+
};
|
18
|
+
exports.resetTocContext = resetTocContext;
|
5
19
|
const processTableOfContents = async (block, tableOfContents) => {
|
6
20
|
if (["heading_1", "heading_2", "heading_3"].includes(block.type) &&
|
7
21
|
block[block.type]?.rich_text?.length > 0) {
|
8
22
|
const plainText = block[block.type]?.rich_text?.[0]?.plain_text || "";
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
23
|
+
// 부모 컨텍스트 추가
|
24
|
+
let contextualId = plainText;
|
25
|
+
let parentHash = "";
|
26
|
+
let level = 0;
|
27
|
+
// 상위 헤딩에 따라 컨텍스트 저장
|
28
|
+
if (block.type === "heading_1") {
|
29
|
+
lastH1Title = plainText;
|
30
|
+
lastH2Title = "";
|
31
|
+
contextualId = `h1-${plainText}`;
|
32
|
+
level = 1;
|
33
|
+
// heading_1은 최상위 레벨이므로 부모가 없음
|
34
|
+
}
|
35
|
+
else if (block.type === "heading_2") {
|
36
|
+
lastH2Title = plainText;
|
37
|
+
contextualId = `h2-${lastH1Title}-${plainText}`;
|
38
|
+
parentHash = lastH1Hash; // heading_2의 부모는 가장 최근의 heading_1
|
39
|
+
level = 2;
|
40
|
+
}
|
41
|
+
else if (block.type === "heading_3") {
|
42
|
+
contextualId = `h3-${lastH1Title}-${lastH2Title}-${plainText}`;
|
43
|
+
parentHash = lastH2Hash || lastH1Hash; // heading_3의 부모는 가장 최근의 heading_2, 없으면 heading_1
|
44
|
+
level = 3;
|
45
|
+
}
|
46
|
+
// 이미 처리된 플레인텍스트인지 확인
|
47
|
+
let hash;
|
48
|
+
if (hashCache.has(contextualId)) {
|
49
|
+
hash = hashCache.get(contextualId);
|
50
|
+
}
|
51
|
+
else {
|
52
|
+
hash = `link-${plainText
|
53
|
+
.replace(/[^a-zA-Z0-9가-힣\s-_]/g, "")
|
54
|
+
.trim()
|
55
|
+
.replace(/\s+/g, "-")
|
56
|
+
.toLowerCase()}-${(0, crypto_1.randomUUID)().substring(0, 4)}`;
|
57
|
+
hashCache.set(contextualId, hash);
|
58
|
+
}
|
59
|
+
// 현재 해시 저장
|
60
|
+
if (block.type === "heading_1") {
|
61
|
+
lastH1Hash = hash;
|
62
|
+
}
|
63
|
+
else if (block.type === "heading_2") {
|
64
|
+
lastH2Hash = hash;
|
65
|
+
}
|
14
66
|
block.hash = hash;
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
67
|
+
// 중복 체크 - 동일한 hash가 이미 존재하는지 확인
|
68
|
+
const existingTocIndex = tableOfContents.findIndex((toc) => toc.hash === hash);
|
69
|
+
if (existingTocIndex === -1) {
|
70
|
+
tableOfContents.push({
|
71
|
+
type: block.type,
|
72
|
+
hash,
|
73
|
+
title: plainText,
|
74
|
+
parentHash,
|
75
|
+
level,
|
76
|
+
});
|
77
|
+
}
|
20
78
|
}
|
21
79
|
};
|
22
80
|
exports.processTableOfContents = processTableOfContents;
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import { Reporter } from "gatsby";
|
2
|
+
/**
|
3
|
+
* TocEntry interface representing a table of contents entry
|
4
|
+
*/
|
5
|
+
export interface TocEntry {
|
6
|
+
type: string;
|
7
|
+
hash: string;
|
8
|
+
title: string;
|
9
|
+
parentHash?: string;
|
10
|
+
level?: number;
|
11
|
+
contextTitle?: string;
|
12
|
+
}
|
13
|
+
/**
|
14
|
+
* Utility function to efficiently handle large table of contents arrays
|
15
|
+
* by removing duplicates and optionally limiting size while preserving hierarchy
|
16
|
+
*/
|
17
|
+
export declare const optimizeTocArray: (tocEntries: TocEntry[], reporter: Reporter, options?: {
|
18
|
+
maxSize?: number;
|
19
|
+
warnThreshold?: number;
|
20
|
+
removeDuplicates?: boolean;
|
21
|
+
structureByLevel?: boolean;
|
22
|
+
enhanceDuplicates?: boolean;
|
23
|
+
}) => TocEntry[];
|
24
|
+
/**
|
25
|
+
* Enhances TOC entries with contextual titles to disambiguate duplicate titles
|
26
|
+
*/
|
27
|
+
export declare const enrichDuplicateTitles: (tocEntries: TocEntry[], reporter: Reporter) => void;
|
@@ -0,0 +1,114 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.enrichDuplicateTitles = exports.optimizeTocArray = void 0;
|
4
|
+
/**
|
5
|
+
* Utility function to efficiently handle large table of contents arrays
|
6
|
+
* by removing duplicates and optionally limiting size while preserving hierarchy
|
7
|
+
*/
|
8
|
+
const optimizeTocArray = (tocEntries, reporter, options = {}) => {
|
9
|
+
const { maxSize = 1000, // Maximum entries to include
|
10
|
+
warnThreshold = 300, // Threshold to issue warning
|
11
|
+
removeDuplicates = true, // Whether to remove duplicates
|
12
|
+
structureByLevel = true, // Whether to structure by heading level
|
13
|
+
enhanceDuplicates = true, // Whether to enhance duplicate titles with context
|
14
|
+
} = options;
|
15
|
+
if (!tocEntries || tocEntries.length === 0) {
|
16
|
+
return [];
|
17
|
+
}
|
18
|
+
// Track memory usage for large TOCs
|
19
|
+
if (tocEntries.length > warnThreshold) {
|
20
|
+
reporter.warn(`Large table of contents detected (${tocEntries.length} items). This might affect performance.`);
|
21
|
+
}
|
22
|
+
// Enrich TOC entries with level information if not present
|
23
|
+
const enrichedTocEntries = tocEntries.map((entry) => {
|
24
|
+
if (!entry.level) {
|
25
|
+
const level = entry.type === "heading_1"
|
26
|
+
? 1
|
27
|
+
: entry.type === "heading_2"
|
28
|
+
? 2
|
29
|
+
: entry.type === "heading_3"
|
30
|
+
? 3
|
31
|
+
: 0;
|
32
|
+
return { ...entry, level };
|
33
|
+
}
|
34
|
+
return entry;
|
35
|
+
});
|
36
|
+
// Sort by appearance (using array index as proxy) and then by level
|
37
|
+
enrichedTocEntries.sort((a, b) => {
|
38
|
+
// Sort by level to ensure headers are properly nested
|
39
|
+
return (a.level || 0) - (b.level || 0);
|
40
|
+
});
|
41
|
+
// Add context to duplicate title entries for better display
|
42
|
+
if (enhanceDuplicates) {
|
43
|
+
(0, exports.enrichDuplicateTitles)(enrichedTocEntries, reporter);
|
44
|
+
}
|
45
|
+
// Remove duplicates if requested
|
46
|
+
let processedToc = enrichedTocEntries;
|
47
|
+
if (removeDuplicates) {
|
48
|
+
const startTime = Date.now();
|
49
|
+
const uniqueMap = new Map();
|
50
|
+
// Use the hash to ensure uniqueness, but preserve context when display titles are the same
|
51
|
+
for (const entry of enrichedTocEntries) {
|
52
|
+
uniqueMap.set(entry.hash, entry);
|
53
|
+
}
|
54
|
+
processedToc = Array.from(uniqueMap.values());
|
55
|
+
const removedCount = enrichedTocEntries.length - processedToc.length;
|
56
|
+
const processTime = Date.now() - startTime;
|
57
|
+
if (removedCount > 0) {
|
58
|
+
reporter.info(`Removed ${removedCount} duplicate TOC entries in ${processTime}ms.`);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
// Limit size if necessary
|
62
|
+
if (processedToc.length > maxSize) {
|
63
|
+
reporter.warn(`Table of contents exceeds maximum size (${processedToc.length} > ${maxSize}). Truncating.`);
|
64
|
+
processedToc = processedToc.slice(0, maxSize);
|
65
|
+
}
|
66
|
+
return processedToc;
|
67
|
+
};
|
68
|
+
exports.optimizeTocArray = optimizeTocArray;
|
69
|
+
/**
|
70
|
+
* Enhances TOC entries with contextual titles to disambiguate duplicate titles
|
71
|
+
*/
|
72
|
+
const enrichDuplicateTitles = (tocEntries, reporter) => {
|
73
|
+
if (!tocEntries || tocEntries.length === 0)
|
74
|
+
return;
|
75
|
+
// Find duplicate titles
|
76
|
+
const titleCounts = new Map();
|
77
|
+
const duplicateTitles = new Set();
|
78
|
+
// Count occurrences of each title
|
79
|
+
for (const entry of tocEntries) {
|
80
|
+
const count = (titleCounts.get(entry.title) || 0) + 1;
|
81
|
+
titleCounts.set(entry.title, count);
|
82
|
+
if (count > 1) {
|
83
|
+
duplicateTitles.add(entry.title);
|
84
|
+
}
|
85
|
+
}
|
86
|
+
if (duplicateTitles.size === 0)
|
87
|
+
return;
|
88
|
+
reporter.info(`Found ${duplicateTitles.size} duplicate title(s) in TOC. Adding context...`);
|
89
|
+
// Build parent map for fast lookups
|
90
|
+
const entryMap = new Map();
|
91
|
+
for (const entry of tocEntries) {
|
92
|
+
entryMap.set(entry.hash, entry);
|
93
|
+
}
|
94
|
+
// Create a hierarchical map to find parents
|
95
|
+
for (const entry of tocEntries) {
|
96
|
+
if (duplicateTitles.has(entry.title)) {
|
97
|
+
let context = "";
|
98
|
+
let parentEntry;
|
99
|
+
// Find parent context
|
100
|
+
if (entry.parentHash && entryMap.has(entry.parentHash)) {
|
101
|
+
parentEntry = entryMap.get(entry.parentHash);
|
102
|
+
context = parentEntry?.title || "";
|
103
|
+
}
|
104
|
+
// Add context to title
|
105
|
+
if (context) {
|
106
|
+
entry.contextTitle = `${entry.title} (${context})`;
|
107
|
+
}
|
108
|
+
else {
|
109
|
+
entry.contextTitle = entry.title;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
}
|
113
|
+
};
|
114
|
+
exports.enrichDuplicateTitles = enrichDuplicateTitles;
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "gatsby-source-notion-churnotion",
|
3
3
|
"description": "Gatsby plugin that can connect with One Notion Database RECURSIVELY using official API",
|
4
|
-
"version": "1.2.
|
4
|
+
"version": "1.2.4",
|
5
5
|
"skipLibCheck": true,
|
6
6
|
"license": "0BSD",
|
7
7
|
"main": "./dist/gatsby-node.js",
|