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
- if (trimmed.startsWith('```')) {
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 language = trimmed.slice(3).trim() || undefined
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().startsWith('```')) {
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 = localStorage.getItem(STORAGE_KEY)
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-annotator",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Browser-based Markdown annotator for AI-assisted review",
5
5
  "type": "module",
6
6
  "bin": {