mdsel 0.1.2 → 0.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 +92 -109
- package/dist/cli.mjs +349 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,38 +10,30 @@ mdsel parses Markdown documents into semantic trees and exposes machine-addressa
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
$ mdsel README.md
|
|
13
|
-
h1 mdsel
|
|
14
|
-
h2 Demo
|
|
15
|
-
h2 Installation
|
|
16
|
-
h2 Quick Start
|
|
17
|
-
h2
|
|
18
|
-
h3
|
|
19
|
-
h3
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
h2 Output Format
|
|
27
|
-
h2 Error Handling
|
|
28
|
-
h2 Development
|
|
29
|
-
h2 License
|
|
13
|
+
h1.0 mdsel
|
|
14
|
+
h2.0 Demo
|
|
15
|
+
h2.1 Installation
|
|
16
|
+
h2.2 Quick Start
|
|
17
|
+
h2.3 Usage
|
|
18
|
+
h3.0 Index (files only)
|
|
19
|
+
h3.1 Select (files + selectors)
|
|
20
|
+
h3.2 Search (fuzzy matching)
|
|
21
|
+
h2.4 Selectors
|
|
22
|
+
h2.5 Output Format
|
|
23
|
+
h2.6 Error Handling
|
|
24
|
+
h2.7 Development
|
|
25
|
+
h2.8 License
|
|
30
26
|
---
|
|
31
|
-
code:
|
|
27
|
+
code:29 para:29 list:5 table:4
|
|
32
28
|
```
|
|
33
29
|
|
|
34
30
|
**2. Select specific content by selector:**
|
|
35
31
|
|
|
36
32
|
```bash
|
|
37
|
-
$ mdsel README.md
|
|
38
|
-
```
|
|
39
|
-
```
|
|
33
|
+
$ mdsel h2.1 README.md
|
|
40
34
|
## Installation
|
|
41
35
|
|
|
42
|
-
```bash
|
|
43
36
|
npm install -g mdsel
|
|
44
|
-
```
|
|
45
37
|
|
|
46
38
|
**Requirements**: Node.js >=18.0.0
|
|
47
39
|
```
|
|
@@ -49,9 +41,7 @@ npm install -g mdsel
|
|
|
49
41
|
**3. Drill into nested content:**
|
|
50
42
|
|
|
51
43
|
```bash
|
|
52
|
-
$ mdsel
|
|
53
|
-
```
|
|
54
|
-
```
|
|
44
|
+
$ mdsel "h2.1/code.0" README.md
|
|
55
45
|
npm install -g mdsel
|
|
56
46
|
```
|
|
57
47
|
|
|
@@ -66,143 +56,134 @@ npm install -g mdsel
|
|
|
66
56
|
## Quick Start
|
|
67
57
|
|
|
68
58
|
```bash
|
|
69
|
-
# Index a document to
|
|
59
|
+
# Index a document to see its structure
|
|
70
60
|
mdsel README.md
|
|
71
61
|
|
|
72
|
-
# Select a specific
|
|
73
|
-
mdsel README.md
|
|
62
|
+
# Select a specific section by index
|
|
63
|
+
mdsel h2.1 README.md
|
|
64
|
+
|
|
65
|
+
# Select the entire document
|
|
66
|
+
mdsel '*' README.md
|
|
74
67
|
|
|
75
|
-
# Select
|
|
76
|
-
mdsel
|
|
68
|
+
# Select a nested element (first code block under second h2)
|
|
69
|
+
mdsel "h2.1/code.0" README.md
|
|
77
70
|
|
|
78
|
-
# Select multiple
|
|
79
|
-
mdsel
|
|
71
|
+
# Select multiple sections at once
|
|
72
|
+
mdsel h2.0 h2.1 README.md
|
|
80
73
|
|
|
81
|
-
# Select
|
|
82
|
-
mdsel
|
|
74
|
+
# Select a range of sections
|
|
75
|
+
mdsel h2.0-2 README.md
|
|
83
76
|
|
|
84
|
-
#
|
|
85
|
-
mdsel README.md
|
|
77
|
+
# Fuzzy search when you don't know the exact selector
|
|
78
|
+
mdsel "installation" README.md
|
|
86
79
|
|
|
87
|
-
# Limit output to
|
|
88
|
-
mdsel
|
|
80
|
+
# Limit output to first N lines
|
|
81
|
+
mdsel "h2.0?head=10" README.md
|
|
89
82
|
|
|
90
|
-
#
|
|
83
|
+
# JSON output for programmatic use
|
|
91
84
|
mdsel --json README.md
|
|
92
85
|
```
|
|
93
86
|
|
|
94
87
|
## Usage
|
|
95
88
|
|
|
96
|
-
mdsel automatically detects whether arguments are files or selectors:
|
|
97
|
-
|
|
98
89
|
```bash
|
|
99
|
-
mdsel <files...> [selectors...]
|
|
100
90
|
mdsel [options] <files...> [selectors...]
|
|
101
91
|
```
|
|
102
92
|
|
|
93
|
+
Arguments are auto-detected: `.md` files and existing paths are files, everything else is a selector.
|
|
94
|
+
|
|
103
95
|
**Options**:
|
|
104
96
|
- `--json` - Output JSON instead of text
|
|
97
|
+
- `--help` - Show help
|
|
105
98
|
|
|
106
99
|
### Index (files only)
|
|
107
100
|
|
|
108
|
-
When only files are provided,
|
|
101
|
+
When only files are provided, outputs the document structure:
|
|
109
102
|
|
|
110
103
|
```bash
|
|
111
104
|
mdsel README.md
|
|
112
|
-
mdsel README.md docs/API.md
|
|
113
|
-
mdsel --json README.md
|
|
114
105
|
```
|
|
115
106
|
|
|
116
|
-
**Text Output** (default):
|
|
117
107
|
```
|
|
118
108
|
h1.0 mdsel
|
|
119
|
-
h2.0
|
|
120
|
-
h2.1
|
|
121
|
-
h2.2
|
|
122
|
-
|
|
123
|
-
h3.
|
|
109
|
+
h2.0 Demo
|
|
110
|
+
h2.1 Installation
|
|
111
|
+
h2.2 Quick Start
|
|
112
|
+
h2.3 Usage
|
|
113
|
+
h3.0 Index (files only)
|
|
114
|
+
h3.1 Select (files + selectors)
|
|
115
|
+
h3.2 Search (fuzzy matching)
|
|
124
116
|
---
|
|
125
|
-
code:
|
|
117
|
+
code:29 para:29 list:5 table:4
|
|
126
118
|
```
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"command": "index",
|
|
133
|
-
"timestamp": "2025-01-15T10:30:00.000Z",
|
|
134
|
-
"data": {
|
|
135
|
-
"documents": [
|
|
136
|
-
{
|
|
137
|
-
"namespace": "readme",
|
|
138
|
-
"file_path": "README.md",
|
|
139
|
-
"headings": [...],
|
|
140
|
-
"blocks": {
|
|
141
|
-
"paragraphs": 5,
|
|
142
|
-
"code_blocks": 2,
|
|
143
|
-
"lists": 1,
|
|
144
|
-
"tables": 0,
|
|
145
|
-
"blockquotes": 0
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
],
|
|
149
|
-
"summary": {
|
|
150
|
-
"total_documents": 1,
|
|
151
|
-
"total_nodes": 8,
|
|
152
|
-
"total_selectors": 8
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
```
|
|
120
|
+
The index shows:
|
|
121
|
+
- Heading hierarchy with selectors (e.g., `h1.0`, `h2.0`)
|
|
122
|
+
- Indentation reflecting document structure
|
|
123
|
+
- Block counts for code, paragraphs, lists, tables
|
|
157
124
|
|
|
158
125
|
### Select (files + selectors)
|
|
159
126
|
|
|
160
|
-
When
|
|
127
|
+
When selectors are provided, retrieves matching content:
|
|
161
128
|
|
|
162
129
|
```bash
|
|
163
|
-
# Single
|
|
164
|
-
mdsel README.md
|
|
165
|
-
|
|
166
|
-
# Multiple selectors
|
|
167
|
-
mdsel README.md h2.0 h2.1 code.0
|
|
168
|
-
|
|
169
|
-
# Cross-document selection
|
|
170
|
-
mdsel README.md GUIDE.md h1.0
|
|
171
|
-
|
|
172
|
-
# With query parameters
|
|
173
|
-
mdsel README.md "h2.0?head=10"
|
|
174
|
-
|
|
175
|
-
# Range selection
|
|
176
|
-
mdsel README.md h2.1-3
|
|
130
|
+
# Single result - content only
|
|
131
|
+
mdsel h2.1 README.md
|
|
132
|
+
```
|
|
177
133
|
|
|
178
|
-
# Comma list selection
|
|
179
|
-
mdsel README.md h2.0,2,4
|
|
180
134
|
```
|
|
135
|
+
## Installation
|
|
181
136
|
|
|
182
|
-
|
|
137
|
+
npm install -g mdsel
|
|
138
|
+
|
|
139
|
+
**Requirements**: Node.js >=18.0.0
|
|
183
140
|
```
|
|
184
|
-
## Quick Start
|
|
185
141
|
|
|
186
|
-
|
|
142
|
+
```bash
|
|
143
|
+
# Multiple results - prefixed with selector
|
|
144
|
+
mdsel h2.0 h2.1 README.md
|
|
187
145
|
```
|
|
188
146
|
|
|
189
|
-
**Multiple Results** (selector prefix):
|
|
190
147
|
```
|
|
191
148
|
heading:h2.0:
|
|
192
|
-
##
|
|
193
|
-
|
|
149
|
+
## Demo
|
|
150
|
+
...
|
|
194
151
|
heading:h2.1:
|
|
195
|
-
##
|
|
196
|
-
|
|
152
|
+
## Installation
|
|
153
|
+
...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Errors show suggestions
|
|
158
|
+
mdsel h2.99 README.md
|
|
197
159
|
```
|
|
198
160
|
|
|
199
|
-
**Error Output**:
|
|
200
161
|
```
|
|
201
162
|
!h2.99
|
|
202
|
-
Index out of range: document has
|
|
163
|
+
Index out of range: document has 9 h2 headings
|
|
203
164
|
~h2.0 ~h2.1 ~h2.2
|
|
204
165
|
```
|
|
205
166
|
|
|
167
|
+
### Search (fuzzy matching)
|
|
168
|
+
|
|
169
|
+
When input doesn't look like a selector, mdsel performs fuzzy search:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
mdsel "installation" README.md
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Search results for "installation":
|
|
177
|
+
|
|
178
|
+
readme::h2.1 (100% match)
|
|
179
|
+
Installation
|
|
180
|
+
|
|
181
|
+
readme::code.9 (74% match)
|
|
182
|
+
## Installation npm install -g mdsel ...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Search returns selectors you can use directly to fetch the content.
|
|
186
|
+
|
|
206
187
|
## Selectors
|
|
207
188
|
|
|
208
189
|
Selectors are path-based, ordinal, stateless, and deterministic. They resemble CSS/XPath conceptually but are purpose-built for Markdown.
|
|
@@ -223,6 +204,7 @@ Selectors are path-based, ordinal, stateless, and deterministic. They resemble C
|
|
|
223
204
|
|
|
224
205
|
| Category | Full Form | Shorthand |
|
|
225
206
|
|----------|-----------|-----------|
|
|
207
|
+
| Wildcard | `*` | `*` |
|
|
226
208
|
| Root | `root` | - |
|
|
227
209
|
| Headings | `heading:h1` ... `heading:h6` | `h1` ... `h6` |
|
|
228
210
|
| Sections | `section` | - |
|
|
@@ -248,6 +230,7 @@ Two equivalent notations are supported:
|
|
|
248
230
|
|
|
249
231
|
**Basic selection**:
|
|
250
232
|
```bash
|
|
233
|
+
* # Entire document (wildcard)
|
|
251
234
|
root # Document root
|
|
252
235
|
h1.0 # First h1 heading
|
|
253
236
|
h2.1 # Second h2 heading
|
package/dist/cli.mjs
CHANGED
|
@@ -855,6 +855,10 @@ function tokenize(input) {
|
|
|
855
855
|
advance();
|
|
856
856
|
addToken("EQUALS" /* EQUALS */, "=", pos);
|
|
857
857
|
continue;
|
|
858
|
+
case "*":
|
|
859
|
+
advance();
|
|
860
|
+
addToken("STAR" /* STAR */, "*", pos);
|
|
861
|
+
continue;
|
|
858
862
|
case '"':
|
|
859
863
|
case "'": {
|
|
860
864
|
const strPos = position();
|
|
@@ -976,7 +980,9 @@ var Parser = class {
|
|
|
976
980
|
let nodeType;
|
|
977
981
|
let subtype;
|
|
978
982
|
let index;
|
|
979
|
-
if (this.match("
|
|
983
|
+
if (this.match("STAR" /* STAR */)) {
|
|
984
|
+
nodeType = "all";
|
|
985
|
+
} else if (this.match("ROOT" /* ROOT */)) {
|
|
980
986
|
nodeType = "root";
|
|
981
987
|
} else if (this.match("HEADING" /* HEADING */)) {
|
|
982
988
|
nodeType = "heading";
|
|
@@ -1420,6 +1426,10 @@ function resolvePathSegments(context, segments) {
|
|
|
1420
1426
|
currentNodes = currentNodes.map(({ node, path }) => ({ node, path: [...path, node] }));
|
|
1421
1427
|
continue;
|
|
1422
1428
|
}
|
|
1429
|
+
if (segment.nodeType === "all") {
|
|
1430
|
+
currentNodes = currentNodes.map(({ node, path }) => ({ node, path: [...path, node] }));
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1423
1433
|
const currentNode = currentNodes[0]?.node;
|
|
1424
1434
|
const currentPath = currentNodes[0]?.path ?? [];
|
|
1425
1435
|
const matches = findMatchingChildren(currentNode, segment);
|
|
@@ -1558,6 +1568,9 @@ function segmentToStringWithIndex(segment, index) {
|
|
|
1558
1568
|
return segStr;
|
|
1559
1569
|
}
|
|
1560
1570
|
function segmentToString(segment) {
|
|
1571
|
+
if (segment.nodeType === "all") {
|
|
1572
|
+
return "*";
|
|
1573
|
+
}
|
|
1561
1574
|
let segStr = segment.nodeType;
|
|
1562
1575
|
if (segment.subtype) {
|
|
1563
1576
|
segStr += `:${segment.subtype}`;
|
|
@@ -1684,6 +1697,193 @@ function selectorToString2(selector) {
|
|
|
1684
1697
|
return result;
|
|
1685
1698
|
}
|
|
1686
1699
|
|
|
1700
|
+
// src/resolver/search.ts
|
|
1701
|
+
import { toString as toString2 } from "mdast-util-to-string";
|
|
1702
|
+
function searchDocument(tree, namespace, query, options = {}) {
|
|
1703
|
+
const { maxResults = 10, minScore = 0.3, includeCode = true } = options;
|
|
1704
|
+
const results = [];
|
|
1705
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
1706
|
+
if (!normalizedQuery) {
|
|
1707
|
+
return [];
|
|
1708
|
+
}
|
|
1709
|
+
const headingIndices = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
|
|
1710
|
+
const blockIndices = {
|
|
1711
|
+
paragraph: 0,
|
|
1712
|
+
code: 0,
|
|
1713
|
+
list: 0,
|
|
1714
|
+
table: 0,
|
|
1715
|
+
blockquote: 0
|
|
1716
|
+
};
|
|
1717
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
1718
|
+
const node = tree.children[i];
|
|
1719
|
+
if (!node) continue;
|
|
1720
|
+
const nodeType = node.type;
|
|
1721
|
+
const text = toString2(node);
|
|
1722
|
+
const normalizedText = text.toLowerCase();
|
|
1723
|
+
const matchResult = calculateMatchScore(normalizedQuery, normalizedText, text);
|
|
1724
|
+
if (matchResult.score >= minScore) {
|
|
1725
|
+
let selector;
|
|
1726
|
+
let type;
|
|
1727
|
+
if (nodeType === "heading" && "depth" in node) {
|
|
1728
|
+
const depth = node.depth;
|
|
1729
|
+
const idx = headingIndices[depth] ?? 0;
|
|
1730
|
+
selector = `${namespace}::h${depth}.${idx}`;
|
|
1731
|
+
type = `heading:h${depth}`;
|
|
1732
|
+
headingIndices[depth] = idx + 1;
|
|
1733
|
+
} else if (nodeType in blockIndices) {
|
|
1734
|
+
const idx = blockIndices[nodeType] ?? 0;
|
|
1735
|
+
const shorthand = getBlockShorthand(nodeType);
|
|
1736
|
+
selector = `${namespace}::${shorthand}.${idx}`;
|
|
1737
|
+
type = nodeType;
|
|
1738
|
+
blockIndices[nodeType] = idx + 1;
|
|
1739
|
+
if (nodeType === "code" && !includeCode) {
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
} else {
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
results.push({
|
|
1746
|
+
selector,
|
|
1747
|
+
type,
|
|
1748
|
+
preview: createPreview(text, normalizedQuery, 100),
|
|
1749
|
+
content: text,
|
|
1750
|
+
score: matchResult.score,
|
|
1751
|
+
matchType: matchResult.matchType
|
|
1752
|
+
});
|
|
1753
|
+
} else {
|
|
1754
|
+
if (nodeType === "heading" && "depth" in node) {
|
|
1755
|
+
const depth = node.depth;
|
|
1756
|
+
headingIndices[depth] = (headingIndices[depth] ?? 0) + 1;
|
|
1757
|
+
} else if (nodeType in blockIndices) {
|
|
1758
|
+
blockIndices[nodeType] = (blockIndices[nodeType] ?? 0) + 1;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
const sectionResults = searchHeadingSections(tree, namespace, normalizedQuery, minScore);
|
|
1763
|
+
results.push(...sectionResults);
|
|
1764
|
+
return results.sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
1765
|
+
}
|
|
1766
|
+
function searchHeadingSections(tree, namespace, normalizedQuery, minScore) {
|
|
1767
|
+
const results = [];
|
|
1768
|
+
const headingIndices = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
|
|
1769
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
1770
|
+
const node = tree.children[i];
|
|
1771
|
+
if (!node || node.type !== "heading" || !("depth" in node)) {
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
const depth = node.depth;
|
|
1775
|
+
const idx = headingIndices[depth] ?? 0;
|
|
1776
|
+
headingIndices[depth] = idx + 1;
|
|
1777
|
+
const sectionParts = [toString2(node)];
|
|
1778
|
+
for (let j = i + 1; j < tree.children.length; j++) {
|
|
1779
|
+
const sibling = tree.children[j];
|
|
1780
|
+
if (!sibling) break;
|
|
1781
|
+
if (sibling.type === "heading" && "depth" in sibling && sibling.depth <= depth) {
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
sectionParts.push(toString2(sibling));
|
|
1785
|
+
}
|
|
1786
|
+
const sectionText = sectionParts.join("\n");
|
|
1787
|
+
const normalizedSection = sectionText.toLowerCase();
|
|
1788
|
+
const matchResult = calculateMatchScore(normalizedQuery, normalizedSection, sectionText);
|
|
1789
|
+
if (matchResult.score >= minScore) {
|
|
1790
|
+
const selector = `${namespace}::h${depth}.${idx}`;
|
|
1791
|
+
const existing = results.find((r) => r.selector === selector);
|
|
1792
|
+
if (!existing) {
|
|
1793
|
+
results.push({
|
|
1794
|
+
selector,
|
|
1795
|
+
type: `heading:h${depth}`,
|
|
1796
|
+
preview: createPreview(sectionText, normalizedQuery, 100),
|
|
1797
|
+
content: sectionText,
|
|
1798
|
+
score: matchResult.score * 0.9,
|
|
1799
|
+
// Slightly lower score for section matches
|
|
1800
|
+
matchType: matchResult.matchType
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return results;
|
|
1806
|
+
}
|
|
1807
|
+
function calculateMatchScore(normalizedQuery, normalizedText, originalText) {
|
|
1808
|
+
if (normalizedText === normalizedQuery) {
|
|
1809
|
+
return { score: 1, matchType: "exact" };
|
|
1810
|
+
}
|
|
1811
|
+
if (normalizedText.includes(normalizedQuery)) {
|
|
1812
|
+
const coverage = normalizedQuery.length / normalizedText.length;
|
|
1813
|
+
const score = 0.7 + coverage * 0.25;
|
|
1814
|
+
return { score, matchType: "substring" };
|
|
1815
|
+
}
|
|
1816
|
+
const queryWords = normalizedQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
1817
|
+
const textWords = normalizedText.split(/\s+/).filter((w) => w.length > 0);
|
|
1818
|
+
if (queryWords.length > 0) {
|
|
1819
|
+
let matchedWords = 0;
|
|
1820
|
+
for (const queryWord of queryWords) {
|
|
1821
|
+
for (const textWord of textWords) {
|
|
1822
|
+
if (textWord === queryWord) {
|
|
1823
|
+
matchedWords++;
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
if (textWord.includes(queryWord) || queryWord.includes(textWord)) {
|
|
1827
|
+
matchedWords += 0.7;
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
const distance = levenshteinDistance(queryWord, textWord);
|
|
1831
|
+
const maxLen = Math.max(queryWord.length, textWord.length);
|
|
1832
|
+
const similarity = (maxLen - distance) / maxLen;
|
|
1833
|
+
if (similarity > 0.7) {
|
|
1834
|
+
matchedWords += similarity * 0.5;
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const wordMatchRatio = matchedWords / queryWords.length;
|
|
1840
|
+
if (wordMatchRatio > 0.3) {
|
|
1841
|
+
return { score: wordMatchRatio * 0.6, matchType: "fuzzy" };
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return { score: 0, matchType: "fuzzy" };
|
|
1845
|
+
}
|
|
1846
|
+
function createPreview(text, query, maxLength) {
|
|
1847
|
+
const normalizedText = text.toLowerCase();
|
|
1848
|
+
const index = normalizedText.indexOf(query);
|
|
1849
|
+
if (index === -1) {
|
|
1850
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
1851
|
+
if (cleaned.length <= maxLength) {
|
|
1852
|
+
return cleaned;
|
|
1853
|
+
}
|
|
1854
|
+
return cleaned.slice(0, maxLength - 3) + "...";
|
|
1855
|
+
}
|
|
1856
|
+
const start = Math.max(0, index - Math.floor((maxLength - query.length) / 2));
|
|
1857
|
+
const end = Math.min(text.length, start + maxLength);
|
|
1858
|
+
let preview = text.slice(start, end).replace(/\s+/g, " ");
|
|
1859
|
+
if (start > 0) {
|
|
1860
|
+
preview = "..." + preview;
|
|
1861
|
+
}
|
|
1862
|
+
if (end < text.length) {
|
|
1863
|
+
preview = preview + "...";
|
|
1864
|
+
}
|
|
1865
|
+
return preview.trim();
|
|
1866
|
+
}
|
|
1867
|
+
function getBlockShorthand(type) {
|
|
1868
|
+
switch (type) {
|
|
1869
|
+
case "paragraph":
|
|
1870
|
+
return "para";
|
|
1871
|
+
case "blockquote":
|
|
1872
|
+
return "quote";
|
|
1873
|
+
default:
|
|
1874
|
+
return type;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function searchMultipleDocuments(documents, query, options = {}) {
|
|
1878
|
+
const allResults = [];
|
|
1879
|
+
for (const doc of documents) {
|
|
1880
|
+
const docResults = searchDocument(doc.tree, doc.namespace, query, options);
|
|
1881
|
+
allResults.push(...docResults);
|
|
1882
|
+
}
|
|
1883
|
+
const maxResults = options.maxResults ?? 10;
|
|
1884
|
+
return allResults.sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1687
1887
|
// src/cli/commands/select-command.ts
|
|
1688
1888
|
async function selectCommand(selector, files, options = {}) {
|
|
1689
1889
|
const useJson = options.json === true;
|
|
@@ -1698,22 +1898,32 @@ async function selectCommand(selector, files, options = {}) {
|
|
|
1698
1898
|
return;
|
|
1699
1899
|
}
|
|
1700
1900
|
let selectorAst;
|
|
1901
|
+
let parseError = null;
|
|
1701
1902
|
try {
|
|
1702
1903
|
selectorAst = parseSelector(selector);
|
|
1703
1904
|
} catch (error) {
|
|
1704
1905
|
if (error instanceof SelectorParseError) {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1906
|
+
parseError = error;
|
|
1907
|
+
} else {
|
|
1908
|
+
throw error;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
if (parseError) {
|
|
1912
|
+
const looksLikeSearch = isLikelySearchQuery(selector);
|
|
1913
|
+
if (looksLikeSearch) {
|
|
1914
|
+
await performSearchFallback(selector, files, options);
|
|
1714
1915
|
return;
|
|
1715
1916
|
}
|
|
1716
|
-
|
|
1917
|
+
const errorEntry = createErrorEntry(
|
|
1918
|
+
"INVALID_SELECTOR",
|
|
1919
|
+
parseError.code,
|
|
1920
|
+
parseError.message,
|
|
1921
|
+
void 0,
|
|
1922
|
+
selector
|
|
1923
|
+
);
|
|
1924
|
+
outputError2([errorEntry], useJson);
|
|
1925
|
+
exitWithCode(ExitCode.ERROR);
|
|
1926
|
+
return;
|
|
1717
1927
|
}
|
|
1718
1928
|
const truncateOptions = {};
|
|
1719
1929
|
if (selectorAst.queryParams) {
|
|
@@ -1945,6 +2155,121 @@ async function selectMultiCommand(selectors, files, options = {}) {
|
|
|
1945
2155
|
}
|
|
1946
2156
|
exitWithCode(allUnresolved.length > 0 ? ExitCode.ERROR : ExitCode.SUCCESS);
|
|
1947
2157
|
}
|
|
2158
|
+
function isLikelySearchQuery(input) {
|
|
2159
|
+
if (!input.trim()) {
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
if (input.includes("::")) {
|
|
2163
|
+
return false;
|
|
2164
|
+
}
|
|
2165
|
+
if (input.includes("[") && !input.includes("]") || !input.includes("[") && input.includes("]")) {
|
|
2166
|
+
return false;
|
|
2167
|
+
}
|
|
2168
|
+
if (/^(h[1-6]|code|para|paragraph|list|table|quote|blockquote|root|section|heading|block)[\.\[]/.test(input)) {
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
if (/^(heading|block):/.test(input)) {
|
|
2172
|
+
return false;
|
|
2173
|
+
}
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
async function performSearchFallback(query, files, options) {
|
|
2177
|
+
const useJson = options.json === true;
|
|
2178
|
+
const documents = [];
|
|
2179
|
+
const parseErrors = [];
|
|
2180
|
+
for (const file of files) {
|
|
2181
|
+
try {
|
|
2182
|
+
const result = await parseFile(file);
|
|
2183
|
+
const namespace = deriveNamespace(file);
|
|
2184
|
+
documents.push({
|
|
2185
|
+
namespace,
|
|
2186
|
+
tree: result.ast
|
|
2187
|
+
});
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
if (error instanceof ParserError) {
|
|
2190
|
+
parseErrors.push(
|
|
2191
|
+
createErrorEntry(
|
|
2192
|
+
error.code,
|
|
2193
|
+
error.code,
|
|
2194
|
+
error.message,
|
|
2195
|
+
error.filePath
|
|
2196
|
+
)
|
|
2197
|
+
);
|
|
2198
|
+
} else if (error instanceof Error) {
|
|
2199
|
+
parseErrors.push(createErrorEntry("PROCESSING_ERROR", "UNKNOWN", error.message, file));
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (documents.length === 0) {
|
|
2204
|
+
outputError2(parseErrors, useJson);
|
|
2205
|
+
exitWithCode(ExitCode.ERROR);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const searchResults = searchMultipleDocuments(documents, query, {
|
|
2209
|
+
maxResults: 10,
|
|
2210
|
+
minScore: 0.3
|
|
2211
|
+
});
|
|
2212
|
+
if (searchResults.length === 0) {
|
|
2213
|
+
const unresolved = [
|
|
2214
|
+
{
|
|
2215
|
+
selector: query,
|
|
2216
|
+
reason: `No matches found for search query "${query}"`,
|
|
2217
|
+
suggestions: []
|
|
2218
|
+
}
|
|
2219
|
+
];
|
|
2220
|
+
if (useJson) {
|
|
2221
|
+
const response = formatSelectResponse([], unresolved);
|
|
2222
|
+
console.log(JSON.stringify(response));
|
|
2223
|
+
} else {
|
|
2224
|
+
console.log(formatSearchText(query, []));
|
|
2225
|
+
}
|
|
2226
|
+
exitWithCode(ExitCode.ERROR);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
if (useJson) {
|
|
2230
|
+
const matches = formatSearchResultsAsMatches(searchResults);
|
|
2231
|
+
const response = {
|
|
2232
|
+
success: true,
|
|
2233
|
+
command: "search",
|
|
2234
|
+
query,
|
|
2235
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2236
|
+
data: {
|
|
2237
|
+
matches,
|
|
2238
|
+
unresolved: []
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
console.log(JSON.stringify(response));
|
|
2242
|
+
} else {
|
|
2243
|
+
console.log(formatSearchText(query, searchResults));
|
|
2244
|
+
}
|
|
2245
|
+
exitWithCode(ExitCode.SUCCESS);
|
|
2246
|
+
}
|
|
2247
|
+
function formatSearchResultsAsMatches(results) {
|
|
2248
|
+
return results.map((result) => ({
|
|
2249
|
+
selector: result.selector,
|
|
2250
|
+
type: result.type,
|
|
2251
|
+
content: result.content,
|
|
2252
|
+
truncated: false,
|
|
2253
|
+
children_available: [],
|
|
2254
|
+
search_score: result.score,
|
|
2255
|
+
match_type: result.matchType
|
|
2256
|
+
}));
|
|
2257
|
+
}
|
|
2258
|
+
function formatSearchText(query, results) {
|
|
2259
|
+
if (results.length === 0) {
|
|
2260
|
+
return `No matches found for: ${query}`;
|
|
2261
|
+
}
|
|
2262
|
+
const parts = [];
|
|
2263
|
+
parts.push(`Search results for "${query}":`);
|
|
2264
|
+
parts.push("");
|
|
2265
|
+
for (const result of results) {
|
|
2266
|
+
const score = Math.round(result.score * 100);
|
|
2267
|
+
parts.push(`${result.selector} (${score}% match)`);
|
|
2268
|
+
parts.push(` ${result.preview}`);
|
|
2269
|
+
parts.push("");
|
|
2270
|
+
}
|
|
2271
|
+
return parts.join("\n").trimEnd();
|
|
2272
|
+
}
|
|
1948
2273
|
|
|
1949
2274
|
// src/cli/commands/format-command.ts
|
|
1950
2275
|
function formatCommand(command, options = {}) {
|
|
@@ -2023,7 +2348,17 @@ function partitionArgs(args) {
|
|
|
2023
2348
|
return { files, selectors };
|
|
2024
2349
|
}
|
|
2025
2350
|
var program = new Command();
|
|
2026
|
-
program.name("mdsel").description(
|
|
2351
|
+
program.name("mdsel").description(
|
|
2352
|
+
`${pkg.description}
|
|
2353
|
+
|
|
2354
|
+
Examples:
|
|
2355
|
+
mdsel README.md Index document structure
|
|
2356
|
+
mdsel h2.1 README.md Select second h2 section
|
|
2357
|
+
mdsel '*' README.md Select entire document
|
|
2358
|
+
mdsel "h2.1/code.0" README.md Select nested content
|
|
2359
|
+
mdsel "installation" README.md Fuzzy search
|
|
2360
|
+
mdsel --json README.md Output as JSON`
|
|
2361
|
+
).version(pkg.version).option("--json", "Output JSON instead of text").argument("[args...]", "Markdown files and selectors (auto-detected)").action(async (args) => {
|
|
2027
2362
|
try {
|
|
2028
2363
|
const globalOpts = program.opts();
|
|
2029
2364
|
if (args.length === 0) {
|
|
@@ -2057,7 +2392,7 @@ program.name("mdsel").description(pkg.description).version(pkg.version).option("
|
|
|
2057
2392
|
program.command("format").description("Output format specification for tool descriptions").argument("[command]", "Command to describe (index, select, or omit for all)").option("--example", "Show example output instead of terse spec").action((command, options) => {
|
|
2058
2393
|
formatCommand(command, options);
|
|
2059
2394
|
});
|
|
2060
|
-
program.command("index").description("Index markdown files (
|
|
2395
|
+
program.command("index").description("Index markdown files (alternative to: mdsel <files>)").argument("<files...>", "Markdown files to index").action(async (files) => {
|
|
2061
2396
|
try {
|
|
2062
2397
|
const globalOpts = program.opts();
|
|
2063
2398
|
await indexCommand(files, { json: globalOpts.json });
|
|
@@ -2066,7 +2401,7 @@ program.command("index").description("Index markdown files (optional - same as j
|
|
|
2066
2401
|
process.exit(ExitCode.ERROR);
|
|
2067
2402
|
}
|
|
2068
2403
|
});
|
|
2069
|
-
program.command("select").description("Select content
|
|
2404
|
+
program.command("select").description("Select content (alternative to: mdsel <selector> <files>)").argument("<selector>", "Selector to match").argument("<files...>", "Markdown files to search").action(async (selector, files) => {
|
|
2070
2405
|
try {
|
|
2071
2406
|
const globalOpts = program.opts();
|
|
2072
2407
|
const selectors = splitSelectorList(selector);
|