md-annotator 0.6.0 → 0.8.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 +12 -5
- package/client/dist/index.html +626 -441
- package/client/src/utils/parser.js +51 -1
- package/package.json +3 -1
- package/server/config.js +18 -0
- package/server/feedback.js +18 -2
- package/server/routes.js +2 -1
|
@@ -12,6 +12,12 @@ const HTML_VOID_TAGS = new Set([
|
|
|
12
12
|
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
13
13
|
])
|
|
14
14
|
|
|
15
|
+
// HTML tags that can contain markdown content (e.g. <div align="center">)
|
|
16
|
+
const HTML_MIXED_CONTENT_TAGS = new Set([
|
|
17
|
+
'div', 'section', 'details', 'aside', 'article', 'figure', 'figcaption',
|
|
18
|
+
'header', 'footer', 'main', 'nav', 'center'
|
|
19
|
+
])
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* Simplified markdown parser that splits content into linear blocks.
|
|
17
23
|
* Designed for predictable text-anchoring (not AST-based).
|
|
@@ -215,12 +221,56 @@ export function parseMarkdownToBlocks(markdown) {
|
|
|
215
221
|
flush()
|
|
216
222
|
const tagName = tagMatch[1].toLowerCase()
|
|
217
223
|
const htmlStartLine = currentLineNum
|
|
218
|
-
const htmlLines = [line]
|
|
219
224
|
|
|
220
225
|
const isSelfClosing = trimmed.endsWith('/>')
|
|
221
226
|
const isVoid = HTML_VOID_TAGS.has(tagName)
|
|
222
227
|
const hasSameLineClose = new RegExp(`</${tagName}\\s*>`, 'i').test(trimmed)
|
|
223
228
|
|
|
229
|
+
if (!isSelfClosing && !isVoid && !hasSameLineClose && HTML_MIXED_CONTENT_TAGS.has(tagName)) {
|
|
230
|
+
// Mixed content wrapper: split into open tag + inner markdown + close tag
|
|
231
|
+
const closePattern = new RegExp(`</${tagName}\\s*>`, 'i')
|
|
232
|
+
const openPattern = new RegExp(`<${tagName}[\\s>/]`, 'i')
|
|
233
|
+
const innerLines = []
|
|
234
|
+
let closingLine = null
|
|
235
|
+
let depth = 1
|
|
236
|
+
i++
|
|
237
|
+
while (i < lines.length) {
|
|
238
|
+
if (openPattern.test(lines[i].trim())) { depth++ }
|
|
239
|
+
if (closePattern.test(lines[i].trim())) {
|
|
240
|
+
depth--
|
|
241
|
+
if (depth === 0) { closingLine = lines[i]; break }
|
|
242
|
+
}
|
|
243
|
+
innerLines.push(lines[i])
|
|
244
|
+
i++
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Emit opening tag
|
|
248
|
+
blocks.push({
|
|
249
|
+
id: `block-${currentId++}`,
|
|
250
|
+
type: 'html',
|
|
251
|
+
content: line,
|
|
252
|
+
order: currentId,
|
|
253
|
+
startLine: htmlStartLine
|
|
254
|
+
})
|
|
255
|
+
// Recursively parse inner content as markdown
|
|
256
|
+
for (const inner of parseMarkdownToBlocks(innerLines.join('\n'))) {
|
|
257
|
+
blocks.push({ ...inner, id: `block-${currentId++}`, order: currentId, startLine: htmlStartLine + inner.startLine })
|
|
258
|
+
}
|
|
259
|
+
// Emit closing tag
|
|
260
|
+
if (closingLine) {
|
|
261
|
+
blocks.push({
|
|
262
|
+
id: `block-${currentId++}`,
|
|
263
|
+
type: 'html',
|
|
264
|
+
content: closingLine,
|
|
265
|
+
order: currentId,
|
|
266
|
+
startLine: htmlStartLine + innerLines.length + 1
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Non-mixed HTML: collect everything as one opaque block
|
|
273
|
+
const htmlLines = [line]
|
|
224
274
|
if (!isSelfClosing && !isVoid && !hasSameLineClose) {
|
|
225
275
|
const closePattern = new RegExp(`</${tagName}\\s*>`, 'i')
|
|
226
276
|
const openPattern = new RegExp(`<${tagName}[\\s>/]`, 'i')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-annotator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Browser-based Markdown annotator for AI-assisted review",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"highlight.js": "^11.11.0",
|
|
42
42
|
"mermaid": "^11.12.3",
|
|
43
43
|
"open": "^10.1.0",
|
|
44
|
+
"pako": "^2.1.0",
|
|
45
|
+
"plantuml-encoder": "^1.4.0",
|
|
44
46
|
"portfinder": "^1.0.32",
|
|
45
47
|
"react": "^19.0.0",
|
|
46
48
|
"react-dom": "^19.0.0",
|
package/server/config.js
CHANGED
|
@@ -27,10 +27,28 @@ function getHeartbeatTimeoutMs() {
|
|
|
27
27
|
return DEFAULT_HEARTBEAT_TIMEOUT_MS
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function getPlantumlServerUrl() {
|
|
31
|
+
const envUrl = process.env.PLANTUML_SERVER_URL
|
|
32
|
+
if (envUrl) {
|
|
33
|
+
return envUrl.replace(/\/+$/, '')
|
|
34
|
+
}
|
|
35
|
+
return 'https://www.plantuml.com/plantuml'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getKrokiServerUrl() {
|
|
39
|
+
const envUrl = process.env.KROKI_SERVER_URL
|
|
40
|
+
if (envUrl) {
|
|
41
|
+
return envUrl.replace(/\/+$/, '')
|
|
42
|
+
}
|
|
43
|
+
return 'https://kroki.io'
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
export const config = {
|
|
31
47
|
port: getServerPort(),
|
|
32
48
|
browser: process.env.MD_ANNOTATOR_BROWSER || null,
|
|
33
49
|
heartbeatTimeoutMs: getHeartbeatTimeoutMs(),
|
|
34
50
|
forceExitTimeoutMs: 5000,
|
|
35
51
|
jsonLimit: '10mb',
|
|
52
|
+
plantumlServerUrl: getPlantumlServerUrl(),
|
|
53
|
+
krokiServerUrl: getKrokiServerUrl(),
|
|
36
54
|
}
|
package/server/feedback.js
CHANGED
|
@@ -20,11 +20,27 @@ function formatAnnotation(ann, block, heading) {
|
|
|
20
20
|
return output + '\n'
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
if (ann.targetType === 'pinpoint') {
|
|
24
|
+
const isDeletion = ann.type === 'DELETION'
|
|
25
|
+
const label = isDeletion ? 'Remove block' : 'Comment on block'
|
|
26
|
+
let output = `${heading} ${label} (Line ${blockStartLine})\n`
|
|
27
|
+
const preview = (block?.content || ann.originalText || '').slice(0, 200)
|
|
28
|
+
output += `\`\`\`\n${preview}\n\`\`\`\n`
|
|
29
|
+
if (isDeletion) {
|
|
30
|
+
output += `> User wants this block removed from the document.\n`
|
|
31
|
+
} else {
|
|
32
|
+
output += `> ${(ann.text ?? '').replace(/\n/g, '\n> ')}\n`
|
|
33
|
+
}
|
|
34
|
+
return output + '\n'
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
if (ann.targetType === 'diagram') {
|
|
24
38
|
const isDeletion = ann.type === 'DELETION'
|
|
25
|
-
const
|
|
39
|
+
const diagramLang = block?.language === 'plantuml' ? 'PlantUML' : 'Mermaid'
|
|
40
|
+
const fence = block?.language === 'plantuml' ? 'plantuml' : 'mermaid'
|
|
41
|
+
const label = isDeletion ? `Remove ${diagramLang} diagram` : `Comment on ${diagramLang} diagram`
|
|
26
42
|
let output = `${heading} ${label} (Line ${blockStartLine})\n`
|
|
27
|
-
output +=
|
|
43
|
+
output += `\`\`\`${fence}\n${block?.content || ann.originalText}\n\`\`\`\n`
|
|
28
44
|
if (isDeletion) {
|
|
29
45
|
output += `> User wants this diagram removed from the document.\n`
|
|
30
46
|
} else {
|
package/server/routes.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto'
|
|
|
4
4
|
import { readMarkdownFile, isMarkdownFile } from './file.js'
|
|
5
5
|
import { exportFeedback, exportMultiFileFeedback } from './feedback.js'
|
|
6
6
|
import { listWorkspaceFiles } from './workspace.js'
|
|
7
|
+
import { config } from './config.js'
|
|
7
8
|
|
|
8
9
|
function success(data) {
|
|
9
10
|
return { success: true, data }
|
|
@@ -43,7 +44,7 @@ export function createApiRouter(filePaths, resolveDecision, origin = 'cli', stor
|
|
|
43
44
|
}
|
|
44
45
|
})
|
|
45
46
|
)
|
|
46
|
-
res.json(success({ files, origin }))
|
|
47
|
+
res.json(success({ files, origin, config: { plantumlServerUrl: config.plantumlServerUrl, krokiServerUrl: config.krokiServerUrl } }))
|
|
47
48
|
} catch (error) {
|
|
48
49
|
res.status(500).json(failure(error.message))
|
|
49
50
|
}
|