getmdfromleetcode 1.1.2 → 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))
@@ -43,7 +48,7 @@ const argv = yargs(hideBin(process.argv))
43
48
  alias: 'n',
44
49
  type: 'string',
45
50
  description: 'Username for specific solution author',
46
- default: 'endlesscheng' // 设置默认用户名为endlesscheng
51
+ default: 'endlesscheng'
47
52
  })
48
53
  .option('solutionOnly', {
49
54
  alias: 's',
@@ -51,13 +56,30 @@ const argv = yargs(hideBin(process.argv))
51
56
  description: 'Output solution only',
52
57
  default: false
53
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
+ })
54
76
  .help()
55
77
  .alias('help', 'h')
56
78
  .argv;
57
79
 
58
80
  // 主函数
59
81
  async function main() {
60
- const { url, english, raw, clipboard, username, solutionOnly } = argv;
82
+ const { url, english, raw, clipboard, username, solutionOnly, problemOnly, flag, output } = argv;
61
83
  const language = english ? 'english' : 'chinese';
62
84
  const rawOutput = raw;
63
85
 
@@ -69,28 +91,70 @@ async function main() {
69
91
  questionData = await fetchProblemDataViaGraphQL(url, language, username);
70
92
  } catch (apiError) {
71
93
  console.error('Failed to fetch data via GraphQL API, trying to fetch from page...', apiError.message);
72
- // 如果API获取失败,则尝试从页面获取数据
73
94
  questionData = await fetchProblemDataFromPage(url, language, username);
74
95
  }
75
96
 
76
97
  // 添加语言信息到 questionData
77
98
  questionData.isEnglish = english;
78
99
 
79
- // 根据solutionOnly参数决定输出内容
100
+ // 根据solutionOnly或problemOnly参数决定输出内容
80
101
  let outputContent;
81
- if (solutionOnly) {
102
+ if (solutionOnly && problemOnly) {
103
+ console.log('Cannot use both solutionOnly and problemOnly at the same time.');
104
+ return;
105
+ } else if (solutionOnly) {
82
106
  // 只输出题解
83
107
  if (questionData.solution) {
84
- outputContent = formatAsMarkdown(questionData, rawOutput, true); // 传递solutionOnly标志
108
+ outputContent = formatAsMarkdown(questionData, rawOutput, true);
85
109
  } else {
86
110
  console.log('No solution found for the given problem.');
87
111
  return;
88
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;
89
141
  } else {
90
142
  // 正常输出完整内容
91
143
  outputContent = formatAsMarkdown(questionData, rawOutput);
92
144
  }
93
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
+ }
157
+
94
158
  // 如果用户指定了复制到剪贴板的选项,则复制内容
95
159
  if (clipboard) {
96
160
  try {
@@ -112,11 +176,11 @@ async function main() {
112
176
  console.log('Content copied to clipboard successfully!');
113
177
  } catch (clipboardError) {
114
178
  console.error('Failed to copy content to clipboard:', clipboardError.message);
115
- // 即使复制到剪贴板失败,仍然输出内容
116
- console.log(outputContent);
117
179
  }
118
- } else {
119
- // 正常输出内容
180
+ }
181
+
182
+ // 如果没有指定输出文件或剪贴板,则输出到控制台
183
+ if (!output && !clipboard) {
120
184
  console.log(outputContent);
121
185
  }
122
186
  } catch (error) {
@@ -5,15 +5,15 @@
5
5
  import * as cheerio from 'cheerio';
6
6
 
7
7
  // 将题目内容格式化为Markdown
8
- function formatAsMarkdown(question, rawOutput = false, solutionOnly = 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, solutionOnly = 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,27 +98,120 @@ function formatAsMarkdown(question, rawOutput = false, solutionOnly = 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
- // 如果只输出题解
148
- if (solutionOnly) {
149
- if (solution && solution.content) {
150
- // 直接返回题解内容
151
- let solutionContent = solution.content || '';
152
- return solutionContent;
172
+ if (difficulty) {
173
+ markdown += `**Difficulty:** ${difficulty}\n\n`;
153
174
  } else {
154
- return '没有找到题解';
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`;
155
209
  }
210
+
211
+ return markdown;
156
212
  }
157
213
 
158
- // 构建Markdown输出
214
+ // 构建完整Markdown输出(包含题解)
159
215
  let markdown = '';
160
216
  if (title) {
161
217
  markdown += `# ${title}\n\n`;
@@ -175,12 +231,110 @@ function formatAsMarkdown(question, rawOutput = false, solutionOnly = false) {
175
231
  markdown += `**Tags:** ${tags}\n\n`;
176
232
  }
177
233
 
178
- 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
+
179
334
  // 格式化描述内容
180
335
  let formattedDescription = description;
181
336
 
182
337
  // 仅保留解释部分的处理
183
- // 处理解释部分
184
338
  formattedDescription = formattedDescription.replace(/解释:/g, '\n**解释:** ');
185
339
  formattedDescription = formattedDescription.replace(/Explanation:/g, '\n**Explanation:** ');
186
340
 
@@ -192,34 +346,290 @@ function formatAsMarkdown(question, rawOutput = false, solutionOnly = false) {
192
346
  const descriptionTitle = isEnglish ? "Description" : "题目描述";
193
347
  markdown += `## ${descriptionTitle}\n\n${formattedDescription}\n`;
194
348
  } else {
195
- // 根据语言显示不同的标题
196
349
  const descriptionTitle = isEnglish ? "Description" : "题目描述";
197
350
  markdown += `## ${descriptionTitle}\n\n未能提取到题目描述\n\n`;
198
351
  }
352
+
199
353
  // 添加题目来源
200
354
  if (url) {
201
355
  markdown += `\n\n## SOURCE\n\n[${title}](${url})\n\n`;
202
356
  }
203
- // 添加题解内容(如果存在)
204
- if (solution && solution.content) {
205
- markdown += '\n## 题解\n\n';
206
-
207
- // 直接使用题解的原始内容,不进行Markdown格式转换
208
- // 保留HTML格式并处理特殊字符
209
- let solutionContent = solution.content || '';
210
-
211
- // 处理HTML实体,但保留HTML标签结构
212
- // solutionContent = solutionContent.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
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
+ }
213
377
 
214
- // solutionContent = solutionContent.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
378
+ return markdown;
379
+ }
215
380
 
216
- markdown += solutionContent + '\n';
217
- } else {
218
- markdown += '\n## 题解\n\n';
219
- 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
+ }
220
400
  }
401
+
402
+ return false;
403
+ }
221
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
+
222
632
  return markdown;
223
633
  }
224
634
 
225
- export { formatAsMarkdown };
635
+ export { formatAsMarkdown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getmdfromleetcode",
3
- "version": "1.1.2",
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",