mrmd-project 0.1.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 +335 -0
- package/package.json +46 -0
- package/spec.md +1056 -0
- package/src/assets.js +142 -0
- package/src/fsml.js +371 -0
- package/src/index.js +65 -0
- package/src/links.js +198 -0
- package/src/project.js +293 -0
- package/src/scaffold.js +136 -0
- package/src/search.js +177 -0
package/src/search.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search module - Fuzzy search utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides fuzzy matching for file paths.
|
|
5
|
+
*
|
|
6
|
+
* @module Search
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fuzzy match a query against a string
|
|
11
|
+
*
|
|
12
|
+
* @param {string} query - Search query
|
|
13
|
+
* @param {string} target - String to match against
|
|
14
|
+
* @returns {object} Match result with score and matched indices
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* Search.fuzzyMatch('instal', 'installation')
|
|
18
|
+
* // Returns { score: 15, matches: [0, 1, 2, 3, 4, 5] }
|
|
19
|
+
*
|
|
20
|
+
* Search.fuzzyMatch('xyz', 'installation')
|
|
21
|
+
* // Returns { score: 0, matches: [] }
|
|
22
|
+
*/
|
|
23
|
+
export function fuzzyMatch(query, target) {
|
|
24
|
+
if (!query) return { score: 0, matches: [] };
|
|
25
|
+
|
|
26
|
+
const queryLower = query.toLowerCase();
|
|
27
|
+
const targetLower = target.toLowerCase();
|
|
28
|
+
|
|
29
|
+
let queryIndex = 0;
|
|
30
|
+
let score = 0;
|
|
31
|
+
const matches = [];
|
|
32
|
+
let prevMatchIndex = -1;
|
|
33
|
+
let consecutive = 0;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < target.length && queryIndex < query.length; i++) {
|
|
36
|
+
if (targetLower[i] === queryLower[queryIndex]) {
|
|
37
|
+
matches.push(i);
|
|
38
|
+
|
|
39
|
+
// Scoring
|
|
40
|
+
if (prevMatchIndex === i - 1) {
|
|
41
|
+
// Consecutive match
|
|
42
|
+
consecutive++;
|
|
43
|
+
score += 2 + consecutive;
|
|
44
|
+
} else {
|
|
45
|
+
consecutive = 0;
|
|
46
|
+
score += 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Bonus for word boundaries
|
|
50
|
+
if (i === 0 || '/._- '.includes(target[i - 1])) {
|
|
51
|
+
score += 5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Bonus for camelCase
|
|
55
|
+
if (i > 0 && target[i] === target[i].toUpperCase() &&
|
|
56
|
+
target[i - 1] === target[i - 1].toLowerCase() &&
|
|
57
|
+
target[i] !== target[i].toLowerCase()) {
|
|
58
|
+
score += 3;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
prevMatchIndex = i;
|
|
62
|
+
queryIndex++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// All query characters must match
|
|
67
|
+
if (queryIndex !== query.length) {
|
|
68
|
+
return { score: 0, matches: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { score, matches };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Search files by full path
|
|
76
|
+
*
|
|
77
|
+
* Splits query into tokens and matches each against the path.
|
|
78
|
+
* Results are ranked by match quality.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} query - Search query (space-separated tokens)
|
|
81
|
+
* @param {string[]} paths - Array of file paths
|
|
82
|
+
* @returns {object[]} Search results with scores and match info
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* Search.files('thesis readme', ['/home/user/thesis/README.md', '/home/user/other/README.md'])
|
|
86
|
+
* // Returns [
|
|
87
|
+
* // { path: '/home/user/thesis/README.md', score: 25, ... },
|
|
88
|
+
* // { path: '/home/user/other/README.md', score: 10, ... }
|
|
89
|
+
* // ]
|
|
90
|
+
*/
|
|
91
|
+
export function files(query, paths) {
|
|
92
|
+
if (!query.trim()) {
|
|
93
|
+
// Return all files with no scoring
|
|
94
|
+
return paths.map(path => ({
|
|
95
|
+
path,
|
|
96
|
+
score: 0,
|
|
97
|
+
nameMatches: [],
|
|
98
|
+
dirMatches: [],
|
|
99
|
+
...parsePath(path),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
104
|
+
const results = [];
|
|
105
|
+
|
|
106
|
+
for (const path of paths) {
|
|
107
|
+
const pathLower = path.toLowerCase();
|
|
108
|
+
const { name, dir } = parsePath(path);
|
|
109
|
+
|
|
110
|
+
let totalScore = 0;
|
|
111
|
+
let allMatched = true;
|
|
112
|
+
const nameMatches = [];
|
|
113
|
+
const dirMatches = [];
|
|
114
|
+
|
|
115
|
+
for (const token of tokens) {
|
|
116
|
+
// Try matching against name first (higher weight)
|
|
117
|
+
const nameResult = fuzzyMatch(token, name);
|
|
118
|
+
if (nameResult.score > 0) {
|
|
119
|
+
totalScore += nameResult.score * 2; // Name matches worth more
|
|
120
|
+
nameMatches.push(...nameResult.matches);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try matching against full path
|
|
125
|
+
const pathResult = fuzzyMatch(token, path);
|
|
126
|
+
if (pathResult.score > 0) {
|
|
127
|
+
totalScore += pathResult.score;
|
|
128
|
+
|
|
129
|
+
// Split matches into dir and name portions
|
|
130
|
+
const dirLength = path.lastIndexOf('/') + 1;
|
|
131
|
+
for (const idx of pathResult.matches) {
|
|
132
|
+
if (idx < dirLength) {
|
|
133
|
+
dirMatches.push(idx);
|
|
134
|
+
} else {
|
|
135
|
+
nameMatches.push(idx - dirLength);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Token didn't match
|
|
142
|
+
allMatched = false;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (allMatched && totalScore > 0) {
|
|
147
|
+
results.push({
|
|
148
|
+
path,
|
|
149
|
+
score: totalScore,
|
|
150
|
+
name,
|
|
151
|
+
dir,
|
|
152
|
+
nameMatches: [...new Set(nameMatches)].sort((a, b) => a - b),
|
|
153
|
+
dirMatches: [...new Set(dirMatches)].sort((a, b) => a - b),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Sort by score descending
|
|
159
|
+
results.sort((a, b) => b.score - a.score);
|
|
160
|
+
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse a path into name and directory components
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
function parsePath(path) {
|
|
169
|
+
// Simplify home directory
|
|
170
|
+
const displayPath = path.replace(/^\/home\/[^/]+/, '~');
|
|
171
|
+
const lastSlash = displayPath.lastIndexOf('/');
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
name: lastSlash >= 0 ? displayPath.slice(lastSlash + 1) : displayPath,
|
|
175
|
+
dir: lastSlash >= 0 ? displayPath.slice(0, lastSlash) : '',
|
|
176
|
+
};
|
|
177
|
+
}
|