getmdfromleetcode 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - 支持多种编程语言的代码展示
14
14
  - 数学公式自动转换为 LaTeX 格式
15
15
  - 支持将生成内容复制到系统剪贴板
16
+ - 支持只输出题目内容或只输出题解内容
16
17
 
17
18
  ## 安装
18
19
 
@@ -62,6 +63,8 @@ getmdfromleetcode -u <leetcode-problem-url>
62
63
  - `-e, --english`: 切换到英文内容显示
63
64
  - `-r, --raw`: 输出原始 HTML 格式内容
64
65
  - `-c, --clipboard`: 将输出内容复制到系统剪贴板
66
+ - `-s, --solution-only`: 只输出题解内容
67
+ - `-p, --problem-only`: 只输出题目内容
65
68
 
66
69
  示例:
67
70
  ```bash
@@ -77,6 +80,12 @@ getmdfromleetcode -u https://leetcode.cn/problems/two-sum/ -r
77
80
  # 获取题目内容并复制到剪贴板
78
81
  getmdfromleetcode -u https://leetcode.cn/problems/two-sum/ -c
79
82
 
83
+ # 只获取题解内容
84
+ getmdfromleetcode -u https://leetcode.cn/problems/two-sum/ -s
85
+
86
+ # 只获取题目内容
87
+ getmdfromleetcode -u https://leetcode.cn/problems/two-sum/ -p
88
+
80
89
  # 组合使用多个参数
81
90
  getmdfromleetcode -u https://leetcode.cn/problems/two-sum/ -e -c
82
91
  ```
package/index.js CHANGED
@@ -11,7 +11,12 @@ import { hideBin } from 'yargs/helpers';
11
11
  import { fetchProblemDataViaGraphQL, fetchProblemDataFromPage } from './lib/problemDataFetcher.js';
12
12
  import { formatAsMarkdown } from './lib/markdownFormatter.js';
13
13
  import { execSync } from 'child_process';
14
- import { writeFileSync } from 'fs';
14
+ import { writeFileSync, readFileSync } from 'fs';
15
+ import { fileURLToPath } from 'url';
16
+ import { dirname, join } from 'path';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
15
20
 
16
21
  // 解析命令行参数
17
22
  const argv = yargs(hideBin(process.argv))
@@ -39,13 +44,42 @@ const argv = yargs(hideBin(process.argv))
39
44
  description: 'Copy output to clipboard',
40
45
  default: false
41
46
  })
47
+ .option('username', {
48
+ alias: 'n',
49
+ type: 'string',
50
+ description: 'Username for specific solution author',
51
+ default: 'endlesscheng'
52
+ })
53
+ .option('solutionOnly', {
54
+ alias: 's',
55
+ type: 'boolean',
56
+ description: 'Output solution only',
57
+ default: false
58
+ })
59
+ .option('problemOnly', {
60
+ alias: 'p',
61
+ type: 'boolean',
62
+ description: 'Output problem content only',
63
+ default: false
64
+ })
65
+ .option('flag', {
66
+ alias: 'f',
67
+ type: 'boolean',
68
+ description: 'Output tags and difficulty only',
69
+ default: false
70
+ })
71
+ .option('output', {
72
+ alias: 'o',
73
+ type: 'string',
74
+ description: 'Output file path'
75
+ })
42
76
  .help()
43
77
  .alias('help', 'h')
44
78
  .argv;
45
79
 
46
80
  // 主函数
47
81
  async function main() {
48
- const { url, english, raw, clipboard } = argv;
82
+ const { url, english, raw, clipboard, username, solutionOnly, problemOnly, flag, output } = argv;
49
83
  const language = english ? 'english' : 'chinese';
50
84
  const rawOutput = raw;
51
85
 
@@ -54,24 +88,78 @@ async function main() {
54
88
 
55
89
  // 首先尝试通过GraphQL API获取数据
56
90
  try {
57
- questionData = await fetchProblemDataViaGraphQL(url, language);
91
+ questionData = await fetchProblemDataViaGraphQL(url, language, username);
58
92
  } catch (apiError) {
59
93
  console.error('Failed to fetch data via GraphQL API, trying to fetch from page...', apiError.message);
60
- // 如果API获取失败,则尝试从页面获取数据
61
- questionData = await fetchProblemDataFromPage(url, language);
94
+ questionData = await fetchProblemDataFromPage(url, language, username);
62
95
  }
63
96
 
64
97
  // 添加语言信息到 questionData
65
98
  questionData.isEnglish = english;
66
99
 
67
- // 格式化为Markdown
68
- const markdownContent = formatAsMarkdown(questionData, rawOutput);
100
+ // 根据solutionOnly或problemOnly参数决定输出内容
101
+ let outputContent;
102
+ if (solutionOnly && problemOnly) {
103
+ console.log('Cannot use both solutionOnly and problemOnly at the same time.');
104
+ return;
105
+ } else if (solutionOnly) {
106
+ // 只输出题解
107
+ if (questionData.solution) {
108
+ outputContent = formatAsMarkdown(questionData, rawOutput, true);
109
+ } else {
110
+ console.log('No solution found for the given problem.');
111
+ return;
112
+ }
113
+ } else if (problemOnly) {
114
+ // 只输出题目内容
115
+ outputContent = formatAsMarkdown(questionData, rawOutput, false, true);
116
+ } else if (flag) {
117
+ // 只输出标签和难度
118
+ let result = '';
119
+
120
+ // 添加难度
121
+ if (questionData.difficulty) {
122
+ const difficultyLabel = questionData.isEnglish ? 'Difficulty: ' : '难度: ';
123
+ result += `${difficultyLabel}${questionData.difficulty}`;
124
+ }
125
+
126
+ // 添加标签
127
+ if (questionData.topicTags && questionData.topicTags.length > 0) {
128
+ if (result) result += '\n'; // 如果已经有内容了,添加换行
129
+ const tagsLabel = questionData.isEnglish ? 'Tags: ' : '标签: ';
130
+ const tags = questionData.topicTags.map(tag =>
131
+ questionData.isEnglish ? tag.name : (tag.translatedName || tag.name)
132
+ ).join(', ');
133
+ result += `${tagsLabel}${tags}`;
134
+ } else {
135
+ if (result) result += '\n'; // 如果已经有内容了,添加换行
136
+ const noTagsMessage = questionData.isEnglish ? 'No tags found.' : '未找到标签.';
137
+ result += noTagsMessage;
138
+ }
139
+
140
+ outputContent = result;
141
+ } else {
142
+ // 正常输出完整内容
143
+ outputContent = formatAsMarkdown(questionData, rawOutput);
144
+ }
145
+
146
+ // 如果用户指定了输出文件,则写入文件
147
+ if (output) {
148
+ try {
149
+ writeFileSync(output, outputContent, 'utf8');
150
+ console.log(`Content successfully written to: ${output}`);
151
+ } catch (fileError) {
152
+ console.error('Failed to write to file:', fileError.message);
153
+ // 即使写入文件失败,仍然输出内容
154
+ console.log(outputContent);
155
+ }
156
+ }
69
157
 
70
158
  // 如果用户指定了复制到剪贴板的选项,则复制内容
71
159
  if (clipboard) {
72
160
  try {
73
161
  // 将内容写入临时文件
74
- writeFileSync('/tmp/leetcode_content.txt', markdownContent);
162
+ writeFileSync('/tmp/leetcode_content.txt', outputContent);
75
163
 
76
164
  // 根据操作系统使用不同的命令复制到剪贴板
77
165
  if (process.platform === 'darwin') {
@@ -88,12 +176,12 @@ async function main() {
88
176
  console.log('Content copied to clipboard successfully!');
89
177
  } catch (clipboardError) {
90
178
  console.error('Failed to copy content to clipboard:', clipboardError.message);
91
- // 即使复制到剪贴板失败,仍然输出内容
92
- console.log(markdownContent);
93
179
  }
94
- } else {
95
- // 正常输出内容
96
- console.log(markdownContent);
180
+ }
181
+
182
+ // 如果没有指定输出文件或剪贴板,则输出到控制台
183
+ if (!output && !clipboard) {
184
+ console.log(outputContent);
97
185
  }
98
186
  } catch (error) {
99
187
  console.error('Error fetching or processing problem data:', error.message);
@@ -5,15 +5,15 @@
5
5
  import * as cheerio from 'cheerio';
6
6
 
7
7
  // 将题目内容格式化为Markdown
8
- function formatAsMarkdown(question, rawOutput = false) {
8
+ function formatAsMarkdown(question, rawOutput = false, solutionOnly = false, problemOnly = false) {
9
9
  const title = question.displayTitle;
10
10
  const difficulty = question.difficulty;
11
11
  const content = question.displayContent;
12
12
  const topicTags = question.topicTags || [];
13
13
  const solution = question.solution;
14
14
  const isEnglish = question.isEnglish || false;
15
- const url = question.url || ''; // 获取题目URL
16
- //console.log(question);
15
+ const url = question.url || '';
16
+
17
17
  // 如果需要原始输出,则返回包含标题和难度的原始HTML内容
18
18
  if (rawOutput) {
19
19
  let rawResult = '';
@@ -25,108 +25,71 @@ function formatAsMarkdown(question, rawOutput = false) {
25
25
  }
26
26
  rawResult += content;
27
27
 
28
- // 添加题解内容(如果存在)
29
- if (solution && solution.content) {
30
- rawResult += `\n\n<h2>题解</h2>\n`;
31
- rawResult += solution.content || '';
32
- } else {
33
- rawResult += `\n\n<h2>题解</h2>\n`;
34
- rawResult += `<p>没有找到题解</p>\n`;
28
+ // 如果不是只输出题目内容,才添加题解
29
+ if (!problemOnly) {
30
+ // 添加题解内容(如果存在)
31
+ if (solution && solution.content) {
32
+ rawResult += `\n\n<h2>题解</h2>\n`;
33
+ rawResult += solution.content || '';
34
+ } else {
35
+ rawResult += `\n\n<h2>题解</h2>\n`;
36
+ rawResult += `<p>没有找到题解</p>\n`;
37
+ }
35
38
  }
36
39
 
37
40
  return rawResult;
38
41
  }
39
42
 
40
- // 使用cheerio处理HTML格式的描述
41
- const $ = cheerio.load(content, { decodeEntities: false });
43
+ // 如果只输出题解
44
+ if (solutionOnly) {
45
+ if (solution && solution.content) {
46
+ // 检查 content 是否已经是 Markdown 格式
47
+ // 如果包含 Markdown 标记(如 ##, ```),则直接返回
48
+ if (isMarkdownContent(solution.content)) {
49
+ let result = '';
50
+ if (solution.title) {
51
+ result += `### ${solution.title}\n\n`;
52
+ }
53
+ result += solution.content;
54
+ return result;
55
+ } else {
56
+ // 使用 cheerio 处理题解的 HTML 内容
57
+ const $ = cheerio.load(solution.content, { decodeEntities: false });
58
+ return convertHtmlToMarkdown($, solution.title || '题解');
59
+ }
60
+ } else {
61
+ return '没有找到题解';
62
+ }
63
+ }
42
64
 
43
- // 提取文本内容并格式化
44
- let description = '';
65
+ // 如果只输出题目内容
66
+ if (problemOnly) {
67
+ // 使用cheerio处理HTML格式的描述
68
+ const $ = cheerio.load(content, { decodeEntities: false });
45
69
 
46
- // 按照HTML中的顺序处理所有元素
47
- $('body').children().each((i, elem) => {
48
- const $elem = $(elem);
70
+ // 提取文本内容并格式化
71
+ let description = '';
49
72
 
50
- if (elem.tagName === 'p') {
51
- // 处理段落中的内联元素
52
- let text = '';
53
- $elem.contents().each((j, child) => {
54
- const $child = $(child);
55
- if (child.type === 'text') {
56
- text += child.data;
57
- } else if (child.tagName === 'strong') {
58
- text += ` **${$child.text()}** `;
59
- } else if (child.tagName === 'em') {
60
- text += ` *${$child.text()}* `;
61
- } else if (child.tagName === 'code') {
62
- text += `\`${$child.text()}\``;
63
- } else if (child.tagName === 'sup') {
64
- // 处理上标(数学公式中的幂)
65
- text += `^{${$child.text()}}`;
66
- } else if (child.tagName === 'sub') {
67
- // 处理下标
68
- text += `_${$child.text()}`;
69
- } else {
70
- // 其他标签直接获取文本
71
- text += $child.text();
72
- }
73
- });
74
- text = text.trim();
75
- if (text) {
76
- // 特殊处理示例标题
77
- if (text.startsWith('**示例') || text.startsWith('**Example')) {
78
- // 去掉前后的**
79
- text = text.replace(/^\*\*(.*)\*\*$/, '$1');
80
- description += `\n\n## ${text}\n\n`;
81
- } else if (text.startsWith('**提示') || text.startsWith('**Hint')) {
82
- // 去掉前后的**
83
- text = text.replace(/^\*\*(.*)\*\*$/, '$1');
84
- description += `\n\n## ${text}\n`;
85
- } else if (text.startsWith('**进阶**')) {
86
- // 去掉前后的**
87
- text = text.replace(/^\*\*(.*)\*\*$/, '$1');
88
- description += `\n\n## ${text}\n`;
89
- } else {
90
- description += text + '\n\n';
91
- }
92
- }
93
- } else if (elem.tagName === 'pre') {
94
- const codeText = $elem.text();
95
- if (codeText) {
96
- description += '```\n' + codeText + '```\n\n';
97
- }
98
- } else if (elem.tagName === 'ul' || elem.tagName === 'ol') {
99
- $elem.children('li').each((j, li) => {
73
+ // 按照HTML中的顺序处理所有元素
74
+ $('body').children().each((i, elem) => {
75
+ const $elem = $(elem);
76
+
77
+ if (elem.tagName === 'p') {
78
+ // 处理段落中的内联元素
100
79
  let text = '';
101
- // 保留li元素中的所有内容,包括strong标签
102
- $(li).contents().each((k, child) => {
80
+ $elem.contents().each((j, child) => {
103
81
  const $child = $(child);
104
82
  if (child.type === 'text') {
105
83
  text += child.data;
106
84
  } else if (child.tagName === 'strong') {
107
85
  text += ` **${$child.text()}** `;
86
+ } else if (child.tagName === 'em') {
87
+ text += ` *${$child.text()}* `;
108
88
  } else if (child.tagName === 'code') {
109
- // 特殊处理code标签中的sup和sub标签
110
- let codeText = '';
111
- $child.contents().each((l, codeChild) => {
112
- if (codeChild.type === 'text') {
113
- codeText += codeChild.data;
114
- } else if (codeChild.tagName === 'sup') {
115
- // 处理上标(数学公式中的幂)
116
- console.log('处理code中的sup标签'); // 调试信息
117
- codeText += `^${$(codeChild).text()}`;
118
- } else if (codeChild.tagName === 'sub') {
119
- // 处理下标
120
- console.log('处理code中的sub标签'); // 调试信息
121
- codeText += `_${$(codeChild).text()}`;
122
- } else {
123
- codeText += $(codeChild).text();
124
- }
125
- });
126
- text += `\`${codeText}\``;
89
+ text += `\`${$child.text()}\``;
127
90
  } else if (child.tagName === 'sup') {
128
91
  // 处理上标(数学公式中的幂)
129
- text += `^${$child.text()}`;
92
+ text += `^{${$child.text()}}`;
130
93
  } else if (child.tagName === 'sub') {
131
94
  // 处理下标
132
95
  text += `_${$child.text()}`;
@@ -135,16 +98,120 @@ function formatAsMarkdown(question, rawOutput = false) {
135
98
  text += $child.text();
136
99
  }
137
100
  });
138
- if (text.trim()) {
139
- // 处理列表项中的数学表达式
140
- description += `- ${text.trim()}\n`;
101
+ text = text.trim();
102
+ if (text) {
103
+ // 特殊处理示例标题
104
+ if (text.startsWith('**示例') || text.startsWith('**Example')) {
105
+ // 去掉前后的**
106
+ text = text.replace(/^\*\*(.*)\*\*$/, '$1');
107
+ description += `\n\n## ${text}\n\n`;
108
+ } else if (text.startsWith('**提示') || text.startsWith('**Hint')) {
109
+ // 去掉前后的**
110
+ text = text.replace(/^\*\*(.*)\*\*$/, '$1');
111
+ description += `\n\n## ${text}\n`;
112
+ } else if (text.startsWith('**进阶**')) {
113
+ // 去掉前后的**
114
+ text = text.replace(/^\*\*(.*)\*\*$/, '$1');
115
+ description += `\n\n## ${text}\n`;
116
+ } else {
117
+ description += text + '\n\n';
118
+ }
141
119
  }
142
- });
143
- description += '\n';
120
+ } else if (elem.tagName === 'pre') {
121
+ const codeText = $elem.text();
122
+ if (codeText) {
123
+ description += '```\n' + codeText + '```\n\n';
124
+ }
125
+ } else if (elem.tagName === 'ul' || elem.tagName === 'ol') {
126
+ $elem.children('li').each((j, li) => {
127
+ let text = '';
128
+ // 保留li元素中的所有内容,包括strong标签
129
+ $(li).contents().each((k, child) => {
130
+ const $child = $(child);
131
+ if (child.type === 'text') {
132
+ text += child.data;
133
+ } else if (child.tagName === 'strong') {
134
+ text += ` **${$child.text()}** `;
135
+ } else if (child.tagName === 'code') {
136
+ // 特殊处理code标签中的sup和sub标签
137
+ let codeText = '';
138
+ $child.contents().each((l, codeChild) => {
139
+ if (codeChild.type === 'text') {
140
+ codeText += codeChild.data;
141
+ } else if (codeChild.tagName === 'sup') {
142
+ codeText += `^${$(codeChild).text()}`;
143
+ } else if (codeChild.tagName === 'sub') {
144
+ codeText += `_${$(codeChild).text()}`;
145
+ } else {
146
+ codeText += $(codeChild).text();
147
+ }
148
+ });
149
+ text += `\`${codeText}\``;
150
+ } else if (child.tagName === 'sup') {
151
+ text += `^${$child.text()}`;
152
+ } else if (child.tagName === 'sub') {
153
+ text += `_${$child.text()}`;
154
+ } else {
155
+ text += $child.text();
156
+ }
157
+ });
158
+ if (text.trim()) {
159
+ description += `- ${text.trim()}\n`;
160
+ }
161
+ });
162
+ description += '\n';
163
+ }
164
+ });
165
+
166
+ // 构建Markdown输出,只包含题目相关信息
167
+ let markdown = '';
168
+ if (title) {
169
+ markdown += `# ${title}\n\n`;
144
170
  }
145
- });
146
171
 
147
- // 构建Markdown输出
172
+ if (difficulty) {
173
+ markdown += `**Difficulty:** ${difficulty}\n\n`;
174
+ } else {
175
+ markdown += `**Difficulty:** 未找到\n\n`;
176
+ }
177
+
178
+ // 添加题目标签
179
+ if (topicTags.length > 0) {
180
+ const tags = topicTags.map(tag =>
181
+ tag.translatedName || tag.name
182
+ ).join(', ');
183
+ markdown += `**Tags:** ${tags}\n\n`;
184
+ }
185
+
186
+ if (description) {
187
+ // 格式化描述内容
188
+ let formattedDescription = description;
189
+
190
+ // 仅保留解释部分的处理
191
+ formattedDescription = formattedDescription.replace(/解释:/g, '\n**解释:** ');
192
+ formattedDescription = formattedDescription.replace(/Explanation:/g, '\n**Explanation:** ');
193
+
194
+ // 清理多余的空白字符
195
+ formattedDescription = formattedDescription.replace(/\n\s*\n\s*\n/g, '\n\n');
196
+ formattedDescription = formattedDescription.replace(/^ +/gm, '');
197
+
198
+ // 根据语言显示不同的标题
199
+ const descriptionTitle = isEnglish ? "Description" : "题目描述";
200
+ markdown += `## ${descriptionTitle}\n\n${formattedDescription}\n`;
201
+ } else {
202
+ const descriptionTitle = isEnglish ? "Description" : "题目描述";
203
+ markdown += `## ${descriptionTitle}\n\n未能提取到题目描述\n\n`;
204
+ }
205
+
206
+ // 添加题目来源
207
+ if (url) {
208
+ markdown += `\n\n## SOURCE\n\n[${title}](${url})\n\n`;
209
+ }
210
+
211
+ return markdown;
212
+ }
213
+
214
+ // 构建完整Markdown输出(包含题解)
148
215
  let markdown = '';
149
216
  if (title) {
150
217
  markdown += `# ${title}\n\n`;
@@ -164,12 +231,110 @@ function formatAsMarkdown(question, rawOutput = false) {
164
231
  markdown += `**Tags:** ${tags}\n\n`;
165
232
  }
166
233
 
167
- if (description) {
234
+ if (content) {
235
+ // 使用cheerio处理HTML格式的描述
236
+ const $ = cheerio.load(content, { decodeEntities: false });
237
+
238
+ // 提取文本内容并格式化
239
+ let description = '';
240
+
241
+ // 按照HTML中的顺序处理所有元素
242
+ $('body').children().each((i, elem) => {
243
+ const $elem = $(elem);
244
+
245
+ if (elem.tagName === 'p') {
246
+ // 处理段落中的内联元素
247
+ let text = '';
248
+ $elem.contents().each((j, child) => {
249
+ const $child = $(child);
250
+ if (child.type === 'text') {
251
+ text += child.data;
252
+ } else if (child.tagName === 'strong') {
253
+ text += ` **${$child.text()}** `;
254
+ } else if (child.tagName === 'em') {
255
+ text += ` *${$child.text()}* `;
256
+ } else if (child.tagName === 'code') {
257
+ text += `\`${$child.text()}\``;
258
+ } else if (child.tagName === 'sup') {
259
+ // 处理上标(数学公式中的幂)
260
+ text += `^{${$child.text()}}`;
261
+ } else if (child.tagName === 'sub') {
262
+ // 处理下标
263
+ text += `_${$child.text()}`;
264
+ } else {
265
+ // 其他标签直接获取文本
266
+ text += $child.text();
267
+ }
268
+ });
269
+ text = text.trim();
270
+ if (text) {
271
+ // 特殊处理示例标题
272
+ if (text.startsWith('**示例') || text.startsWith('**Example')) {
273
+ // 去掉前后的**
274
+ text = text.replace(/^\*\*(.*)\*\*$/, '$1');
275
+ description += `\n\n## ${text}\n\n`;
276
+ } else if (text.startsWith('**提示') || text.startsWith('**Hint')) {
277
+ // 去掉前后的**
278
+ text = text.replace(/^\*\*(.*)\*\*$/, '$1');
279
+ description += `\n\n## ${text}\n`;
280
+ } else if (text.startsWith('**进阶**')) {
281
+ // 去掉前后的**
282
+ text = text.replace(/^\*\*(.*)\*\*$/, '$1');
283
+ description += `\n\n## ${text}\n`;
284
+ } else {
285
+ description += text + '\n\n';
286
+ }
287
+ }
288
+ } else if (elem.tagName === 'pre') {
289
+ const codeText = $elem.text();
290
+ if (codeText) {
291
+ description += '```\n' + codeText + '```\n\n';
292
+ }
293
+ } else if (elem.tagName === 'ul' || elem.tagName === 'ol') {
294
+ $elem.children('li').each((j, li) => {
295
+ let text = '';
296
+ // 保留li元素中的所有内容,包括strong标签
297
+ $(li).contents().each((k, child) => {
298
+ const $child = $(child);
299
+ if (child.type === 'text') {
300
+ text += child.data;
301
+ } else if (child.tagName === 'strong') {
302
+ text += ` **${$child.text()}** `;
303
+ } else if (child.tagName === 'code') {
304
+ // 特殊处理code标签中的sup和sub标签
305
+ let codeText = '';
306
+ $child.contents().each((l, codeChild) => {
307
+ if (codeChild.type === 'text') {
308
+ codeText += codeChild.data;
309
+ } else if (codeChild.tagName === 'sup') {
310
+ codeText += `^${$(codeChild).text()}`;
311
+ } else if (codeChild.tagName === 'sub') {
312
+ codeText += `_${$(codeChild).text()}`;
313
+ } else {
314
+ codeText += $(codeChild).text();
315
+ }
316
+ });
317
+ text += `\`${codeText}\``;
318
+ } else if (child.tagName === 'sup') {
319
+ text += `^${$child.text()}`;
320
+ } else if (child.tagName === 'sub') {
321
+ text += `_${$child.text()}`;
322
+ } else {
323
+ text += $child.text();
324
+ }
325
+ });
326
+ if (text.trim()) {
327
+ description += `- ${text.trim()}\n`;
328
+ }
329
+ });
330
+ description += '\n';
331
+ }
332
+ });
333
+
168
334
  // 格式化描述内容
169
335
  let formattedDescription = description;
170
336
 
171
337
  // 仅保留解释部分的处理
172
- // 处理解释部分
173
338
  formattedDescription = formattedDescription.replace(/解释:/g, '\n**解释:** ');
174
339
  formattedDescription = formattedDescription.replace(/Explanation:/g, '\n**Explanation:** ');
175
340
 
@@ -181,32 +346,290 @@ function formatAsMarkdown(question, rawOutput = false) {
181
346
  const descriptionTitle = isEnglish ? "Description" : "题目描述";
182
347
  markdown += `## ${descriptionTitle}\n\n${formattedDescription}\n`;
183
348
  } else {
184
- // 根据语言显示不同的标题
185
349
  const descriptionTitle = isEnglish ? "Description" : "题目描述";
186
350
  markdown += `## ${descriptionTitle}\n\n未能提取到题目描述\n\n`;
187
351
  }
352
+
188
353
  // 添加题目来源
189
354
  if (url) {
190
355
  markdown += `\n\n## SOURCE\n\n[${title}](${url})\n\n`;
191
356
  }
192
- // 添加题解内容(如果存在)
193
- if (solution && solution.content) {
194
- markdown += '\n## 题解\n\n';
195
-
196
- // 直接使用题解的原始内容,不进行Markdown格式转换
197
- // 保留HTML格式并处理特殊字符
198
- let solutionContent = solution.content || '';
357
+
358
+ // 添加题解内容(如果存在且不是只输出题目内容)
359
+ if (!problemOnly) {
360
+ if (solution && solution.content) {
361
+ markdown += '\n## 题解\n\n';
362
+ // 检查 content 是否已经是 Markdown 格式
363
+ if (isMarkdownContent(solution.content)) {
364
+ if (solution.title) {
365
+ markdown += `### ${solution.title}\n\n`;
366
+ }
367
+ markdown += solution.content + '\n';
368
+ } else {
369
+ const $ = cheerio.load(solution.content, { decodeEntities: false });
370
+ markdown += convertHtmlToMarkdown($, solution.title || '题解') + '\n';
371
+ }
372
+ } else {
373
+ markdown += '\n## 题解\n\n';
374
+ markdown += '没有找到题解\n\n';
375
+ }
376
+ }
199
377
 
200
- // 处理HTML实体,但保留HTML标签结构
201
- // solutionContent = solutionContent.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
378
+ return markdown;
379
+ }
202
380
 
203
- markdown += solutionContent + '\n';
204
- } else {
205
- markdown += '\n## 题解\n\n';
206
- markdown += '没有找到题解\n\n';
381
+ // 检查内容是否已经是 Markdown 格式
382
+ function isMarkdownContent(content) {
383
+ // 检查是否包含常见的 Markdown 标记
384
+ const markdownPatterns = [
385
+ /^#{1,6}\s+/m, // 标题
386
+ /\`\`\`/, // 代码块
387
+ /\[.+\]\(.+\)/, // 链接
388
+ /\*\*.+\*\*/, // 粗体
389
+ /\*.+\*/, // 斜体
390
+ /^-\s+/m, // 无序列表
391
+ /^\d+\.\s+/m, // 有序列表
392
+ /^\> /m, // 引用
393
+ /\$\$?.+\$\$?/ // 数学公式
394
+ ];
395
+
396
+ for (const pattern of markdownPatterns) {
397
+ if (pattern.test(content)) {
398
+ return true;
399
+ }
207
400
  }
401
+
402
+ return false;
403
+ }
208
404
 
405
+ // 将 HTML 内容转换为 Markdown
406
+ function convertHtmlToMarkdown($, title = '') {
407
+ let markdown = '';
408
+
409
+ // 如果有标题,添加标题
410
+ if (title) {
411
+ markdown += `### ${title}\n\n`;
412
+ }
413
+
414
+ // 递归处理每个元素
415
+ function processElement(elem) {
416
+ const $elem = $(elem);
417
+ const tagName = elem.tagName?.toLowerCase() || '';
418
+
419
+ // 跳过注释和脚本
420
+ if (elem.type === 'comment' || tagName === 'script' || tagName === 'style') {
421
+ return '';
422
+ }
423
+
424
+ let result = '';
425
+
426
+ switch (tagName) {
427
+ case 'h1':
428
+ case 'h2':
429
+ case 'h3':
430
+ case 'h4':
431
+ case 'h5':
432
+ case 'h6':
433
+ const level = parseInt(tagName.charAt(1));
434
+ result = '\n' + '#'.repeat(level) + ' ' + processInlineElements($elem).trim() + '\n\n';
435
+ break;
436
+
437
+ case 'p':
438
+ result = processInlineElements($elem) + '\n\n';
439
+ break;
440
+
441
+ case 'strong':
442
+ case 'b':
443
+ result = '**' + processInlineElements($elem) + '**';
444
+ break;
445
+
446
+ case 'em':
447
+ case 'i':
448
+ result = '*' + processInlineElements($elem) + '*';
449
+ break;
450
+
451
+ case 'code':
452
+ const codeText = $elem.text();
453
+ // 检查是否是多行代码块
454
+ if (codeText.includes('\n')) {
455
+ result = '```\n' + codeText + '\n```\n\n';
456
+ } else {
457
+ result = '`' + codeText + '`';
458
+ }
459
+ break;
460
+
461
+ case 'pre':
462
+ // 处理代码块
463
+ const $code = $elem.find('code').first();
464
+ const langClass = $code.attr('class') || '';
465
+ const langMatch = langClass.match(/language-(\w+)/);
466
+ const lang = langMatch ? langMatch[1] : '';
467
+ const preContent = $code.length > 0 ? $code.text() : $elem.text();
468
+ result = `\`\`\`${lang}\n${preContent}\n\`\`\`\n\n`;
469
+ break;
470
+
471
+ case 'ul':
472
+ $elem.children('li').each((i, li) => {
473
+ result += '- ' + processInlineElements($(li)) + '\n';
474
+ });
475
+ result += '\n';
476
+ break;
477
+
478
+ case 'ol':
479
+ $elem.children('li').each((i, li) => {
480
+ result += `${i + 1}. ` + processInlineElements($(li)) + '\n';
481
+ });
482
+ result += '\n';
483
+ break;
484
+
485
+ case 'li':
486
+ result = processInlineElements($elem) + '\n';
487
+ break;
488
+
489
+ case 'a':
490
+ const href = $elem.attr('href') || '';
491
+ const linkText = processInlineElements($elem);
492
+ result = `[${linkText}](${href})`;
493
+ break;
494
+
495
+ case 'blockquote':
496
+ result = '> ' + processInlineElements($elem).replace(/\n/g, '\n> ') + '\n\n';
497
+ break;
498
+
499
+ case 'br':
500
+ result = '\n';
501
+ break;
502
+
503
+ case 'hr':
504
+ result = '\n---\n\n';
505
+ break;
506
+
507
+ case 'table':
508
+ result = '\n' + processTable($elem) + '\n';
509
+ break;
510
+
511
+ case 'img':
512
+ const src = $elem.attr('src') || '';
513
+ const alt = $elem.attr('alt') || '';
514
+ result = `![${alt}](${src})\n\n`;
515
+ break;
516
+
517
+ case 'div':
518
+ case 'section':
519
+ case 'article':
520
+ case 'span':
521
+ $elem.contents().each((i, child) => {
522
+ if (child.type === 'text') {
523
+ const text = child.data.trim();
524
+ if (text) {
525
+ result += text + ' ';
526
+ }
527
+ } else if (child.tagName) {
528
+ result += processElement(child);
529
+ }
530
+ });
531
+ if (result.trim()) {
532
+ result += '\n\n';
533
+ }
534
+ break;
535
+
536
+ default:
537
+ // 处理其他标签,递归处理其子元素
538
+ $elem.contents().each((i, child) => {
539
+ if (child.type === 'text') {
540
+ result += child.data;
541
+ } else if (child.tagName) {
542
+ result += processElement(child);
543
+ }
544
+ });
545
+ break;
546
+ }
547
+
548
+ return result;
549
+ }
550
+
551
+ // 处理内联元素
552
+ function processInlineElements($elem) {
553
+ let result = '';
554
+ $elem.contents().each((i, child) => {
555
+ if (child.type === 'text') {
556
+ result += child.data;
557
+ } else if (child.tagName) {
558
+ const tagName = child.tagName?.toLowerCase() || '';
559
+ if (['sup', 'sub'].includes(tagName)) {
560
+ const content = processInlineElements($(child));
561
+ result += tagName === 'sup' ? `^${content}` : `_${content}`;
562
+ } else {
563
+ result += processElement(child);
564
+ }
565
+ }
566
+ });
567
+ return result;
568
+ }
569
+
570
+ // 处理表格
571
+ function processTable($table) {
572
+ let tableMarkdown = '';
573
+
574
+ // 收集所有行
575
+ const rows = [];
576
+
577
+ // 处理表头
578
+ const $thead = $table.find('thead').first();
579
+ if ($thead.length) {
580
+ $thead.find('tr').each((i, tr) => {
581
+ const $tr = $(tr);
582
+ const $cells = $tr.find('th');
583
+ if ($cells.length) {
584
+ rows.push({
585
+ cells: $cells.map((j, cell) => processInlineElements($(cell))).get(),
586
+ isHeader: true
587
+ });
588
+ }
589
+ });
590
+ }
591
+
592
+ // 处理表体
593
+ const $tbody = $table.find('tbody').first();
594
+ const $rows = $tbody.length ? $tbody.find('tr') : $table.find('tr');
595
+ $rows.each((i, tr) => {
596
+ const $tr = $(tr);
597
+ const $cells = $tr.find('td, th');
598
+ if ($cells.length) {
599
+ rows.push({
600
+ cells: $cells.map((j, cell) => processInlineElements($(cell))).get(),
601
+ isHeader: false
602
+ });
603
+ }
604
+ });
605
+
606
+ if (rows.length > 0) {
607
+ // 添加表头行
608
+ const headerRow = rows[0];
609
+ tableMarkdown += '| ' + headerRow.cells.join(' | ') + ' |\n';
610
+
611
+ // 添加分隔行
612
+ tableMarkdown += '| ' + headerRow.cells.map(() => '---').join(' | ') + ' |\n';
613
+
614
+ // 添加数据行
615
+ for (let i = headerRow.isHeader ? 1 : 0; i < rows.length; i++) {
616
+ const row = rows[i];
617
+ tableMarkdown += '| ' + row.cells.join(' | ') + ' |\n';
618
+ }
619
+ }
620
+
621
+ return tableMarkdown;
622
+ }
623
+
624
+ // 处理 body 中的所有内容
625
+ $('body').contents().each((i, elem) => {
626
+ markdown += processElement(elem);
627
+ });
628
+
629
+ // 清理多余的空行
630
+ markdown = markdown.replace(/\n{3,}/g, '\n\n');
631
+
209
632
  return markdown;
210
633
  }
211
634
 
212
- export { formatAsMarkdown };
635
+ export { formatAsMarkdown };
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  // 使用GraphQL API获取LeetCode题目数据
6
- async function fetchProblemDataViaGraphQL(url, language = 'chinese') {
6
+ async function fetchProblemDataViaGraphQL(url, language = 'chinese', username = null) {
7
7
  // 从URL中提取题目slug
8
8
  const match = url.match(/\/problems\/([^/]+)/);
9
9
  const slug = match ? match[1] : null;
@@ -35,8 +35,8 @@ async function fetchProblemDataViaGraphQL(url, language = 'chinese') {
35
35
  `;
36
36
 
37
37
  const solutionQuery = `
38
- query solutionData($slug: String!) {
39
- questionSolutionArticles(first: 1, questionSlug: $slug) {
38
+ query solutionData($slug: String!, $first: Int) {
39
+ questionSolutionArticles(first: $first, questionSlug: $slug) {
40
40
  edges {
41
41
  node {
42
42
  title
@@ -85,7 +85,7 @@ async function fetchProblemDataViaGraphQL(url, language = 'chinese') {
85
85
  },
86
86
  body: JSON.stringify({
87
87
  query: solutionQuery,
88
- variables: { slug: slug }
88
+ variables: { slug: slug, first: 20 } // 增加数量以查找特定用户
89
89
  })
90
90
  });
91
91
 
@@ -114,7 +114,23 @@ async function fetchProblemDataViaGraphQL(url, language = 'chinese') {
114
114
  if (solutionData && solutionData.data && solutionData.data.questionSolutionArticles &&
115
115
  solutionData.data.questionSolutionArticles.edges &&
116
116
  solutionData.data.questionSolutionArticles.edges.length > 0) {
117
- question.solution = solutionData.data.questionSolutionArticles.edges[0].node;
117
+
118
+ if (username) {
119
+ // 尝试查找指定用户名的题解
120
+ const userSolution = solutionData.data.questionSolutionArticles.edges.find(edge =>
121
+ edge.node.author && edge.node.author.username === username
122
+ );
123
+
124
+ // 如果找到了指定用户的题解,则使用它;否则使用第一个题解
125
+ if (userSolution) {
126
+ question.solution = userSolution.node;
127
+ } else if (solutionData.data.questionSolutionArticles.edges.length > 0) {
128
+ question.solution = solutionData.data.questionSolutionArticles.edges[0].node;
129
+ }
130
+ } else {
131
+ // 如果没有指定用户名,则使用第一个题解
132
+ question.solution = solutionData.data.questionSolutionArticles.edges[0].node;
133
+ }
118
134
  }
119
135
 
120
136
  // 添加URL到question对象
@@ -124,7 +140,7 @@ async function fetchProblemDataViaGraphQL(url, language = 'chinese') {
124
140
  }
125
141
 
126
142
  // 从页面的__NEXT_DATA__中提取题目数据
127
- async function fetchProblemDataFromPage(url, language = 'chinese') {
143
+ async function fetchProblemDataFromPage(url, language = 'chinese', username = null) {
128
144
  // 从URL中提取题目slug
129
145
  const match = url.match(/\/problems\/([^/]+)/);
130
146
  const slug = match ? match[1] : null;
@@ -183,7 +199,7 @@ async function fetchProblemDataFromPage(url, language = 'chinese') {
183
199
 
184
200
  // 单独获取题解数据,因为页面中可能不包含题解数据
185
201
  try {
186
- const solutionData = await fetchSolutionData(slug);
202
+ const solutionData = await fetchSolutionData(slug, username);
187
203
  if (solutionData) {
188
204
  question.solution = solutionData;
189
205
  }
@@ -201,12 +217,12 @@ async function fetchProblemDataFromPage(url, language = 'chinese') {
201
217
  }
202
218
 
203
219
  // 单独获取题解数据
204
- async function fetchSolutionData(slug) {
220
+ async function fetchSolutionData(slug, username = null) {
205
221
  const graphqlUrl = 'https://leetcode.cn/graphql';
206
222
 
207
223
  const solutionQuery = `
208
- query solutionData($slug: String!) {
209
- questionSolutionArticles(first: 1, questionSlug: $slug) {
224
+ query solutionData($slug: String!, $first: Int) {
225
+ questionSolutionArticles(first: $first, questionSlug: $slug) {
210
226
  edges {
211
227
  node {
212
228
  title
@@ -230,7 +246,7 @@ async function fetchSolutionData(slug) {
230
246
  },
231
247
  body: JSON.stringify({
232
248
  query: solutionQuery,
233
- variables: { slug: slug }
249
+ variables: { slug: slug, first: 20 } // 增加数量以查找特定用户
234
250
  })
235
251
  });
236
252
 
@@ -247,7 +263,23 @@ async function fetchSolutionData(slug) {
247
263
  if (solutionData.data && solutionData.data.questionSolutionArticles &&
248
264
  solutionData.data.questionSolutionArticles.edges &&
249
265
  solutionData.data.questionSolutionArticles.edges.length > 0) {
250
- return solutionData.data.questionSolutionArticles.edges[0].node;
266
+
267
+ if (username) {
268
+ // 尝试查找指定用户名的题解
269
+ const userSolution = solutionData.data.questionSolutionArticles.edges.find(edge =>
270
+ edge.node.author && edge.node.author.username === username
271
+ );
272
+
273
+ // 如果找到了指定用户的题解,则使用它;否则使用第一个题解
274
+ if (userSolution) {
275
+ return userSolution.node;
276
+ } else {
277
+ return solutionData.data.questionSolutionArticles.edges[0].node;
278
+ }
279
+ } else {
280
+ // 如果没有指定用户名,则返回第一个题解
281
+ return solutionData.data.questionSolutionArticles.edges[0].node;
282
+ }
251
283
  }
252
284
 
253
285
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getmdfromleetcode",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "A command line tool to fetch LeetCode problem content and convert it to Markdown",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -10,7 +10,13 @@
10
10
  "scripts": {
11
11
  "start": "node index.js"
12
12
  },
13
- "keywords": ["leetcode", "markdown", "cli", "solution", "algorithm"],
13
+ "keywords": [
14
+ "leetcode",
15
+ "markdown",
16
+ "cli",
17
+ "solution",
18
+ "algorithm"
19
+ ],
14
20
  "author": "User",
15
21
  "license": "MIT",
16
22
  "dependencies": {
@@ -28,4 +34,4 @@
28
34
  "engines": {
29
35
  "node": ">=14.0.0"
30
36
  }
31
- }
37
+ }