solo-doc 0.1.0 → 0.2.0
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 +41 -0
- package/dist/bin/solo-doc.js +53 -18
- package/dist/src/ai/OllamaClient.js +59 -0
- package/dist/src/commands/VSCommand.js +157 -0
- package/dist/src/utils/TocExtractor.js +33 -0
- package/dist/src/utils/filename.js +18 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -71,6 +71,47 @@ solo-doc "http://10.1.2.3/docs/index.html" --type acp
|
|
|
71
71
|
| `--limit <number>` | 限制爬取的页面数量 (用于测试/调试)。 | 无限制 |
|
|
72
72
|
| `--no-headless` | 在可见模式下运行浏览器 (仅限 ACP,用于调试)。 | Headless (无头模式) |
|
|
73
73
|
|
|
74
|
+
## 🧪 实验性功能 (Beta)
|
|
75
|
+
|
|
76
|
+
### 🤖 文档对比 (AI VS 模式)
|
|
77
|
+
|
|
78
|
+
使用本地 AI 模型对比两个文档的内容差异和结构差异。此功能处于 Beta 阶段。
|
|
79
|
+
|
|
80
|
+
> **⚠️ 前置要求**:
|
|
81
|
+
> 1. 本地已安装并运行 [Ollama](https://ollama.com/)。
|
|
82
|
+
> 2. 已拉取所需的模型(推荐 `qwen3-vl:8b` 或类似多模态/大文本模型)。
|
|
83
|
+
> 3. 确保 Ollama 服务监听在 `http://127.0.0.1:11434`。
|
|
84
|
+
|
|
85
|
+
#### 用法
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
solo-doc vs <baseline-url> <target-url> [options]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### 示例
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 对比 OpenShift 和 Alauda 的文档
|
|
95
|
+
solo-doc vs \
|
|
96
|
+
"https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/building_applications/index" \
|
|
97
|
+
"https://docs.alauda.io/container_platform/4.2/developer/building_application/index.html" \
|
|
98
|
+
--model qwen3-vl:8b
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
此命令将按顺序执行:
|
|
102
|
+
1. **自动爬取**: 分别爬取两个 URL 并保存为 Markdown 文件(如果已存在则跳过)。
|
|
103
|
+
2. **提取目录**: 提取两个文档的目录树结构。
|
|
104
|
+
3. **AI 分析**: 调用本地 Ollama 模型,根据 `solo-doc-prompt.md` 定义的提示词进行两步分析:
|
|
105
|
+
- 生成 `vs-result.md`: 详细的内容与结构差异分析。
|
|
106
|
+
- 生成 `vs-tree.md`: 包含差异标注的合并目录树。
|
|
107
|
+
|
|
108
|
+
#### VS 模式选项
|
|
109
|
+
|
|
110
|
+
| 选项 | 描述 | 默认值 |
|
|
111
|
+
|--------|-------------|---------|
|
|
112
|
+
| `--model <name>` | 指定使用的 Ollama 模型名称。 | `qwen3-vl:8b` |
|
|
113
|
+
| `-f, --force` | 强制重新爬取文档,即使文件已存在。 | false |
|
|
114
|
+
|
|
74
115
|
## ✅ 环境要求
|
|
75
116
|
|
|
76
117
|
- Node.js >= 20
|
package/dist/bin/solo-doc.js
CHANGED
|
@@ -9,18 +9,59 @@ const CrawlerContext_1 = require("../src/CrawlerContext");
|
|
|
9
9
|
const OCPStrategy_1 = require("../src/strategies/OCPStrategy");
|
|
10
10
|
const ACPStrategy_1 = require("../src/strategies/ACPStrategy");
|
|
11
11
|
const StrategyDetector_1 = require("../src/utils/StrategyDetector");
|
|
12
|
+
const filename_1 = require("../src/utils/filename");
|
|
13
|
+
const VSCommand_1 = require("../src/commands/VSCommand");
|
|
12
14
|
const chalk_1 = __importDefault(require("chalk"));
|
|
13
15
|
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
17
|
const program = new commander_1.Command();
|
|
18
|
+
const getPackageVersion = () => {
|
|
19
|
+
try {
|
|
20
|
+
// Try production path first (dist/bin -> root)
|
|
21
|
+
const prodPath = path_1.default.resolve(__dirname, '../../package.json');
|
|
22
|
+
if (fs_1.default.existsSync(prodPath)) {
|
|
23
|
+
return JSON.parse(fs_1.default.readFileSync(prodPath, 'utf-8')).version;
|
|
24
|
+
}
|
|
25
|
+
// Try dev path (bin -> root)
|
|
26
|
+
const devPath = path_1.default.resolve(__dirname, '../package.json');
|
|
27
|
+
if (fs_1.default.existsSync(devPath)) {
|
|
28
|
+
return JSON.parse(fs_1.default.readFileSync(devPath, 'utf-8')).version;
|
|
29
|
+
}
|
|
30
|
+
return '1.0.0';
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return '1.0.0';
|
|
34
|
+
}
|
|
35
|
+
};
|
|
15
36
|
program
|
|
16
37
|
.name('solo-doc')
|
|
17
38
|
.description('CLI to crawl documentation sites and convert to single Markdown file')
|
|
18
|
-
.version(
|
|
39
|
+
.version(getPackageVersion());
|
|
40
|
+
// VS Command
|
|
41
|
+
program
|
|
42
|
+
.command('vs')
|
|
43
|
+
.description('Compare two documentation sites using AI (Beta)')
|
|
44
|
+
.argument('<baseline>', 'Baseline documentation URL')
|
|
45
|
+
.argument('<target>', 'Target documentation URL')
|
|
46
|
+
.option('--model <model>', 'Ollama model to use', 'qwen3-vl:8b')
|
|
47
|
+
.action(async (baseline, target, options) => {
|
|
48
|
+
try {
|
|
49
|
+
console.log(chalk_1.default.yellow('⚠️ [Beta Feature] This feature requires a local Ollama instance running at http://127.0.0.1:11434'));
|
|
50
|
+
await VSCommand_1.VSCommand.run(baseline, target, options);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error(chalk_1.default.red(`[VS Mode] Failed: ${error.message}`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// Default Crawl Command (Implicit)
|
|
58
|
+
program
|
|
19
59
|
.argument('<url>', 'The documentation URL to crawl')
|
|
20
60
|
.option('-t, --type <type>', 'Force specify strategy type (ocp, acp)')
|
|
21
61
|
.option('-o, --output <path>', 'Output file path')
|
|
22
62
|
.option('--limit <number>', 'Limit number of pages (for debug)', parseInt)
|
|
23
63
|
.option('--no-headless', 'Run in headful mode (show browser) - Only for ACP')
|
|
64
|
+
.option('-f, --force', 'Force overwrite existing file')
|
|
24
65
|
.action(async (url, options) => {
|
|
25
66
|
try {
|
|
26
67
|
// 1. Determine Strategy
|
|
@@ -39,33 +80,27 @@ program
|
|
|
39
80
|
}
|
|
40
81
|
// 2. Instantiate Strategy
|
|
41
82
|
let strategy;
|
|
42
|
-
// Helper function to generate default filename from URL
|
|
43
|
-
const generateDefaultFilename = (urlStr, typePrefix) => {
|
|
44
|
-
try {
|
|
45
|
-
const u = new URL(urlStr);
|
|
46
|
-
// Get the last path segment that isn't 'index.html' or 'index' or empty
|
|
47
|
-
const segments = u.pathname.split('/').filter(s => s && s !== 'index.html' && s !== 'index');
|
|
48
|
-
const lastSegment = segments.length > 0 ? segments[segments.length - 1] : 'docs';
|
|
49
|
-
// Sanitize filename
|
|
50
|
-
const safeName = lastSegment.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
51
|
-
return `${typePrefix}-${safeName}.md`;
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
return `${typePrefix}-docs.md`;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
83
|
let defaultOutput;
|
|
58
84
|
if (type === 'ocp' || type === StrategyDetector_1.StrategyType.OCP) {
|
|
59
85
|
strategy = new OCPStrategy_1.OCPStrategy();
|
|
60
|
-
defaultOutput = generateDefaultFilename(url, 'ocp');
|
|
86
|
+
defaultOutput = (0, filename_1.generateDefaultFilename)(url, 'ocp');
|
|
61
87
|
}
|
|
62
88
|
else {
|
|
63
89
|
strategy = new ACPStrategy_1.ACPStrategy();
|
|
64
|
-
defaultOutput = generateDefaultFilename(url, 'acp');
|
|
90
|
+
defaultOutput = (0, filename_1.generateDefaultFilename)(url, 'acp');
|
|
65
91
|
}
|
|
66
92
|
// 3. Prepare Context
|
|
67
93
|
const context = new CrawlerContext_1.CrawlerContext(strategy);
|
|
68
94
|
const outputPath = path_1.default.resolve(process.cwd(), options.output || defaultOutput);
|
|
95
|
+
// Check if file exists
|
|
96
|
+
if (fs_1.default.existsSync(outputPath) && !options.force) {
|
|
97
|
+
console.log(chalk_1.default.yellow('--------------------------------------------------'));
|
|
98
|
+
console.log(chalk_1.default.yellow(`ℹ File already exists: ${outputPath}`));
|
|
99
|
+
console.log(chalk_1.default.yellow(' Skipping crawl to save time.'));
|
|
100
|
+
console.log(chalk_1.default.gray(' Use --force or -f to overwrite.'));
|
|
101
|
+
console.log(chalk_1.default.yellow('--------------------------------------------------'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
69
104
|
// 4. Run
|
|
70
105
|
await context.run(url, {
|
|
71
106
|
output: outputPath,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OllamaClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class OllamaClient {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
// Use 127.0.0.1 instead of localhost to avoid IPv6 issues (ECONNREFUSED ::1)
|
|
11
|
+
this.endpoint = options.endpoint || 'http://127.0.0.1:11434';
|
|
12
|
+
this.model = options.model;
|
|
13
|
+
}
|
|
14
|
+
async generate(prompt, onToken) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await axios_1.default.post(`${this.endpoint}/api/generate`, {
|
|
17
|
+
model: this.model,
|
|
18
|
+
prompt: prompt,
|
|
19
|
+
stream: true
|
|
20
|
+
}, {
|
|
21
|
+
responseType: 'stream'
|
|
22
|
+
});
|
|
23
|
+
let fullResponse = '';
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const stream = response.data;
|
|
26
|
+
stream.on('data', (chunk) => {
|
|
27
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
try {
|
|
30
|
+
const json = JSON.parse(line);
|
|
31
|
+
if (json.response) {
|
|
32
|
+
fullResponse += json.response;
|
|
33
|
+
if (onToken) {
|
|
34
|
+
onToken(json.response);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (json.done) {
|
|
38
|
+
// stream ended
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
// ignore partial JSON
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
stream.on('end', () => {
|
|
47
|
+
resolve(fullResponse);
|
|
48
|
+
});
|
|
49
|
+
stream.on('error', (err) => {
|
|
50
|
+
reject(err);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw new Error(`Ollama API call failed: ${error.message}. Is Ollama running at ${this.endpoint}?`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.OllamaClient = OllamaClient;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.VSCommand = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const CrawlerContext_1 = require("../CrawlerContext");
|
|
12
|
+
const OCPStrategy_1 = require("../strategies/OCPStrategy");
|
|
13
|
+
const ACPStrategy_1 = require("../strategies/ACPStrategy");
|
|
14
|
+
const StrategyDetector_1 = require("../utils/StrategyDetector");
|
|
15
|
+
const filename_1 = require("../utils/filename");
|
|
16
|
+
const TocExtractor_1 = require("../utils/TocExtractor");
|
|
17
|
+
const OllamaClient_1 = require("../ai/OllamaClient");
|
|
18
|
+
class VSCommand {
|
|
19
|
+
static async run(baselineUrl, targetUrl, options) {
|
|
20
|
+
console.log(chalk_1.default.blue(`[VS Mode] Starting comparison between:`));
|
|
21
|
+
console.log(chalk_1.default.gray(`Baseline: ${baselineUrl}`));
|
|
22
|
+
console.log(chalk_1.default.gray(`Target: ${targetUrl}`));
|
|
23
|
+
console.log(chalk_1.default.gray(`Model: ${options.model}`));
|
|
24
|
+
// 1. Crawl Baseline
|
|
25
|
+
const baselineFile = await VSCommand.crawlUrl(baselineUrl, 'baseline');
|
|
26
|
+
// 2. Crawl Target
|
|
27
|
+
const targetFile = await VSCommand.crawlUrl(targetUrl, 'target');
|
|
28
|
+
// 3. Extract TOC
|
|
29
|
+
const baselineContent = fs_1.default.readFileSync(baselineFile, 'utf-8');
|
|
30
|
+
const targetContent = fs_1.default.readFileSync(targetFile, 'utf-8');
|
|
31
|
+
const baselineToc = TocExtractor_1.TocExtractor.extract(baselineContent);
|
|
32
|
+
const targetToc = TocExtractor_1.TocExtractor.extract(targetContent);
|
|
33
|
+
console.log(chalk_1.default.green(`[VS Mode] TOC extracted.`));
|
|
34
|
+
// 4. Load Prompts
|
|
35
|
+
const promptPath = path_1.default.resolve(process.cwd(), 'solo-doc-prompt.md');
|
|
36
|
+
let promptContent = '';
|
|
37
|
+
if (fs_1.default.existsSync(promptPath)) {
|
|
38
|
+
promptContent = fs_1.default.readFileSync(promptPath, 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Try to look in package root (assuming we might be running from bin)
|
|
42
|
+
// or just fail gracefully
|
|
43
|
+
const altPath = path_1.default.resolve(__dirname, '../../solo-doc-prompt.md');
|
|
44
|
+
if (fs_1.default.existsSync(altPath)) {
|
|
45
|
+
promptContent = fs_1.default.readFileSync(altPath, 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.warn(chalk_1.default.yellow('[VS Mode] Warning: solo-doc-prompt.md not found. Using default internal prompts.'));
|
|
49
|
+
// Define fallback prompts here if needed, or throw
|
|
50
|
+
// For now, let's throw to ensure user provides the file as requested
|
|
51
|
+
throw new Error('Could not find solo-doc-prompt.md in current directory.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const prompts = VSCommand.parsePrompts(promptContent);
|
|
55
|
+
if (prompts.length < 2) {
|
|
56
|
+
throw new Error('Found fewer than 2 prompt templates in solo-doc-prompt.md');
|
|
57
|
+
}
|
|
58
|
+
const client = new OllamaClient_1.OllamaClient({ model: options.model });
|
|
59
|
+
// 5. Step 1: Independent Comparison
|
|
60
|
+
console.log(chalk_1.default.blue(`[VS Mode] Step 1: Analyzing differences...`));
|
|
61
|
+
// Replace placeholders
|
|
62
|
+
// Note: The prompt template has [看附件ocp的文档目录树]
|
|
63
|
+
let prompt1 = prompts[0];
|
|
64
|
+
prompt1 = prompt1.replace('[看附件ocp的文档目录树]', '\n' + baselineToc + '\n');
|
|
65
|
+
prompt1 = prompt1.replace('[看附件alauda的文档目录树]', '\n' + targetToc + '\n');
|
|
66
|
+
const spinner1 = (0, ora_1.default)('Waiting for AI response (this may take a while)...').start();
|
|
67
|
+
let hasStartedOutput1 = false;
|
|
68
|
+
const result1 = await client.generate(prompt1, (token) => {
|
|
69
|
+
if (!hasStartedOutput1) {
|
|
70
|
+
spinner1.stop();
|
|
71
|
+
process.stdout.write(chalk_1.default.cyan('AI Thinking: '));
|
|
72
|
+
hasStartedOutput1 = true;
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(token);
|
|
75
|
+
});
|
|
76
|
+
if (!hasStartedOutput1)
|
|
77
|
+
spinner1.stop();
|
|
78
|
+
process.stdout.write('\n');
|
|
79
|
+
const result1File = 'vs-result.md';
|
|
80
|
+
fs_1.default.writeFileSync(result1File, result1);
|
|
81
|
+
console.log(chalk_1.default.green(`[VS Mode] Step 1 complete. Saved to ${result1File}`));
|
|
82
|
+
// 6. Step 2: Integration
|
|
83
|
+
console.log(chalk_1.default.blue(`[VS Mode] Step 2: Integrating into documentation tree...`));
|
|
84
|
+
let prompt2 = prompts[1];
|
|
85
|
+
// The prompt says "基于OpenShift文档的目录树" - we should inject it if it's not explicitly placeholder
|
|
86
|
+
// Or we just prepend context.
|
|
87
|
+
// The template: "基于OpenShift文档的目录树,和上面的详细对比总结。"
|
|
88
|
+
// We construct the full prompt by prepending data.
|
|
89
|
+
const context2 = `
|
|
90
|
+
OpenShift文档目录树:
|
|
91
|
+
${baselineToc}
|
|
92
|
+
|
|
93
|
+
详细对比总结:
|
|
94
|
+
${result1}
|
|
95
|
+
|
|
96
|
+
`;
|
|
97
|
+
prompt2 = context2 + prompt2;
|
|
98
|
+
const spinner2 = (0, ora_1.default)('Waiting for AI response (this may take a while)...').start();
|
|
99
|
+
let hasStartedOutput2 = false;
|
|
100
|
+
const result2 = await client.generate(prompt2, (token) => {
|
|
101
|
+
if (!hasStartedOutput2) {
|
|
102
|
+
spinner2.stop();
|
|
103
|
+
process.stdout.write(chalk_1.default.cyan('AI Thinking: '));
|
|
104
|
+
hasStartedOutput2 = true;
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write(token);
|
|
107
|
+
});
|
|
108
|
+
if (!hasStartedOutput2)
|
|
109
|
+
spinner2.stop();
|
|
110
|
+
process.stdout.write('\n');
|
|
111
|
+
const result2File = 'vs-tree.md';
|
|
112
|
+
fs_1.default.writeFileSync(result2File, result2);
|
|
113
|
+
console.log(chalk_1.default.green(`[VS Mode] Step 2 complete. Saved to ${result2File}`));
|
|
114
|
+
console.log(chalk_1.default.green(`[VS Mode] All tasks finished.`));
|
|
115
|
+
}
|
|
116
|
+
static async crawlUrl(url, prefix) {
|
|
117
|
+
// Try to detect type
|
|
118
|
+
let type = StrategyDetector_1.StrategyDetector.detect(url);
|
|
119
|
+
let strategy;
|
|
120
|
+
// Simple logic: if detects OCP, use OCP. Else ACP (more generic).
|
|
121
|
+
if (type === StrategyDetector_1.StrategyType.OCP) {
|
|
122
|
+
strategy = new OCPStrategy_1.OCPStrategy();
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Default to ACP which uses Puppeteer
|
|
126
|
+
strategy = new ACPStrategy_1.ACPStrategy();
|
|
127
|
+
}
|
|
128
|
+
const filename = (0, filename_1.generateDefaultFilename)(url, prefix);
|
|
129
|
+
const outputPath = path_1.default.resolve(process.cwd(), filename);
|
|
130
|
+
// Check if file exists
|
|
131
|
+
if (fs_1.default.existsSync(outputPath)) {
|
|
132
|
+
console.log(chalk_1.default.yellow('--------------------------------------------------'));
|
|
133
|
+
console.log(chalk_1.default.yellow(`ℹ File already exists: ${outputPath}`));
|
|
134
|
+
console.log(chalk_1.default.yellow(' Using cached version for comparison.'));
|
|
135
|
+
console.log(chalk_1.default.yellow('--------------------------------------------------'));
|
|
136
|
+
return outputPath;
|
|
137
|
+
}
|
|
138
|
+
console.log(chalk_1.default.blue(`[VS Mode] Crawling ${url} -> ${filename}...`));
|
|
139
|
+
const context = new CrawlerContext_1.CrawlerContext(strategy);
|
|
140
|
+
// Suppress console log from crawler to keep output clean?
|
|
141
|
+
// Or keep it to show progress. Keep it.
|
|
142
|
+
await context.run(url, { output: outputPath, headless: true });
|
|
143
|
+
return outputPath;
|
|
144
|
+
}
|
|
145
|
+
static parsePrompts(content) {
|
|
146
|
+
// Match content inside ``` ... ``` blocks that follow "Prompt模板"
|
|
147
|
+
// Regex: /Prompt模板.*?\n```([\s\S]*?)```/g
|
|
148
|
+
const regex = /Prompt模板.*?\n```([\s\S]*?)```/g;
|
|
149
|
+
const matches = [];
|
|
150
|
+
let match;
|
|
151
|
+
while ((match = regex.exec(content)) !== null) {
|
|
152
|
+
matches.push(match[1].trim());
|
|
153
|
+
}
|
|
154
|
+
return matches;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.VSCommand = VSCommand;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TocExtractor = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the Table of Contents (headers) from Markdown content.
|
|
6
|
+
* Returns a string representation of the tree.
|
|
7
|
+
*/
|
|
8
|
+
class TocExtractor {
|
|
9
|
+
static extract(markdown) {
|
|
10
|
+
const lines = markdown.split('\n');
|
|
11
|
+
const tocLines = [];
|
|
12
|
+
let inCodeBlock = false;
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
// Simple code block detection to avoid headers inside code blocks
|
|
15
|
+
if (line.trim().startsWith('```')) {
|
|
16
|
+
inCodeBlock = !inCodeBlock;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (inCodeBlock)
|
|
20
|
+
continue;
|
|
21
|
+
// Match headers H1 to H3 only
|
|
22
|
+
// Regex: ^#{1,3}\s
|
|
23
|
+
if (line.match(/^#{1,3}\s/)) {
|
|
24
|
+
tocLines.push(line.trim());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (tocLines.length === 0) {
|
|
28
|
+
return "No headers found.";
|
|
29
|
+
}
|
|
30
|
+
return tocLines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.TocExtractor = TocExtractor;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateDefaultFilename = void 0;
|
|
4
|
+
const generateDefaultFilename = (urlStr, typePrefix) => {
|
|
5
|
+
try {
|
|
6
|
+
const u = new URL(urlStr);
|
|
7
|
+
// Get the last path segment that isn't 'index.html' or 'index' or empty
|
|
8
|
+
const segments = u.pathname.split('/').filter(s => s && s !== 'index.html' && s !== 'index');
|
|
9
|
+
const lastSegment = segments.length > 0 ? segments[segments.length - 1] : 'docs';
|
|
10
|
+
// Sanitize filename
|
|
11
|
+
const safeName = lastSegment.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
12
|
+
return `${typePrefix}-${safeName}.md`;
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
return `${typePrefix}-docs.md`;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
exports.generateDefaultFilename = generateDefaultFilename;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solo-doc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"main": "dist/bin/solo-doc.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"solo-doc": "dist/bin/solo-doc.js"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"clean": "rm -rf dist",
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"prepublishOnly": "npm run clean && npm run build",
|
|
16
|
+
"release": "npm run clean && npm run build && npm version patch --force && npm publish --access=public",
|
|
16
17
|
"start": "node dist/bin/solo-doc.js",
|
|
17
18
|
"dev": "ts-node bin/solo-doc.ts"
|
|
18
19
|
},
|