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.
@@ -133,7 +133,7 @@ const createSchemaCustomization = ({ actions, schema }) => {
133
133
  version: Int
134
134
  description: String
135
135
  slug: String
136
- tableOfContents: [JSON]
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) {
@@ -4,3 +4,4 @@ export * from "./imageBlockProcessor";
4
4
  export * from "./mediaBlockProcessor";
5
5
  export * from "./structureBlockProcessor";
6
6
  export * from "./textBlockProcessor";
7
+ export * from "./tableOfContentsBlockProcessor";
@@ -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;
@@ -1,7 +1,4 @@
1
1
  import { Actions, GatsbyCache, Reporter } from "gatsby";
2
2
  import { BaseContentBlock } from "notion-types";
3
- 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, {
4
- type: string;
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]>;
@@ -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
- export declare const processTableOfContents: (block: BaseContentBlock, tableOfContents: {
3
- type: string;
4
- hash: string;
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
- const hash = `link-${plainText
10
- .replace(/[^a-zA-Z0-9가-힣\s-_]/g, "")
11
- .trim()
12
- .replace(/\s+/g, "-")
13
- .toLowerCase()}-${(0, crypto_1.randomUUID)().substring(0, 4)}`;
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
- tableOfContents.push({
16
- type: block.type,
17
- hash,
18
- title: plainText,
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.2",
4
+ "version": "1.2.4",
5
5
  "skipLibCheck": true,
6
6
  "license": "0BSD",
7
7
  "main": "./dist/gatsby-node.js",