md-annotator 0.10.0 → 0.11.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.
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-based search across multiple file contents.
|
|
3
|
+
* Returns results grouped by file with line numbers and context snippets.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CONTEXT_CHARS = 40
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ path: string, content: string }[]} files
|
|
10
|
+
* @param {string} query
|
|
11
|
+
* @returns {{ fileIndex: number, filePath: string, matches: { line: number, offset: number, context: string }[] }[]}
|
|
12
|
+
*/
|
|
13
|
+
export function searchAcrossFiles(files, query) {
|
|
14
|
+
if (!query || !files?.length) { return [] }
|
|
15
|
+
|
|
16
|
+
const lowerQuery = query.toLowerCase()
|
|
17
|
+
const results = []
|
|
18
|
+
|
|
19
|
+
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
20
|
+
const file = files[fileIndex]
|
|
21
|
+
const content = file.content || ''
|
|
22
|
+
const lowerContent = content.toLowerCase()
|
|
23
|
+
const matches = []
|
|
24
|
+
let searchIdx = 0
|
|
25
|
+
|
|
26
|
+
while ((searchIdx = lowerContent.indexOf(lowerQuery, searchIdx)) !== -1) {
|
|
27
|
+
const line = content.slice(0, searchIdx).split('\n').length
|
|
28
|
+
const lineStart = content.lastIndexOf('\n', searchIdx - 1) + 1
|
|
29
|
+
const lineEnd = content.indexOf('\n', searchIdx)
|
|
30
|
+
const lineText = content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd)
|
|
31
|
+
|
|
32
|
+
const offsetInLine = searchIdx - lineStart
|
|
33
|
+
const contextStart = Math.max(0, offsetInLine - CONTEXT_CHARS)
|
|
34
|
+
const contextEnd = Math.min(lineText.length, offsetInLine + query.length + CONTEXT_CHARS)
|
|
35
|
+
const prefix = contextStart > 0 ? '...' : ''
|
|
36
|
+
const suffix = contextEnd < lineText.length ? '...' : ''
|
|
37
|
+
const context = prefix + lineText.slice(contextStart, contextEnd) + suffix
|
|
38
|
+
|
|
39
|
+
matches.push({ line, offset: searchIdx, context })
|
|
40
|
+
searchIdx += lowerQuery.length
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (matches.length > 0) {
|
|
44
|
+
results.push({
|
|
45
|
+
fileIndex,
|
|
46
|
+
filePath: file.path,
|
|
47
|
+
matches,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
}
|
|
@@ -178,14 +178,17 @@ export function parseMarkdownToBlocks(markdown, { allowFrontmatter = true } = {}
|
|
|
178
178
|
continue
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
// Code blocks
|
|
182
|
-
|
|
181
|
+
// Code blocks (supports nested fences per CommonMark: closing fence must have >= opening backtick count)
|
|
182
|
+
const fenceMatch = trimmed.match(/^(`{3,})/)
|
|
183
|
+
if (fenceMatch) {
|
|
183
184
|
flush()
|
|
184
185
|
const codeStartLine = currentLineNum
|
|
185
|
-
const
|
|
186
|
+
const fenceLen = fenceMatch[1].length
|
|
187
|
+
const language = trimmed.slice(fenceLen).trim() || undefined
|
|
188
|
+
const closingPattern = new RegExp(`^\`{${fenceLen},}$`)
|
|
186
189
|
const codeContent = []
|
|
187
190
|
i++
|
|
188
|
-
while (i < lines.length && !lines[i].trim()
|
|
191
|
+
while (i < lines.length && !closingPattern.test(lines[i].trim())) {
|
|
189
192
|
codeContent.push(lines[i])
|
|
190
193
|
i++
|
|
191
194
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Quick annotation labels for fast categorization.
|
|
3
3
|
* Alt+1–0 shortcuts apply a label directly to the current selection.
|
|
4
4
|
*/
|
|
5
|
+
import { getItem } from './storage.js'
|
|
5
6
|
|
|
6
7
|
const STORAGE_KEY = 'md-annotator-quick-labels'
|
|
7
8
|
|
|
@@ -33,7 +34,7 @@ export const DEFAULT_LABELS = [
|
|
|
33
34
|
|
|
34
35
|
export function getQuickLabels() {
|
|
35
36
|
try {
|
|
36
|
-
const stored =
|
|
37
|
+
const stored = getItem(STORAGE_KEY)
|
|
37
38
|
if (stored) {
|
|
38
39
|
const parsed = JSON.parse(stored)
|
|
39
40
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie-based storage utility
|
|
3
|
+
*
|
|
4
|
+
* Uses cookies instead of localStorage so settings persist across
|
|
5
|
+
* different ports (each invocation uses a random port).
|
|
6
|
+
* Cookies are scoped by domain, not port, so localhost:54321 and
|
|
7
|
+
* localhost:54322 share the same cookies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365
|
|
11
|
+
|
|
12
|
+
function escapeRegex(str) {
|
|
13
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getItem(key) {
|
|
17
|
+
try {
|
|
18
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${escapeRegex(key)}=([^;]*)`))
|
|
19
|
+
return match ? decodeURIComponent(match[1]) : null
|
|
20
|
+
} catch {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setItem(key, value) {
|
|
26
|
+
try {
|
|
27
|
+
const encoded = encodeURIComponent(value)
|
|
28
|
+
document.cookie = `${key}=${encoded}; path=/; max-age=${ONE_YEAR_SECONDS}; SameSite=Lax`
|
|
29
|
+
} catch {
|
|
30
|
+
// Cookie not available
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function removeItem(key) {
|
|
35
|
+
try {
|
|
36
|
+
document.cookie = `${key}=; path=/; max-age=0`
|
|
37
|
+
} catch {
|
|
38
|
+
// Cookie not available
|
|
39
|
+
}
|
|
40
|
+
}
|