md-annotator 0.8.0 → 0.9.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/client/dist/index.html +371 -369
- package/package.json +1 -2
- package/server/annotator.js +20 -13
- package/server/config.js +1 -0
- package/server/feedback.js +31 -2
- package/client/src/utils/storage.js +0 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-annotator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Browser-based Markdown annotator for AI-assisted review",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"open": "^10.1.0",
|
|
44
44
|
"pako": "^2.1.0",
|
|
45
45
|
"plantuml-encoder": "^1.4.0",
|
|
46
|
-
"portfinder": "^1.0.32",
|
|
47
46
|
"react": "^19.0.0",
|
|
48
47
|
"react-dom": "^19.0.0",
|
|
49
48
|
"web-highlighter": "^0.7.4"
|
package/server/annotator.js
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* Provides startAnnotatorServer() for both CLI and plugin usage.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync } from 'node:fs'
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
7
7
|
import { createHash } from 'node:crypto'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import { dirname, join } from 'node:path'
|
|
10
10
|
import express from 'express'
|
|
11
11
|
import cors from 'cors'
|
|
12
|
-
import portfinder from 'portfinder'
|
|
13
12
|
import { config } from './config.js'
|
|
14
13
|
import { createApiRouter } from './routes.js'
|
|
15
14
|
import { readMarkdownFile } from './file.js'
|
|
@@ -19,6 +18,15 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
19
18
|
const DIST_PATH = join(__dirname, '..', 'client', 'dist')
|
|
20
19
|
const DEV_PATH = join(__dirname, '..', 'client')
|
|
21
20
|
|
|
21
|
+
// Pre-load index.html into memory at module load time (avoids per-request disk I/O)
|
|
22
|
+
const DIST_INDEX = join(DIST_PATH, 'index.html')
|
|
23
|
+
const DEV_INDEX = join(DEV_PATH, 'index.html')
|
|
24
|
+
const preloadedHtml = existsSync(DIST_INDEX)
|
|
25
|
+
? readFileSync(DIST_INDEX, 'utf-8')
|
|
26
|
+
: existsSync(DEV_INDEX)
|
|
27
|
+
? readFileSync(DEV_INDEX, 'utf-8')
|
|
28
|
+
: null
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
31
|
* Resolve notes for a specific file from the feedbackNotes array.
|
|
24
32
|
* Notes apply to the first file only (multi-file uses one invocation per file).
|
|
@@ -59,14 +67,14 @@ export async function startAnnotatorServer(options) {
|
|
|
59
67
|
app.use(cors())
|
|
60
68
|
app.use(express.json({ limit: config.jsonLimit }))
|
|
61
69
|
|
|
62
|
-
// Serve
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
// Serve HTML from memory (pre-loaded or embedded)
|
|
71
|
+
const html = htmlContent || preloadedHtml
|
|
72
|
+
if (html) {
|
|
65
73
|
app.get('/', (_req, res) => {
|
|
66
|
-
res.type('html').send(
|
|
74
|
+
res.type('html').send(html)
|
|
67
75
|
})
|
|
68
76
|
} else {
|
|
69
|
-
//
|
|
77
|
+
// Fallback: serve from disk (dev mode without built index.html)
|
|
70
78
|
const clientPath = existsSync(DIST_PATH) ? DIST_PATH : DEV_PATH
|
|
71
79
|
app.use(express.static(clientPath))
|
|
72
80
|
}
|
|
@@ -131,15 +139,14 @@ export async function startAnnotatorServer(options) {
|
|
|
131
139
|
// API routes with multi-file support
|
|
132
140
|
app.use(createApiRouter(filePaths, safeResolve, origin, stores))
|
|
133
141
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
// Start server
|
|
142
|
+
// Start server — use port 0 to let the OS assign a free port instantly,
|
|
143
|
+
// falling back to configured port if explicitly set via MD_ANNOTATOR_PORT
|
|
144
|
+
const requestedPort = config.portExplicit ? config.port : 0
|
|
139
145
|
const server = await new Promise((resolve) => {
|
|
140
|
-
const s = app.listen(
|
|
146
|
+
const s = app.listen(requestedPort, () => resolve(s))
|
|
141
147
|
})
|
|
142
148
|
|
|
149
|
+
const port = server.address().port
|
|
143
150
|
const url = `http://localhost:${port}`
|
|
144
151
|
|
|
145
152
|
// Call onReady callback if provided
|
package/server/config.js
CHANGED
|
@@ -45,6 +45,7 @@ function getKrokiServerUrl() {
|
|
|
45
45
|
|
|
46
46
|
export const config = {
|
|
47
47
|
port: getServerPort(),
|
|
48
|
+
portExplicit: !!process.env.MD_ANNOTATOR_PORT,
|
|
48
49
|
browser: process.env.MD_ANNOTATOR_BROWSER || null,
|
|
49
50
|
heartbeatTimeoutMs: getHeartbeatTimeoutMs(),
|
|
50
51
|
forceExitTimeoutMs: 5000,
|
package/server/feedback.js
CHANGED
|
@@ -49,6 +49,27 @@ function formatAnnotation(ann, block, heading) {
|
|
|
49
49
|
return output + '\n'
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Source view annotations — blockId is "source-line-N" (0-indexed)
|
|
53
|
+
if (ann.targetType === 'source') {
|
|
54
|
+
const lineMatch = ann.blockId?.match(/^source-line-(\d+)$/)
|
|
55
|
+
const startLine = lineMatch ? parseInt(lineMatch[1], 10) + 1 : 1
|
|
56
|
+
const newlinesInSelection = (ann.originalText.match(/\n/g) || []).length
|
|
57
|
+
const endLine = startLine + newlinesInSelection
|
|
58
|
+
const lineRef = startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`
|
|
59
|
+
|
|
60
|
+
let output = `${heading} `
|
|
61
|
+
if (ann.type === 'DELETION') {
|
|
62
|
+
output += `Remove this (${lineRef}, source)\n`
|
|
63
|
+
output += `\`\`\`\n${ann.originalText}\n\`\`\`\n`
|
|
64
|
+
output += `> User wants this removed from the document.\n`
|
|
65
|
+
} else if (ann.type === 'COMMENT') {
|
|
66
|
+
output += `Comment on (${lineRef}, source)\n`
|
|
67
|
+
output += `\`\`\`\n${ann.originalText}\n\`\`\`\n`
|
|
68
|
+
output += `> ${(ann.text ?? '').replace(/\n/g, '\n> ')}\n`
|
|
69
|
+
}
|
|
70
|
+
return output + '\n'
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
const blockContent = block?.content || ''
|
|
53
74
|
const textBeforeSelection = blockContent.slice(0, ann.startOffset)
|
|
54
75
|
const linesBeforeSelection = (textBeforeSelection.match(/\n/g) || []).length
|
|
@@ -79,10 +100,18 @@ function formatAnnotation(ann, block, heading) {
|
|
|
79
100
|
return output + '\n'
|
|
80
101
|
}
|
|
81
102
|
|
|
103
|
+
function getBlockOrder(blockId, blocks) {
|
|
104
|
+
const sourceMatch = blockId?.match(/^source-line-(\d+)$/)
|
|
105
|
+
if (sourceMatch) { return parseInt(sourceMatch[1], 10) + 1 }
|
|
106
|
+
const block = blocks.find(blk => blk.id === blockId)
|
|
107
|
+
if (block?.startLine) { return block.startLine }
|
|
108
|
+
return Infinity
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
function sortAnnotations(annotations, blocks) {
|
|
83
112
|
return [...annotations].sort((a, b) => {
|
|
84
|
-
const blockA =
|
|
85
|
-
const blockB =
|
|
113
|
+
const blockA = getBlockOrder(a.blockId, blocks)
|
|
114
|
+
const blockB = getBlockOrder(b.blockId, blocks)
|
|
86
115
|
if (blockA !== blockB) {return blockA - blockB}
|
|
87
116
|
return a.startOffset - b.startOffset
|
|
88
117
|
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cookie-based storage utility.
|
|
3
|
-
*
|
|
4
|
-
* Uses cookies instead of localStorage so settings persist across
|
|
5
|
-
* different ports (each session 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
|
-
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
|
-
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
|
-
const AUTO_CLOSE_KEY = 'md-annotator-auto-close'
|
|
35
|
-
|
|
36
|
-
export function getAutoCloseDelay() {
|
|
37
|
-
const val = getItem(AUTO_CLOSE_KEY)
|
|
38
|
-
if (val === '0' || val === '3' || val === '5') {return val}
|
|
39
|
-
return 'off'
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function setAutoCloseDelay(delay) {
|
|
43
|
-
setItem(AUTO_CLOSE_KEY, delay)
|
|
44
|
-
}
|