gatsby-source-notion-churnotion 1.2.4 → 1.2.6

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSchemaCustomization = void 0;
4
4
  const constants_1 = require("./constants");
5
+ const resolvers_1 = require("./util/resolvers");
5
6
  const createSchemaCustomization = ({ actions, schema }) => {
6
7
  const { createTypes } = actions;
7
8
  createTypes([
@@ -119,57 +120,118 @@ const createSchemaCustomization = ({ actions, schema }) => {
119
120
  },
120
121
  },
121
122
  }),
122
- `
123
- type ${constants_1.NODE_TYPE.Post} implements Node {
124
- id: ID!
125
- category: ${constants_1.NODE_TYPE.Category}! @link(by: "id", from: "category")
126
- tags: [${constants_1.NODE_TYPE.Tag}] @link(by: "id")
127
- book: ${constants_1.NODE_TYPE.Book} @link(by: "id")
128
- book_index: Int
129
- title: String
130
- content: [JSON]
131
- create_date: Date! @dateformat
132
- update_date: Date! @dateformat
133
- version: Int
134
- description: String
135
- slug: String
136
- tableOfContents: [TocEntry]
137
- category_list: [${constants_1.NODE_TYPE.Category}]
138
- url: String!
139
- thumbnail: File @link(by: "id", from: "thumbnail")
140
- rawText: String
141
- }
142
-
143
- type ${constants_1.NODE_TYPE.Tag} implements Node {
144
- id: ID!
145
- tag_name: String!
146
- slug: String!
147
- color: String!
148
- children: [${constants_1.NODE_TYPE.Post}] @link(by: "id", from: "tags")
149
- url: String!
150
- }
151
-
152
- type ${constants_1.NODE_TYPE.Metadata} implements Node {
153
- id: ID!,
154
- title: String,
155
- description: String,
156
- image: String,
157
- url: String,
158
- }
159
-
160
- type ${constants_1.NODE_TYPE.RelatedPost} implements Node {
161
- posts: [${constants_1.NODE_TYPE.Post}] @link(by: "id")
162
- }
163
-
164
- type TocEntry {
165
- type: String!
166
- hash: String!
167
- title: String!
168
- parentHash: String
169
- level: Int
170
- contextTitle: String
171
- }
172
- `,
123
+ schema.buildObjectType({
124
+ name: constants_1.NODE_TYPE.Post,
125
+ interfaces: ["Node"],
126
+ fields: {
127
+ id: "ID!",
128
+ category: {
129
+ type: `${constants_1.NODE_TYPE.Category}!`,
130
+ extensions: {
131
+ link: { by: "id", from: "category" },
132
+ },
133
+ },
134
+ tags: {
135
+ type: `[${constants_1.NODE_TYPE.Tag}]`,
136
+ extensions: {
137
+ link: { by: "id" },
138
+ },
139
+ },
140
+ book: {
141
+ type: constants_1.NODE_TYPE.Book,
142
+ extensions: {
143
+ link: { by: "id" },
144
+ },
145
+ },
146
+ book_index: "Int",
147
+ title: "String",
148
+ content: "[JSON]",
149
+ create_date: {
150
+ type: "Date!",
151
+ extensions: {
152
+ dateformat: {},
153
+ },
154
+ },
155
+ update_date: {
156
+ type: "Date!",
157
+ extensions: {
158
+ dateformat: {},
159
+ },
160
+ },
161
+ version: "Int",
162
+ description: "String",
163
+ slug: "String",
164
+ tableOfContents: "[TocEntry]",
165
+ formattedTableOfContents: {
166
+ type: "[TocEntry]",
167
+ resolve: (source, args, context) => {
168
+ if (!source.tableOfContents)
169
+ return [];
170
+ return (0, resolvers_1.formatTableOfContents)(source.tableOfContents, context.reporter);
171
+ },
172
+ },
173
+ category_list: `[${constants_1.NODE_TYPE.Category}]`,
174
+ url: "String!",
175
+ thumbnail: {
176
+ type: "File",
177
+ extensions: {
178
+ link: { by: "id", from: "thumbnail" },
179
+ },
180
+ },
181
+ rawText: "String",
182
+ },
183
+ }),
184
+ schema.buildObjectType({
185
+ name: constants_1.NODE_TYPE.Tag,
186
+ interfaces: ["Node"],
187
+ fields: {
188
+ id: "ID!",
189
+ tag_name: "String!",
190
+ slug: "String!",
191
+ color: "String!",
192
+ children: {
193
+ type: `[${constants_1.NODE_TYPE.Post}]`,
194
+ extensions: {
195
+ link: { by: "id", from: "tags" },
196
+ },
197
+ },
198
+ url: "String!",
199
+ },
200
+ }),
201
+ schema.buildObjectType({
202
+ name: constants_1.NODE_TYPE.Metadata,
203
+ interfaces: ["Node"],
204
+ fields: {
205
+ id: "ID!",
206
+ title: "String",
207
+ description: "String",
208
+ image: "String",
209
+ url: "String",
210
+ },
211
+ }),
212
+ schema.buildObjectType({
213
+ name: constants_1.NODE_TYPE.RelatedPost,
214
+ interfaces: ["Node"],
215
+ fields: {
216
+ posts: {
217
+ type: `[${constants_1.NODE_TYPE.Post}]`,
218
+ extensions: {
219
+ link: { by: "id" },
220
+ },
221
+ },
222
+ },
223
+ }),
224
+ schema.buildObjectType({
225
+ name: "TocEntry",
226
+ fields: {
227
+ type: "String!",
228
+ hash: "String!",
229
+ title: "String!",
230
+ contextTitle: "String",
231
+ parentHash: "String",
232
+ level: "Int",
233
+ },
234
+ }),
173
235
  ]);
174
236
  };
175
237
  exports.createSchemaCustomization = createSchemaCustomization;
@@ -29,6 +29,11 @@ const processBlocksForContent = async (blocks, actions, getCache, createNodeId,
29
29
  const updatedBlocks = [];
30
30
  // 첫 번째 이미지 블록을 찾아 썸네일로 사용
31
31
  let firstImageIndex = blocks.findIndex((block) => block.type === "image");
32
+ // 헤딩 타입 블록을 먼저 처리하여 구조를 생성
33
+ const headingBlocks = blocks.filter((block) => ["heading_1", "heading_2", "heading_3"].includes(block.type));
34
+ if (headingBlocks.length > 0) {
35
+ reporter.info(`Found ${headingBlocks.length} heading blocks for TOC generation`);
36
+ }
32
37
  // 블록 처리
33
38
  const processResults = await Promise.all(blocks.map(async (block, index) => {
34
39
  const result = await processorRegistry.processBlock(block);
@@ -46,8 +51,19 @@ const processBlocksForContent = async (blocks, actions, getCache, createNodeId,
46
51
  }
47
52
  return result;
48
53
  }));
54
+ // 목차 로깅 및 디버깅
55
+ const h1Count = tableOfContents.filter((item) => item.type === "heading_1").length;
56
+ const h2Count = tableOfContents.filter((item) => item.type === "heading_2").length;
57
+ const h3Count = tableOfContents.filter((item) => item.type === "heading_3").length;
58
+ reporter.info(`TOC entries before optimization: ${tableOfContents.length} total, ${h1Count} h1, ${h2Count} h2, ${h3Count} h3`);
49
59
  // 목차 최적화
50
- const processedToc = (0, tocHelper_1.optimizeTocArray)(tableOfContents, reporter);
60
+ const processedToc = (0, tocHelper_1.optimizeTocArray)(tableOfContents, reporter, {
61
+ maxSize: 2000,
62
+ warnThreshold: 500,
63
+ removeDuplicates: true,
64
+ enhanceDuplicates: true,
65
+ buildHierarchy: true,
66
+ });
51
67
  // 업데이트된 블록 적용
52
68
  processResults.forEach((result, index) => {
53
69
  if (result.updatedBlock) {
@@ -0,0 +1,6 @@
1
+ import { Reporter } from "gatsby";
2
+ import { TocEntry } from "./tocHelper";
3
+ /**
4
+ * 목차 구조를 계층적으로 변환하는 함수
5
+ */
6
+ export declare const formatTableOfContents: (tableOfContents: TocEntry[], reporter: Reporter) => TocEntry[];
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatTableOfContents = void 0;
4
+ /**
5
+ * 목차 구조를 계층적으로 변환하는 함수
6
+ */
7
+ const formatTableOfContents = (tableOfContents, reporter) => {
8
+ if (!tableOfContents || tableOfContents.length === 0) {
9
+ return [];
10
+ }
11
+ try {
12
+ // Deep clone to avoid modifying original
13
+ const tocClone = JSON.parse(JSON.stringify(tableOfContents));
14
+ // Ensure TOC entries are sorted by level first, then by appearance order
15
+ const sortedToc = tocClone.sort((a, b) => {
16
+ // Level 기준 정렬
17
+ if ((a.level || 0) !== (b.level || 0)) {
18
+ return (a.level || 0) - (b.level || 0);
19
+ }
20
+ // Original order is preserved
21
+ return 0;
22
+ });
23
+ // Add contextTitle where missing
24
+ for (const entry of sortedToc) {
25
+ if (!entry.contextTitle) {
26
+ entry.contextTitle = entry.title;
27
+ }
28
+ }
29
+ // 1. Find all main headings (h1)
30
+ const mainHeadings = sortedToc.filter((entry) => entry.level === 1);
31
+ // 2. Find all command headings (h2) with their parent h1
32
+ const commandHeadings = sortedToc.filter((entry) => entry.level === 2);
33
+ // Group h2 by parent h1 for easier traversal
34
+ const h1ToH2Map = new Map();
35
+ for (const h1 of mainHeadings) {
36
+ h1ToH2Map.set(h1.hash, []);
37
+ }
38
+ for (const h2 of commandHeadings) {
39
+ if (h2.parentHash) {
40
+ const h2List = h1ToH2Map.get(h2.parentHash) || [];
41
+ h2List.push(h2);
42
+ h1ToH2Map.set(h2.parentHash, h2List);
43
+ }
44
+ }
45
+ // 3. Find all subheadings (h3) with their parent h2
46
+ const subheadings = sortedToc.filter((entry) => entry.level === 3);
47
+ // Group h3 by parent h2 for easier traversal
48
+ const h2ToH3Map = new Map();
49
+ for (const h2 of commandHeadings) {
50
+ h2ToH3Map.set(h2.hash, []);
51
+ }
52
+ for (const h3 of subheadings) {
53
+ if (h3.parentHash) {
54
+ const h3List = h2ToH3Map.get(h3.parentHash) || [];
55
+ h3List.push(h3);
56
+ h2ToH3Map.set(h3.parentHash, h3List);
57
+ }
58
+ }
59
+ // Find all unique h3 titles that appear anywhere
60
+ const allH3Titles = new Set();
61
+ for (const h3 of subheadings) {
62
+ allH3Titles.add(h3.title);
63
+ }
64
+ // Common expected subheadings for commands
65
+ const commonSubheadings = [
66
+ "요약",
67
+ "개요",
68
+ "설명",
69
+ "옵션",
70
+ "문법",
71
+ "주요 명령어",
72
+ "주요 속성 설명",
73
+ "설치",
74
+ "기본 명령",
75
+ ];
76
+ // Add common subheadings to the set
77
+ for (const title of commonSubheadings) {
78
+ allH3Titles.add(title);
79
+ }
80
+ reporter.info(`Found ${allH3Titles.size} unique h3 titles across sections`);
81
+ // For each command, ensure all common subheadings exist
82
+ const finalToc = [...sortedToc];
83
+ let addedEntries = 0;
84
+ // Create a map of known title combinations to avoid duplicates
85
+ const knownCombinations = new Set();
86
+ // Add all existing combinations to the known set
87
+ for (const entry of subheadings) {
88
+ if (entry.parentHash) {
89
+ const parentCommand = commandHeadings.find((h2) => h2.hash === entry.parentHash);
90
+ if (parentCommand) {
91
+ const combo = `${parentCommand.title}:${entry.title}`;
92
+ knownCombinations.add(combo);
93
+ }
94
+ }
95
+ }
96
+ // Check which commands are missing which subheadings
97
+ for (const h2 of commandHeadings) {
98
+ const h3Entries = h2ToH3Map.get(h2.hash) || [];
99
+ const existingTitles = new Set(h3Entries.map((entry) => entry.title));
100
+ // For each expected subheading, check if it exists
101
+ for (const title of commonSubheadings) {
102
+ const combo = `${h2.title}:${title}`;
103
+ // Skip if we already have this combination
104
+ if (knownCombinations.has(combo) || existingTitles.has(title)) {
105
+ continue;
106
+ }
107
+ // We don't have this subheading for this command - but don't add synthetic entries
108
+ // in this implementation as we want to show only what's actually in the document
109
+ }
110
+ }
111
+ // If we added entries, re-sort the TOC
112
+ if (addedEntries > 0) {
113
+ reporter.info(`Added ${addedEntries} missing subheadings to TOC`);
114
+ finalToc.sort((a, b) => {
115
+ if ((a.level || 0) !== (b.level || 0)) {
116
+ return (a.level || 0) - (b.level || 0);
117
+ }
118
+ return 0;
119
+ });
120
+ }
121
+ // Always make sure all h3 entries have context titles that include their parent command
122
+ for (const entry of finalToc) {
123
+ if (entry.level === 3 && entry.parentHash) {
124
+ const parentCommand = commandHeadings.find((h2) => h2.hash === entry.parentHash);
125
+ if (parentCommand) {
126
+ entry.contextTitle = `${entry.title} (${parentCommand.title})`;
127
+ }
128
+ }
129
+ }
130
+ return finalToc;
131
+ }
132
+ catch (error) {
133
+ reporter.error(`Error formatting table of contents: ${error}`);
134
+ return tableOfContents; // Return original on error
135
+ }
136
+ };
137
+ exports.formatTableOfContents = formatTableOfContents;
@@ -9,11 +9,13 @@ let lastH1Hash = "";
9
9
  let lastH2Hash = "";
10
10
  let lastH1Title = "";
11
11
  let lastH2Title = "";
12
+ let currentCommandSection = ""; // 현재 명령어 섹션 추적
12
13
  const resetTocContext = () => {
13
14
  lastH1Hash = "";
14
15
  lastH2Hash = "";
15
16
  lastH1Title = "";
16
17
  lastH2Title = "";
18
+ currentCommandSection = "";
17
19
  };
18
20
  exports.resetTocContext = resetTocContext;
19
21
  const processTableOfContents = async (block, tableOfContents) => {
@@ -28,19 +30,33 @@ const processTableOfContents = async (block, tableOfContents) => {
28
30
  if (block.type === "heading_1") {
29
31
  lastH1Title = plainText;
30
32
  lastH2Title = "";
33
+ lastH2Hash = ""; // H1을 만나면 H2 해시도 초기화
34
+ currentCommandSection = ""; // 새 섹션 시작
31
35
  contextualId = `h1-${plainText}`;
32
36
  level = 1;
33
37
  // heading_1은 최상위 레벨이므로 부모가 없음
34
38
  }
35
39
  else if (block.type === "heading_2") {
40
+ // H2는 반드시 H1 아래에 속함
36
41
  lastH2Title = plainText;
42
+ currentCommandSection = plainText; // 현재 명령어 설정 (예: useradd, usermod 등)
37
43
  contextualId = `h2-${lastH1Title}-${plainText}`;
38
44
  parentHash = lastH1Hash; // heading_2의 부모는 가장 최근의 heading_1
39
45
  level = 2;
40
46
  }
41
47
  else if (block.type === "heading_3") {
42
- contextualId = `h3-${lastH1Title}-${lastH2Title}-${plainText}`;
43
- parentHash = lastH2Hash || lastH1Hash; // heading_3의 부모는 가장 최근의 heading_2, 없으면 heading_1
48
+ // H3 처리를 개선
49
+ const parentSectionInfo = lastH2Hash
50
+ ? currentCommandSection
51
+ : lastH1Title;
52
+ const fullContextId = `h3-${lastH1Title}-${parentSectionInfo}-${plainText}`;
53
+ contextualId = fullContextId;
54
+ // 현재 H2가 없다면 바로 H1을 부모로 설정
55
+ parentHash = lastH2Hash || lastH1Hash;
56
+ // H2가 없는 경우 현재 H1 제목을 컨텍스트로 사용
57
+ if (!lastH2Hash) {
58
+ currentCommandSection = lastH1Title;
59
+ }
44
60
  level = 3;
45
61
  }
46
62
  // 이미 처리된 플레인텍스트인지 확인
@@ -64,13 +80,29 @@ const processTableOfContents = async (block, tableOfContents) => {
64
80
  lastH2Hash = hash;
65
81
  }
66
82
  block.hash = hash;
83
+ // 항상 복사본을 사용하여 중복을 방지
84
+ const displayTitle = plainText;
85
+ let contextTitle = displayTitle;
86
+ // 서브헤딩에 컨텍스트 추가 (H3)
87
+ if (level === 3) {
88
+ // H2가 없으면 H1을 컨텍스트로 사용
89
+ const contextSectionName = lastH2Hash
90
+ ? currentCommandSection
91
+ : lastH1Title;
92
+ contextTitle = `${plainText} (${contextSectionName})`;
93
+ }
94
+ // H2 항목도 컨텍스트 추가
95
+ else if (level === 2) {
96
+ contextTitle = `${plainText} (${lastH1Title})`;
97
+ }
67
98
  // 중복 체크 - 동일한 hash가 이미 존재하는지 확인
68
99
  const existingTocIndex = tableOfContents.findIndex((toc) => toc.hash === hash);
69
100
  if (existingTocIndex === -1) {
70
101
  tableOfContents.push({
71
102
  type: block.type,
72
103
  hash,
73
- title: plainText,
104
+ title: displayTitle,
105
+ contextTitle,
74
106
  parentHash,
75
107
  level,
76
108
  });
@@ -6,9 +6,10 @@ export interface TocEntry {
6
6
  type: string;
7
7
  hash: string;
8
8
  title: string;
9
+ contextTitle?: string;
9
10
  parentHash?: string;
10
11
  level?: number;
11
- contextTitle?: string;
12
+ children?: TocEntry[];
12
13
  }
13
14
  /**
14
15
  * Utility function to efficiently handle large table of contents arrays
@@ -18,9 +19,13 @@ export declare const optimizeTocArray: (tocEntries: TocEntry[], reporter: Report
18
19
  maxSize?: number;
19
20
  warnThreshold?: number;
20
21
  removeDuplicates?: boolean;
21
- structureByLevel?: boolean;
22
22
  enhanceDuplicates?: boolean;
23
+ buildHierarchy?: boolean;
23
24
  }) => TocEntry[];
25
+ /**
26
+ * TOC 구조의 유효성을 검사하고 문제가 있는 항목을 필터링
27
+ */
28
+ export declare const validateTocStructure: (tocEntries: TocEntry[], reporter: Reporter) => TocEntry[];
24
29
  /**
25
30
  * Enhances TOC entries with contextual titles to disambiguate duplicate titles
26
31
  */
@@ -1,16 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.enrichDuplicateTitles = exports.optimizeTocArray = void 0;
3
+ exports.enrichDuplicateTitles = exports.validateTocStructure = exports.optimizeTocArray = void 0;
4
4
  /**
5
5
  * Utility function to efficiently handle large table of contents arrays
6
6
  * by removing duplicates and optionally limiting size while preserving hierarchy
7
7
  */
8
8
  const optimizeTocArray = (tocEntries, reporter, options = {}) => {
9
- const { maxSize = 1000, // Maximum entries to include
9
+ const { maxSize = 2000, // Maximum entries to include
10
10
  warnThreshold = 300, // Threshold to issue warning
11
11
  removeDuplicates = true, // Whether to remove duplicates
12
- structureByLevel = true, // Whether to structure by heading level
13
12
  enhanceDuplicates = true, // Whether to enhance duplicate titles with context
13
+ buildHierarchy = true, // Whether to build the hierarchy tree
14
14
  } = options;
15
15
  if (!tocEntries || tocEntries.length === 0) {
16
16
  return [];
@@ -19,7 +19,7 @@ const optimizeTocArray = (tocEntries, reporter, options = {}) => {
19
19
  if (tocEntries.length > warnThreshold) {
20
20
  reporter.warn(`Large table of contents detected (${tocEntries.length} items). This might affect performance.`);
21
21
  }
22
- // Enrich TOC entries with level information if not present
22
+ // Pre-process all entries to ensure proper levels
23
23
  const enrichedTocEntries = tocEntries.map((entry) => {
24
24
  if (!entry.level) {
25
25
  const level = entry.type === "heading_1"
@@ -33,39 +33,225 @@ const optimizeTocArray = (tocEntries, reporter, options = {}) => {
33
33
  }
34
34
  return entry;
35
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
- }
36
+ // 구조적 유효성 검증 정리
37
+ const validatedToc = (0, exports.validateTocStructure)(enrichedTocEntries, reporter);
45
38
  // Remove duplicates if requested
46
- let processedToc = enrichedTocEntries;
39
+ let processedToc = validatedToc;
47
40
  if (removeDuplicates) {
48
41
  const startTime = Date.now();
42
+ // 중복 해시 및 타이틀 검사를 위한 맵
49
43
  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);
44
+ // 섹션별 유효한 하위 항목 분류
45
+ const h1Entries = new Map(); // 최상위 항목
46
+ const childEntries = new Map(); // 상위 해시 -> 하위 해시 세트
47
+ // 1. 먼저 모든 H1 항목을 수집하고 맵에 저장
48
+ for (const entry of validatedToc) {
49
+ if (entry.level === 1) {
50
+ h1Entries.set(entry.hash, entry);
51
+ childEntries.set(entry.hash, new Set());
52
+ }
53
+ }
54
+ // 2. 각 항목을 처리하여 해시 기반으로 적절한 부모-자식 관계 검증
55
+ for (const entry of validatedToc) {
56
+ // H1은 이미 처리됨
57
+ if (entry.level === 1) {
58
+ uniqueMap.set(entry.hash, entry);
59
+ continue;
60
+ }
61
+ // H2, H3 항목 처리
62
+ if (entry.parentHash) {
63
+ // 부모 해시가 있는 경우 부모가 실제로 존재하는지 확인
64
+ const parentExists = uniqueMap.has(entry.parentHash) || h1Entries.has(entry.parentHash);
65
+ if (parentExists) {
66
+ // 부모에 이 항목을 자식으로 추가
67
+ if (childEntries.has(entry.parentHash)) {
68
+ childEntries.get(entry.parentHash).add(entry.hash);
69
+ }
70
+ else {
71
+ childEntries.set(entry.parentHash, new Set([entry.hash]));
72
+ }
73
+ // 항목을 유효한 것으로 추가
74
+ uniqueMap.set(entry.hash, entry);
75
+ }
76
+ else {
77
+ // 부모가 존재하지 않는 고아 항목
78
+ reporter.warn(`Skipping orphaned TOC entry: ${entry.title} (parent hash ${entry.parentHash} not found)`);
79
+ }
80
+ }
81
+ else if (entry.level === 2) {
82
+ // H2 항목이지만 부모 해시가 없는 경우 (비정상)
83
+ reporter.warn(`Skipping H2 entry without parent: ${entry.title}`);
84
+ }
85
+ else {
86
+ // 부모 해시가 없지만 유효할 수 있는 항목
87
+ uniqueMap.set(entry.hash, entry);
88
+ }
53
89
  }
90
+ // 내부 일관성 검사: 동일한 제목의 항목이 여러 섹션에 있는 경우
91
+ const titleToSections = new Map();
92
+ for (const entry of uniqueMap.values()) {
93
+ if (entry.level === 2) {
94
+ const title = entry.title.toLowerCase();
95
+ if (!titleToSections.has(title)) {
96
+ titleToSections.set(title, new Set());
97
+ }
98
+ // 이 제목이 속한 부모 H1 추적
99
+ if (entry.parentHash) {
100
+ titleToSections.get(title).add(entry.parentHash);
101
+ }
102
+ }
103
+ }
104
+ // 여러 섹션에 동일한 제목의 H2가 있는지 확인하고, 영향받는 H2의 contextTitle을 더 강화
105
+ for (const [title, sections] of titleToSections.entries()) {
106
+ if (sections.size > 1) {
107
+ reporter.info(`Title "${title}" appears across ${sections.size} different sections. Enhancing context.`);
108
+ // 해당 제목의 모든 항목에 대해 상위 항목의 제목을 컨텍스트에 추가
109
+ for (const entry of uniqueMap.values()) {
110
+ if (entry.title.toLowerCase() === title && entry.parentHash) {
111
+ const parentEntry = h1Entries.get(entry.parentHash);
112
+ if (parentEntry) {
113
+ entry.contextTitle = `${entry.title} (${parentEntry.title})`;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ // 최종 결과를 배열로 변환
54
120
  processedToc = Array.from(uniqueMap.values());
55
- const removedCount = enrichedTocEntries.length - processedToc.length;
121
+ const removedCount = validatedToc.length - processedToc.length;
56
122
  const processTime = Date.now() - startTime;
57
123
  if (removedCount > 0) {
58
- reporter.info(`Removed ${removedCount} duplicate TOC entries in ${processTime}ms.`);
124
+ reporter.info(`Removed ${removedCount} duplicate/invalid TOC entries in ${processTime}ms.`);
59
125
  }
60
126
  }
127
+ // Add context to duplicate title entries for better display
128
+ if (enhanceDuplicates) {
129
+ (0, exports.enrichDuplicateTitles)(processedToc, reporter);
130
+ }
61
131
  // Limit size if necessary
62
132
  if (processedToc.length > maxSize) {
63
133
  reporter.warn(`Table of contents exceeds maximum size (${processedToc.length} > ${maxSize}). Truncating.`);
64
134
  processedToc = processedToc.slice(0, maxSize);
65
135
  }
136
+ // Build the hierarchical structure if requested
137
+ if (buildHierarchy) {
138
+ // First level sort - by level, then by approximate order
139
+ processedToc.sort((a, b) => {
140
+ if ((a.level || 0) !== (b.level || 0)) {
141
+ return (a.level || 0) - (b.level || 0);
142
+ }
143
+ return 0; // Keep original order for same level
144
+ });
145
+ // 확인을 위한 로깅
146
+ const h1Count = processedToc.filter((item) => item.level === 1).length;
147
+ const h2Count = processedToc.filter((item) => item.level === 2).length;
148
+ const h3Count = processedToc.filter((item) => item.level === 3).length;
149
+ reporter.info(`TOC structure: ${h1Count} h1, ${h2Count} h2, ${h3Count} h3 headings`);
150
+ }
66
151
  return processedToc;
67
152
  };
68
153
  exports.optimizeTocArray = optimizeTocArray;
154
+ /**
155
+ * TOC 구조의 유효성을 검사하고 문제가 있는 항목을 필터링
156
+ */
157
+ const validateTocStructure = (tocEntries, reporter) => {
158
+ if (!tocEntries || tocEntries.length === 0) {
159
+ return [];
160
+ }
161
+ // 1. 섹션별 매핑 구성
162
+ const h1Map = new Map();
163
+ const h1Children = new Map();
164
+ // 2. 모든 H1 항목 수집
165
+ for (const entry of tocEntries) {
166
+ if (entry.level === 1) {
167
+ h1Map.set(entry.hash, entry);
168
+ h1Children.set(entry.hash, new Set());
169
+ }
170
+ }
171
+ // H1이 없으면 원본 반환
172
+ if (h1Map.size === 0) {
173
+ return tocEntries;
174
+ }
175
+ // 3. 각 항목을 올바른 부모에 연결
176
+ for (const entry of tocEntries) {
177
+ if (entry.level !== 1 && entry.parentHash) {
178
+ // 부모가 H1인 경우
179
+ if (h1Map.has(entry.parentHash)) {
180
+ h1Children.get(entry.parentHash).add(entry.hash);
181
+ }
182
+ }
183
+ }
184
+ // 4. 각 H1 섹션 내에서 중복된 H2/H3 타이틀 발견 (같은 섹션에 동일 타이틀 중복)
185
+ const suspiciousEntries = new Set();
186
+ const sectionTitles = new Map();
187
+ for (const [h1Hash, childHashes] of h1Children.entries()) {
188
+ const titleCounts = new Map();
189
+ sectionTitles.set(h1Hash, titleCounts);
190
+ // 이 H1에 속한 모든 자식 항목 검사
191
+ for (const entry of tocEntries) {
192
+ if ((entry.level === 2 || entry.level === 3) &&
193
+ entry.parentHash === h1Hash) {
194
+ const title = entry.title.toLowerCase();
195
+ const count = (titleCounts.get(title) || 0) + 1;
196
+ titleCounts.set(title, count);
197
+ // 이 섹션에 이 제목이 이미 있으면 중복으로 표시
198
+ if (count > 1) {
199
+ suspiciousEntries.add(entry.hash);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ // 5. 다른 섹션에 속할 것으로 보이는 항목 식별 (예: 14. Squid 프록시 서버의 잘못된 자식)
205
+ const titleToSections = new Map();
206
+ for (const entry of tocEntries) {
207
+ if (entry.level === 2) {
208
+ const title = entry.title.toLowerCase();
209
+ if (!titleToSections.has(title)) {
210
+ titleToSections.set(title, new Set());
211
+ }
212
+ // 이 제목이 어떤 H1 섹션에 속하는지 기록
213
+ if (entry.parentHash) {
214
+ titleToSections.get(title).add(entry.parentHash);
215
+ }
216
+ }
217
+ }
218
+ // 여러 섹션에 동일한 제목의 항목이 있는지 검사
219
+ for (const [title, sections] of titleToSections.entries()) {
220
+ if (sections.size > 1) {
221
+ reporter.info(`Title "${title}" appears across ${sections.size} different sections.`);
222
+ // 일반적인 명령어 이름인지 확인 (cp, mv, rm 등)
223
+ const isCommonCommand = /^(cp|mv|rm|find|grep|curl|wget|ls|chmod|chown|ps|sed|awk|cat|service|file|locate|stat)$/.test(title);
224
+ if (isCommonCommand) {
225
+ reporter.info(`"${title}" is a common command name that appears in multiple sections.`);
226
+ // 각 섹션의 항목 분석
227
+ for (const entry of tocEntries) {
228
+ if (entry.title.toLowerCase() === title && entry.level === 2) {
229
+ // 이 H2 항목이 이 섹션의 내용과 일치하는지 확인할 방법이 필요
230
+ // 예: 이 H2와 연결된 H3 항목이 정당한지 검사
231
+ let hasValidChildren = false;
232
+ for (const child of tocEntries) {
233
+ if (child.level === 3 && child.parentHash === entry.hash) {
234
+ hasValidChildren = true;
235
+ break;
236
+ }
237
+ }
238
+ // 자식이 없는 중복된 명령어는 다른 섹션에서 복사된 것일 가능성이 높음
239
+ if (!hasValidChildren && sections.size > 1) {
240
+ suspiciousEntries.add(entry.hash);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ // 6. 의심스러운 항목 수 보고
248
+ if (suspiciousEntries.size > 0) {
249
+ reporter.info(`Found ${suspiciousEntries.size} suspicious TOC entries that might be duplicates`);
250
+ }
251
+ // 7. 의심스러운 항목 제거된 결과 반환
252
+ return tocEntries.filter((entry) => !suspiciousEntries.has(entry.hash));
253
+ };
254
+ exports.validateTocStructure = validateTocStructure;
69
255
  /**
70
256
  * Enhances TOC entries with contextual titles to disambiguate duplicate titles
71
257
  */
@@ -91,9 +277,9 @@ const enrichDuplicateTitles = (tocEntries, reporter) => {
91
277
  for (const entry of tocEntries) {
92
278
  entryMap.set(entry.hash, entry);
93
279
  }
94
- // Create a hierarchical map to find parents
280
+ // Process each entry to ensure all duplicate titles have context
95
281
  for (const entry of tocEntries) {
96
- if (duplicateTitles.has(entry.title)) {
282
+ if (duplicateTitles.has(entry.title) && !entry.contextTitle) {
97
283
  let context = "";
98
284
  let parentEntry;
99
285
  // Find parent context
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",
4
+ "version": "1.2.6",
5
5
  "skipLibCheck": true,
6
6
  "license": "0BSD",
7
7
  "main": "./dist/gatsby-node.js",