@mrsf/markdown-it-mrsf 0.2.0 → 0.4.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 +15 -4
- package/dist/browser.js +29 -208
- package/dist/browser.js.map +3 -3
- package/dist/controller.d.ts +139 -13
- package/dist/controller.js +847 -10
- package/dist/controller.js.map +3 -3
- package/dist/html.d.ts +13 -0
- package/dist/index.js +29 -208
- package/dist/index.js.map +3 -3
- package/dist/rules/core.d.ts +12 -11
- package/dist/rules/renderer.d.ts +4 -4
- package/dist/style.css +205 -81
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,7 +112,12 @@ Customise with CSS custom properties:
|
|
|
112
112
|
|
|
113
113
|
## Interactive Mode
|
|
114
114
|
|
|
115
|
-
Enable `interactive: true` to add action buttons (Resolve, Reply, Edit) inside tooltips
|
|
115
|
+
Enable `interactive: true` to add action buttons (Resolve, Reply, Edit) inside tooltips, plus:
|
|
116
|
+
|
|
117
|
+
- Gutter “Add comment” buttons for each line badge
|
|
118
|
+
- An inline “Add comment” floater that appears when users select text
|
|
119
|
+
|
|
120
|
+
Buttons render as `<button data-mrsf-action="..." data-mrsf-comment-id="...">` elements — inert without JavaScript.
|
|
116
121
|
|
|
117
122
|
```ts
|
|
118
123
|
md.use(mrsfPlugin, {
|
|
@@ -135,11 +140,15 @@ document.addEventListener("mrsf:resolve", (e) => {
|
|
|
135
140
|
document.addEventListener("mrsf:reply", (e) => {
|
|
136
141
|
console.log("Reply to:", e.detail.commentId);
|
|
137
142
|
});
|
|
143
|
+
|
|
144
|
+
document.addEventListener("mrsf:add", (e) => {
|
|
145
|
+
console.log("Add on line", e.detail.line, "selection", e.detail.selectionText);
|
|
146
|
+
});
|
|
138
147
|
```
|
|
139
148
|
|
|
140
|
-
Events dispatched: `mrsf:resolve`, `mrsf:unresolve`, `mrsf:reply`, `mrsf:edit`, `mrsf:navigate`.
|
|
149
|
+
Events dispatched: `mrsf:add`, `mrsf:resolve`, `mrsf:unresolve`, `mrsf:reply`, `mrsf:edit`, `mrsf:navigate`.
|
|
141
150
|
|
|
142
|
-
Each event's `detail` contains `{ commentId: string, line: number | null, action: string }
|
|
151
|
+
Each event's `detail` contains `{ commentId: string | null, line: number | null, action: string, selectionText?: string | null, start_line?: number | null, end_line?: number | null, start_column?: number | null, end_column?: number | null }` (snake_case to align with the CLI SDK parameters).
|
|
143
152
|
|
|
144
153
|
## Options
|
|
145
154
|
|
|
@@ -154,6 +163,7 @@ Each event's `detail` contains `{ commentId: string, line: number | null, action
|
|
|
154
163
|
| `gutterPosition` | `'left' \| 'tight' \| 'right'` | `'right'` | Badge position: left margin gutter, tight (before text), or floated right |
|
|
155
164
|
| `gutterForInline` | `boolean` | `true` | Show gutter badge for comments that also have inline highlights |
|
|
156
165
|
| `inlineHighlights` | `boolean` | `true` | Render inline text highlights for `selected_text` |
|
|
166
|
+
| `lineHighlight` | `boolean` | `false` | Add background tint on commented lines |
|
|
157
167
|
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Color scheme hint |
|
|
158
168
|
| `cwd` | `string` | `process.cwd()` | Working directory for file resolution |
|
|
159
169
|
|
|
@@ -171,7 +181,8 @@ All rendered elements follow a strict data-attribute contract:
|
|
|
171
181
|
|
|
172
182
|
- `data-mrsf-line` — source line number (1-based)
|
|
173
183
|
- `data-mrsf-comment-id` — comment identifier
|
|
174
|
-
- `data-mrsf-action` — action type (`resolve`, `unresolve`, `reply`, `edit`, `navigate`)
|
|
184
|
+
- `data-mrsf-action` — action type (`add`, `resolve`, `unresolve`, `reply`, `edit`, `navigate`)
|
|
185
|
+
- `data-mrsf-start-line`, `data-mrsf-end-line`, `data-mrsf-start-column`, `data-mrsf-end-column` — optional selection anchors when present
|
|
175
186
|
|
|
176
187
|
## License
|
|
177
188
|
|
package/dist/browser.js
CHANGED
|
@@ -53,117 +53,8 @@ function resolveComments(loader, options) {
|
|
|
53
53
|
return { lineMap, comments };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// ../shared/dist/html.js
|
|
57
|
-
function escapeHtml(str) {
|
|
58
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
59
|
-
}
|
|
60
|
-
function formatTime(iso) {
|
|
61
|
-
if (!iso)
|
|
62
|
-
return "";
|
|
63
|
-
try {
|
|
64
|
-
const d = new Date(iso);
|
|
65
|
-
return d.toLocaleDateString(void 0, {
|
|
66
|
-
year: "numeric",
|
|
67
|
-
month: "short",
|
|
68
|
-
day: "numeric"
|
|
69
|
-
});
|
|
70
|
-
} catch {
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function renderCommentHtml(comment, isReply, interactive) {
|
|
75
|
-
const resolvedClass = comment.resolved ? " mrsf-resolved" : "";
|
|
76
|
-
const replyClass = isReply ? " mrsf-reply" : "";
|
|
77
|
-
let html = `<span class="mrsf-comment${resolvedClass}${replyClass}" data-mrsf-comment-id="${escapeHtml(comment.id)}">`;
|
|
78
|
-
html += `<span class="mrsf-comment-header">`;
|
|
79
|
-
html += `<span class="mrsf-author">${escapeHtml(comment.author)}</span>`;
|
|
80
|
-
if (comment.timestamp) {
|
|
81
|
-
html += `<span class="mrsf-date">${escapeHtml(formatTime(comment.timestamp))}</span>`;
|
|
82
|
-
}
|
|
83
|
-
if (comment.severity) {
|
|
84
|
-
html += `<span class="mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}">${escapeHtml(comment.severity)}</span>`;
|
|
85
|
-
}
|
|
86
|
-
if (comment.type) {
|
|
87
|
-
html += `<span class="mrsf-type">${escapeHtml(comment.type)}</span>`;
|
|
88
|
-
}
|
|
89
|
-
if (comment.resolved) {
|
|
90
|
-
html += `<span class="mrsf-resolved-badge">\u2713 resolved</span>`;
|
|
91
|
-
}
|
|
92
|
-
html += `</span>`;
|
|
93
|
-
if (comment.selected_text) {
|
|
94
|
-
html += `<span class="mrsf-selected-text">${escapeHtml(comment.selected_text)}</span>`;
|
|
95
|
-
}
|
|
96
|
-
html += `<span class="mrsf-comment-body">${escapeHtml(comment.text)}</span>`;
|
|
97
|
-
if (interactive) {
|
|
98
|
-
const line = comment.line != null ? String(comment.line) : "";
|
|
99
|
-
html += `<span class="mrsf-actions">`;
|
|
100
|
-
if (comment.resolved) {
|
|
101
|
-
html += `<button class="mrsf-action-btn" data-mrsf-action="unresolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Unresolve</button>`;
|
|
102
|
-
} else {
|
|
103
|
-
html += `<button class="mrsf-action-btn" data-mrsf-action="resolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Resolve</button>`;
|
|
104
|
-
}
|
|
105
|
-
html += `<button class="mrsf-action-btn" data-mrsf-action="reply" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Reply</button>`;
|
|
106
|
-
html += `<button class="mrsf-action-btn" data-mrsf-action="edit" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Edit</button>`;
|
|
107
|
-
html += `</span>`;
|
|
108
|
-
}
|
|
109
|
-
html += `</span>`;
|
|
110
|
-
return html;
|
|
111
|
-
}
|
|
112
|
-
function renderThreadHtml(thread, interactive) {
|
|
113
|
-
let html = `<span class="mrsf-thread">`;
|
|
114
|
-
html += renderCommentHtml(thread.comment, false, interactive);
|
|
115
|
-
if (thread.replies.length > 0) {
|
|
116
|
-
html += `<span class="mrsf-replies">`;
|
|
117
|
-
for (const reply of thread.replies) {
|
|
118
|
-
html += renderCommentHtml(reply, true, interactive);
|
|
119
|
-
}
|
|
120
|
-
html += `</span>`;
|
|
121
|
-
}
|
|
122
|
-
html += `</span>`;
|
|
123
|
-
return html;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
56
|
// src/rules/core.ts
|
|
127
|
-
function
|
|
128
|
-
if (!token.children) return false;
|
|
129
|
-
const text = thread.comment.selected_text;
|
|
130
|
-
if (!text) return false;
|
|
131
|
-
const trimmed = text.trim();
|
|
132
|
-
if (!trimmed) return false;
|
|
133
|
-
for (let i = 0; i < token.children.length; i++) {
|
|
134
|
-
const child = token.children[i];
|
|
135
|
-
if (child.type !== "text") continue;
|
|
136
|
-
const idx = child.content.indexOf(trimmed);
|
|
137
|
-
if (idx === -1) continue;
|
|
138
|
-
const newTokens = [];
|
|
139
|
-
if (idx > 0) {
|
|
140
|
-
const before = new Token("text", "", 0);
|
|
141
|
-
before.content = child.content.slice(0, idx);
|
|
142
|
-
newTokens.push(before);
|
|
143
|
-
}
|
|
144
|
-
const open = new Token("mrsf_highlight_open", "mark", 1);
|
|
145
|
-
open.attrSet("class", "mrsf-highlight");
|
|
146
|
-
open.attrSet("data-mrsf-comment-id", thread.comment.id);
|
|
147
|
-
open.meta = { thread };
|
|
148
|
-
newTokens.push(open);
|
|
149
|
-
const matched = new Token("text", "", 0);
|
|
150
|
-
matched.content = trimmed;
|
|
151
|
-
newTokens.push(matched);
|
|
152
|
-
const close = new Token("mrsf_highlight_close", "mark", -1);
|
|
153
|
-
close.meta = { thread, interactive };
|
|
154
|
-
newTokens.push(close);
|
|
155
|
-
if (idx + trimmed.length < child.content.length) {
|
|
156
|
-
const after = new Token("text", "", 0);
|
|
157
|
-
after.content = child.content.slice(idx + trimmed.length);
|
|
158
|
-
newTokens.push(after);
|
|
159
|
-
}
|
|
160
|
-
token.children.splice(i, 1, ...newTokens);
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
function installCoreRule(md, lineMap, options) {
|
|
166
|
-
const { interactive, gutterPosition, gutterForInline, inlineHighlights } = options;
|
|
57
|
+
function installCoreRule(md, lineMap, options = {}) {
|
|
167
58
|
md.core.ruler.push("mrsf_inject", (state) => {
|
|
168
59
|
const tokens = state.tokens;
|
|
169
60
|
const TokenCtor = state.Token;
|
|
@@ -173,127 +64,57 @@ function installCoreRule(md, lineMap, options) {
|
|
|
173
64
|
const map = token.map;
|
|
174
65
|
if (!map) continue;
|
|
175
66
|
if (token.type === "inline") continue;
|
|
67
|
+
const startLine1 = map[0] + 1;
|
|
68
|
+
const endLine1 = map[1];
|
|
69
|
+
token.attrSet("data-mrsf-line", String(startLine1));
|
|
70
|
+
token.attrSet("data-mrsf-start-line", String(startLine1));
|
|
71
|
+
token.attrSet("data-mrsf-end-line", String(endLine1));
|
|
176
72
|
for (let line0 = map[0]; line0 < map[1]; line0++) {
|
|
177
73
|
const line = line0 + 1;
|
|
178
74
|
if (processed.has(line)) continue;
|
|
179
|
-
const threads = lineMap.get(line);
|
|
180
|
-
if (!threads || threads.length === 0) continue;
|
|
181
75
|
processed.add(line);
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const badgeToken = new TokenCtor("mrsf_badge", "", 0);
|
|
192
|
-
badgeToken.meta = { line, threads, interactive, gutterPosition };
|
|
193
|
-
tokens.splice(i + 1, 0, badgeToken);
|
|
194
|
-
}
|
|
195
|
-
if (inlineHighlights) {
|
|
196
|
-
const offset = showBadge ? 2 : 1;
|
|
197
|
-
const inlineAfter = tokens[i + offset];
|
|
198
|
-
if (inlineAfter && inlineAfter.type === "inline") {
|
|
199
|
-
for (const thread of threads) {
|
|
200
|
-
if (thread.comment.selected_text) {
|
|
201
|
-
highlightInlineText(inlineAfter, thread, interactive, TokenCtor);
|
|
202
|
-
}
|
|
76
|
+
const threads = lineMap.get(line);
|
|
77
|
+
if (threads && threads.length > 0) {
|
|
78
|
+
if (options.lineHighlight) {
|
|
79
|
+
const existingClass = token.attrGet("class") || "";
|
|
80
|
+
if (!existingClass.includes("mrsf-line-highlight")) {
|
|
81
|
+
token.attrSet(
|
|
82
|
+
"class",
|
|
83
|
+
existingClass ? `${existingClass} mrsf-line-highlight` : "mrsf-line-highlight"
|
|
84
|
+
);
|
|
203
85
|
}
|
|
204
86
|
}
|
|
87
|
+
token.attrSet("data-mrsf-line", String(line));
|
|
205
88
|
}
|
|
206
89
|
}
|
|
207
90
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
91
|
+
const allThreads = [];
|
|
92
|
+
for (const threads of lineMap.values()) {
|
|
93
|
+
allThreads.push(...threads);
|
|
94
|
+
}
|
|
95
|
+
if (allThreads.length > 0) {
|
|
96
|
+
const scriptToken = new TokenCtor("mrsf_data_script", "", 0);
|
|
97
|
+
scriptToken.meta = { threads: allThreads };
|
|
98
|
+
tokens.push(scriptToken);
|
|
214
99
|
}
|
|
215
100
|
});
|
|
216
101
|
}
|
|
217
102
|
|
|
218
103
|
// src/rules/renderer.ts
|
|
219
104
|
function installRendererRules(md) {
|
|
220
|
-
md.renderer.rules["
|
|
221
|
-
const {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
0
|
|
225
|
-
);
|
|
226
|
-
const allResolved = threads.every((t) => t.comment.resolved);
|
|
227
|
-
const highestSeverity = threads.reduce((sev, t) => {
|
|
228
|
-
if (t.comment.severity === "high" || sev === "high") return "high";
|
|
229
|
-
if (t.comment.severity === "medium" || sev === "medium") return "medium";
|
|
230
|
-
if (t.comment.severity === "low" || sev === "low") return "low";
|
|
231
|
-
return sev;
|
|
232
|
-
}, null);
|
|
233
|
-
const resolvedClass = allResolved ? " mrsf-badge-resolved" : "";
|
|
234
|
-
const severityClass = highestSeverity === "high" || highestSeverity === "medium" ? ` mrsf-badge-severity-${highestSeverity}` : "";
|
|
235
|
-
const positionClass = ` mrsf-gutter-${gutterPosition}`;
|
|
236
|
-
const icon = allResolved ? "\u2713" : "\u{1F4AC}";
|
|
237
|
-
let html = `<span class="mrsf-tooltip-anchor${positionClass}">`;
|
|
238
|
-
html += `<span class="mrsf-badge${resolvedClass}${severityClass}" data-mrsf-line="${line}" data-mrsf-action="navigate" data-mrsf-comment-id="${escapeHtml(threads[0].comment.id)}" tabindex="0">${icon} ${total}</span>`;
|
|
239
|
-
html += `<span class="mrsf-tooltip" data-mrsf-line="${line}">`;
|
|
240
|
-
for (const thread of threads) {
|
|
241
|
-
html += renderThreadHtml(thread, interactive);
|
|
242
|
-
}
|
|
243
|
-
html += `</span>`;
|
|
244
|
-
html += `</span>`;
|
|
245
|
-
return html;
|
|
246
|
-
};
|
|
247
|
-
md.renderer.rules["mrsf_highlight_open"] = (tokens, idx) => {
|
|
248
|
-
const token = tokens[idx];
|
|
249
|
-
const cls = token.attrGet("class") || "mrsf-highlight";
|
|
250
|
-
const commentId = token.attrGet("data-mrsf-comment-id") || "";
|
|
251
|
-
const thread = token.meta?.thread;
|
|
252
|
-
let html = "";
|
|
253
|
-
if (thread) {
|
|
254
|
-
html += `<span class="mrsf-tooltip-anchor mrsf-inline-anchor">`;
|
|
255
|
-
}
|
|
256
|
-
html += `<mark class="${escapeHtml(cls)}" data-mrsf-comment-id="${escapeHtml(commentId)}" tabindex="0">`;
|
|
257
|
-
return html;
|
|
258
|
-
};
|
|
259
|
-
md.renderer.rules["mrsf_highlight_close"] = (tokens, idx) => {
|
|
260
|
-
const token = tokens[idx];
|
|
261
|
-
const meta = token.meta;
|
|
262
|
-
let html = "</mark>";
|
|
263
|
-
if (meta?.thread) {
|
|
264
|
-
const thread = meta.thread;
|
|
265
|
-
const interactive = meta.interactive;
|
|
266
|
-
html += `<span class="mrsf-tooltip mrsf-inline-tooltip">`;
|
|
267
|
-
html += renderThreadHtml(thread, interactive);
|
|
268
|
-
html += `</span>`;
|
|
269
|
-
html += `</span>`;
|
|
270
|
-
}
|
|
271
|
-
return html;
|
|
272
|
-
};
|
|
273
|
-
md.renderer.rules["mrsf_gutter_open"] = (tokens, idx) => {
|
|
274
|
-
const cls = tokens[idx].attrGet("class") || "mrsf-gutter-container";
|
|
275
|
-
return `<div class="${escapeHtml(cls)}">`;
|
|
276
|
-
};
|
|
277
|
-
md.renderer.rules["mrsf_gutter_close"] = () => {
|
|
278
|
-
return `</div>`;
|
|
105
|
+
md.renderer.rules["mrsf_data_script"] = (tokens, idx) => {
|
|
106
|
+
const { threads } = tokens[idx].meta;
|
|
107
|
+
const data = JSON.stringify({ threads }).replace(/</g, "\\u003c");
|
|
108
|
+
return `<script type="application/mrsf+json">${data}</script>`;
|
|
279
109
|
};
|
|
280
110
|
}
|
|
281
111
|
|
|
282
112
|
// src/shared.ts
|
|
283
113
|
function createMrsfPlugin(loader) {
|
|
284
114
|
return function mrsfPlugin2(md, options = {}) {
|
|
285
|
-
const interactive = options.interactive ?? false;
|
|
286
|
-
const gutterPosition = options.gutterPosition ?? "right";
|
|
287
|
-
const gutterForInline = options.gutterForInline ?? true;
|
|
288
|
-
const inlineHighlights = options.inlineHighlights ?? true;
|
|
289
115
|
const result = resolveComments(loader, options);
|
|
290
116
|
if (!result) return;
|
|
291
|
-
installCoreRule(md, result.lineMap, {
|
|
292
|
-
interactive,
|
|
293
|
-
gutterPosition,
|
|
294
|
-
gutterForInline,
|
|
295
|
-
inlineHighlights
|
|
296
|
-
});
|
|
117
|
+
installCoreRule(md, result.lineMap, { lineHighlight: options.lineHighlight ?? false });
|
|
297
118
|
installRendererRules(md);
|
|
298
119
|
};
|
|
299
120
|
}
|
package/dist/browser.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../shared/src/comments.ts", "
|
|
4
|
-
"sourcesContent": ["/**\n * Shared comment loading and grouping logic for MRSF rendering plugins.\n */\n\nimport type { MrsfDocument } from \"@mrsf/cli\";\nimport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\nexport type { CommentLoader };\n\n/**\n * Convert an MrsfDocument into a slim comment array.\n */\nexport function toSlimComments(doc: MrsfDocument): SlimComment[] {\n return doc.comments.map((c) => ({\n id: c.id,\n author: c.author || \"Unknown\",\n text: c.text || \"\",\n line: c.line ?? null,\n end_line: c.end_line ?? null,\n start_column: c.start_column ?? null,\n end_column: c.end_column ?? null,\n selected_text: c.selected_text || null,\n resolved: !!c.resolved,\n reply_to: c.reply_to || null,\n severity: c.severity || null,\n type: c.type || null,\n timestamp: c.timestamp || null,\n }));\n}\n\n/**\n * Group comments by line number, threading replies under parents.\n */\nexport function groupByLine(comments: SlimComment[]): LineMap {\n const rootComments = comments.filter((c) => !c.reply_to && c.line != null);\n const replies = comments.filter((c) => c.reply_to);\n\n const replyMap = new Map<string, SlimComment[]>();\n for (const r of replies) {\n const list = replyMap.get(r.reply_to!) || [];\n list.push(r);\n replyMap.set(r.reply_to!, list);\n }\n\n const lineMap: LineMap = new Map();\n for (const c of rootComments) {\n const line = c.line!;\n const threads = lineMap.get(line) || [];\n threads.push({\n comment: c,\n replies: replyMap.get(c.id) || [],\n });\n lineMap.set(line, threads);\n }\n\n return lineMap;\n}\n\n/**\n * Resolve options into a filtered LineMap ready for rendering.\n * Returns null if there are no comments to render.\n */\nexport function resolveComments(\n loader: CommentLoader,\n options: MrsfPluginOptions,\n): { lineMap: LineMap; comments: SlimComment[] } | null {\n const showResolved = options.showResolved ?? true;\n\n const doc = loader(options);\n if (!doc || !doc.comments || doc.comments.length === 0) {\n return null;\n }\n\n let comments = toSlimComments(doc);\n if (!showResolved) {\n comments = comments.filter((c) => !c.resolved);\n }\n\n if (comments.length === 0) return null;\n\n const lineMap = groupByLine(comments);\n return { lineMap, comments };\n}\n", "/**\n * Shared HTML rendering helpers for MRSF rendering plugins.\n *\n * These produce the HTML strings used in tooltips, badges, and comment\n * rendering. Both the markdown-it and rehype plugins use these functions\n * to ensure identical visual output.\n */\n\nimport type { CommentThread, SlimComment } from \"./types.js\";\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nexport function formatTime(iso: string | null): string {\n if (!iso) return \"\";\n try {\n const d = new Date(iso);\n return d.toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n });\n } catch {\n return \"\";\n }\n}\n\nexport function renderCommentHtml(\n comment: SlimComment,\n isReply: boolean,\n interactive: boolean,\n): string {\n const resolvedClass = comment.resolved ? \" mrsf-resolved\" : \"\";\n const replyClass = isReply ? \" mrsf-reply\" : \"\";\n let html = `<span class=\"mrsf-comment${resolvedClass}${replyClass}\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\">`;\n\n // Header\n html += `<span class=\"mrsf-comment-header\">`;\n html += `<span class=\"mrsf-author\">${escapeHtml(comment.author)}</span>`;\n if (comment.timestamp) {\n html += `<span class=\"mrsf-date\">${escapeHtml(formatTime(comment.timestamp))}</span>`;\n }\n if (comment.severity) {\n html += `<span class=\"mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}\">${escapeHtml(comment.severity)}</span>`;\n }\n if (comment.type) {\n html += `<span class=\"mrsf-type\">${escapeHtml(comment.type)}</span>`;\n }\n if (comment.resolved) {\n html += `<span class=\"mrsf-resolved-badge\">\u2713 resolved</span>`;\n }\n html += `</span>`;\n\n // Selected text quote\n if (comment.selected_text) {\n html += `<span class=\"mrsf-selected-text\">${escapeHtml(comment.selected_text)}</span>`;\n }\n\n // Body\n html += `<span class=\"mrsf-comment-body\">${escapeHtml(comment.text)}</span>`;\n\n // Action buttons (interactive mode)\n if (interactive) {\n const line = comment.line != null ? String(comment.line) : \"\";\n html += `<span class=\"mrsf-actions\">`;\n if (comment.resolved) {\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"unresolve\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Unresolve</button>`;\n } else {\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"resolve\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Resolve</button>`;\n }\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"reply\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Reply</button>`;\n html += `<button class=\"mrsf-action-btn\" data-mrsf-action=\"edit\" data-mrsf-comment-id=\"${escapeHtml(comment.id)}\" data-mrsf-line=\"${line}\">Edit</button>`;\n html += `</span>`;\n }\n\n html += `</span>`;\n return html;\n}\n\nexport function renderThreadHtml(thread: CommentThread, interactive: boolean): string {\n let html = `<span class=\"mrsf-thread\">`;\n html += renderCommentHtml(thread.comment, false, interactive);\n if (thread.replies.length > 0) {\n html += `<span class=\"mrsf-replies\">`;\n for (const reply of thread.replies) {\n html += renderCommentHtml(reply, true, interactive);\n }\n html += `</span>`;\n }\n html += `</span>`;\n return html;\n}\n", "/**\n * MRSF markdown-it plugin \u2014 core rule.\n *\n * Walks the parsed token stream and injects `mrsf_badge` tokens\n * at lines that have comments, and wraps `selected_text` matches\n * with `mrsf_highlight_open`/`close` tokens (with tooltip metadata).\n */\n\nimport type Token from \"markdown-it/lib/token.mjs\";\nimport type { Nesting } from \"markdown-it/lib/token.mjs\";\nimport type StateCore from \"markdown-it/lib/rules_core/state_core.mjs\";\nimport type { LineMap, CommentThread } from \"../types.js\";\n\n/** Options forwarded from the plugin to the core rule. */\nexport interface CoreOptions {\n interactive: boolean;\n gutterPosition: \"left\" | \"tight\" | \"right\";\n gutterForInline: boolean;\n inlineHighlights: boolean;\n}\n\n/**\n * Find the token index for a given 1-based source line.\n * markdown-it tokens use `token.map = [startLine, endLine]` (0-based).\n */\nfunction findTokenIndexForLine(\n tokens: Token[],\n line: number,\n): number {\n const line0 = line - 1; // convert to 0-based\n for (let i = 0; i < tokens.length; i++) {\n const map = tokens[i].map;\n if (map && map[0] <= line0 && line0 < map[1]) {\n return i;\n }\n }\n return -1;\n}\n\n/**\n * Wrap occurrences of `text` inside inline token children with\n * highlight open/close tokens. Stores thread data on the tokens\n * so the renderer can produce an inline tooltip.\n */\nfunction highlightInlineText(\n token: Token,\n thread: CommentThread,\n interactive: boolean,\n Token: new (type: string, tag: string, nesting: Nesting) => Token,\n): boolean {\n if (!token.children) return false;\n\n const text = thread.comment.selected_text;\n if (!text) return false;\n const trimmed = text.trim();\n if (!trimmed) return false;\n\n for (let i = 0; i < token.children.length; i++) {\n const child = token.children[i];\n if (child.type !== \"text\") continue;\n\n const idx = child.content.indexOf(trimmed);\n if (idx === -1) continue;\n\n // Split the text token into: before + highlight_open + matched + highlight_close + after\n const newTokens: Token[] = [];\n\n // Text before match\n if (idx > 0) {\n const before = new Token(\"text\", \"\", 0);\n before.content = child.content.slice(0, idx);\n newTokens.push(before);\n }\n\n // Highlight open \u2014 carries thread metadata for inline tooltip\n const open = new Token(\"mrsf_highlight_open\", \"mark\", 1);\n open.attrSet(\"class\", \"mrsf-highlight\");\n open.attrSet(\"data-mrsf-comment-id\", thread.comment.id);\n open.meta = { thread };\n newTokens.push(open);\n\n // Matched text\n const matched = new Token(\"text\", \"\", 0);\n matched.content = trimmed;\n newTokens.push(matched);\n\n // Highlight close \u2014 carries thread metadata for inline tooltip\n const close = new Token(\"mrsf_highlight_close\", \"mark\", -1);\n close.meta = { thread, interactive };\n newTokens.push(close);\n\n // Text after match\n if (idx + trimmed.length < child.content.length) {\n const after = new Token(\"text\", \"\", 0);\n after.content = child.content.slice(idx + trimmed.length);\n newTokens.push(after);\n }\n\n // Replace the original child with our new tokens\n token.children.splice(i, 1, ...newTokens);\n return true;\n }\n\n return false;\n}\n\n/**\n * Install the MRSF core rule on a markdown-it instance.\n */\nexport function installCoreRule(\n md: { core: { ruler: { push: (name: string, fn: (state: StateCore) => void) => void } } },\n lineMap: LineMap,\n options: CoreOptions,\n): void {\n const { interactive, gutterPosition, gutterForInline, inlineHighlights } = options;\n\n md.core.ruler.push(\"mrsf_inject\", (state: StateCore) => {\n const tokens = state.tokens;\n const TokenCtor = state.Token;\n\n // Track which lines we've already processed (to avoid double-injection\n // when multiple tokens share the same source line).\n const processed = new Set<number>();\n\n // We iterate backwards so that inserting tokens doesn't shift indices\n // for tokens we haven't visited yet.\n for (let i = tokens.length - 1; i >= 0; i--) {\n const token = tokens[i];\n const map = token.map;\n if (!map) continue;\n\n // Skip inline tokens \u2014 they share the same map as their parent block.\n // We must process the parent block (paragraph_open, heading_open, etc.)\n // so the badge and class land on the correct rendered element.\n if (token.type === \"inline\") continue;\n\n // Check each line in this token's range for comments\n for (let line0 = map[0]; line0 < map[1]; line0++) {\n const line = line0 + 1; // 1-based\n if (processed.has(line)) continue;\n\n const threads = lineMap.get(line);\n if (!threads || threads.length === 0) continue;\n processed.add(line);\n\n // Add line-highlight class to the block token\n const existingClass = token.attrGet(\"class\") || \"\";\n token.attrSet(\n \"class\",\n existingClass\n ? `${existingClass} mrsf-line-highlight`\n : \"mrsf-line-highlight\",\n );\n token.attrSet(\"data-mrsf-line\", String(line));\n\n // Determine whether all threads on this line have inline highlights\n const allHaveInline = inlineHighlights &&\n threads.every((t) => !!t.comment.selected_text);\n\n // Inject badge unless gutterForInline is false AND all threads are inline-highlighted\n const showBadge = gutterForInline || !allHaveInline || !inlineHighlights;\n if (showBadge) {\n const badgeToken = new TokenCtor(\"mrsf_badge\", \"\", 0);\n badgeToken.meta = { line, threads, interactive, gutterPosition };\n tokens.splice(i + 1, 0, badgeToken);\n }\n\n // Highlight selected_text in the following inline token\n if (inlineHighlights) {\n const offset = showBadge ? 2 : 1;\n const inlineAfter = tokens[i + offset];\n if (inlineAfter && inlineAfter.type === \"inline\") {\n for (const thread of threads) {\n if (thread.comment.selected_text) {\n highlightInlineText(inlineAfter, thread, interactive, TokenCtor);\n }\n }\n }\n }\n }\n }\n\n // Wrap the entire output in a gutter container when using left gutter\n if (gutterPosition === \"left\" && processed.size > 0) {\n const openToken = new TokenCtor(\"mrsf_gutter_open\", \"div\", 1);\n openToken.attrSet(\"class\", \"mrsf-gutter-container\");\n tokens.unshift(openToken);\n const closeToken = new TokenCtor(\"mrsf_gutter_close\", \"div\", -1 as Nesting);\n tokens.push(closeToken);\n }\n });\n}\n", "/**\n * MRSF markdown-it plugin \u2014 renderer rules.\n *\n * Adds custom renderer rules for MRSF tokens:\n * - mrsf_badge: renders badge + tooltip HTML (gutter)\n * - mrsf_highlight_open/close: renders <mark> wrappers with inline tooltips\n */\n\nimport type { CommentThread } from \"../types.js\";\nimport { escapeHtml, renderThreadHtml } from \"@mrsf/plugin-shared\";\n\n/**\n * Install MRSF renderer rules on a markdown-it instance.\n */\nexport function installRendererRules(\n md: { renderer: { rules: Record<string, ((...args: any[]) => string) | undefined> } },\n): void {\n // Badge + tooltip (gutter)\n md.renderer.rules[\"mrsf_badge\"] = (\n tokens: { meta: { line: number; threads: CommentThread[]; interactive: boolean; gutterPosition: \"left\" | \"tight\" | \"right\" } }[],\n idx: number,\n ): string => {\n const { line, threads, interactive, gutterPosition } = tokens[idx].meta;\n\n const total = threads.reduce(\n (n, t) => n + 1 + t.replies.length,\n 0,\n );\n const allResolved = threads.every((t) => t.comment.resolved);\n const highestSeverity = threads.reduce<string | null>((sev, t) => {\n if (t.comment.severity === \"high\" || sev === \"high\") return \"high\";\n if (t.comment.severity === \"medium\" || sev === \"medium\") return \"medium\";\n if (t.comment.severity === \"low\" || sev === \"low\") return \"low\";\n return sev;\n }, null);\n\n const resolvedClass = allResolved ? \" mrsf-badge-resolved\" : \"\";\n const severityClass = highestSeverity === \"high\" || highestSeverity === \"medium\"\n ? ` mrsf-badge-severity-${highestSeverity}`\n : \"\";\n const positionClass = ` mrsf-gutter-${gutterPosition}`;\n\n const icon = allResolved ? \"\u2713\" : \"\uD83D\uDCAC\";\n\n let html = `<span class=\"mrsf-tooltip-anchor${positionClass}\">`;\n\n // Badge\n html += `<span class=\"mrsf-badge${resolvedClass}${severityClass}\" data-mrsf-line=\"${line}\" data-mrsf-action=\"navigate\" data-mrsf-comment-id=\"${escapeHtml(threads[0].comment.id)}\" tabindex=\"0\">${icon} ${total}</span>`;\n\n // Tooltip\n html += `<span class=\"mrsf-tooltip\" data-mrsf-line=\"${line}\">`;\n for (const thread of threads) {\n html += renderThreadHtml(thread, interactive);\n }\n html += `</span>`;\n\n html += `</span>`;\n return html;\n };\n\n // Highlight open \u2014 wraps in tooltip-anchor for inline hover (feature D)\n md.renderer.rules[\"mrsf_highlight_open\"] = (\n tokens: any[],\n idx: number,\n ): string => {\n const token = tokens[idx];\n const cls = token.attrGet(\"class\") || \"mrsf-highlight\";\n const commentId = token.attrGet(\"data-mrsf-comment-id\") || \"\";\n const thread = token.meta?.thread as CommentThread | undefined;\n\n let html = \"\";\n if (thread) {\n html += `<span class=\"mrsf-tooltip-anchor mrsf-inline-anchor\">`;\n }\n html += `<mark class=\"${escapeHtml(cls)}\" data-mrsf-comment-id=\"${escapeHtml(commentId)}\" tabindex=\"0\">`;\n return html;\n };\n\n // Highlight close \u2014 adds inline tooltip and closes tooltip-anchor\n md.renderer.rules[\"mrsf_highlight_close\"] = (\n tokens: any[],\n idx: number,\n ): string => {\n const token = tokens[idx];\n const meta = token.meta;\n let html = \"</mark>\";\n\n if (meta?.thread) {\n const thread = meta.thread as CommentThread;\n const interactive = meta.interactive as boolean;\n html += `<span class=\"mrsf-tooltip mrsf-inline-tooltip\">`;\n html += renderThreadHtml(thread, interactive);\n html += `</span>`;\n html += `</span>`;\n }\n\n return html;\n };\n\n // Gutter container wrapper (left gutter mode)\n md.renderer.rules[\"mrsf_gutter_open\"] = (\n tokens: any[],\n idx: number,\n ): string => {\n const cls = tokens[idx].attrGet(\"class\") || \"mrsf-gutter-container\";\n return `<div class=\"${escapeHtml(cls)}\">`;\n };\n\n md.renderer.rules[\"mrsf_gutter_close\"] = (): string => {\n return `</div>`;\n };\n}\n", "/**\n * Shared logic for @mrsf/markdown-it-mrsf.\n *\n * Contains the plugin factory using shared comment loading/grouping logic.\n */\n\nimport type MarkdownIt from \"markdown-it\";\nimport type { MrsfPluginOptions, CommentLoader } from \"./types.js\";\nimport { resolveComments } from \"@mrsf/plugin-shared\";\nimport { installCoreRule } from \"./rules/core.js\";\nimport { installRendererRules } from \"./rules/renderer.js\";\n\n/**\n * Create a markdown-it plugin function with the given comment loader.\n *\n * @param loader - Function that resolves sidecar data from plugin options\n * @returns A markdown-it plugin function\n */\nexport function createMrsfPlugin(loader: CommentLoader) {\n return function mrsfPlugin(md: MarkdownIt, options: MrsfPluginOptions = {}): void {\n const interactive = options.interactive ?? false;\n const gutterPosition = options.gutterPosition ?? \"right\";\n const gutterForInline = options.gutterForInline ?? true;\n const inlineHighlights = options.inlineHighlights ?? true;\n\n const result = resolveComments(loader, options);\n if (!result) return;\n\n installCoreRule(md, result.lineMap, {\n interactive,\n gutterPosition,\n gutterForInline,\n inlineHighlights,\n });\n installRendererRules(md);\n };\n}\n", "/**\n * Browser-safe entry point for @mrsf/markdown-it-mrsf.\n *\n * This entry is automatically selected by bundlers (Vite, webpack, esbuild)\n * via the `\"browser\"` export condition in package.json. It supports the\n * `comments` and `loader` provisioning modes \u2014 no Node.js file-system APIs.\n *\n * You do NOT need to import this directly. Just use:\n * import { mrsfPlugin } from \"@mrsf/markdown-it-mrsf\";\n *\n * Your bundler will resolve to this file automatically when targeting\n * a browser environment.\n */\n\nimport type { MrsfPluginOptions } from \"./types.js\";\nimport { createMrsfPlugin } from \"./shared.js\";\n\nexport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\n/**\n * The markdown-it plugin function (browser-safe).\n *\n * Supports `comments` (inline data) and `loader` (custom function) options.\n */\nexport const mrsfPlugin = createMrsfPlugin((options: MrsfPluginOptions) => {\n if (options.comments) {\n return options.comments;\n }\n\n if (options.loader) {\n try {\n return options.loader();\n } catch {\n return null;\n }\n }\n\n if (options.sidecarPath || options.documentPath) {\n console.warn(\n \"[@mrsf/markdown-it-mrsf] sidecarPath and documentPath require Node.js. \" +\n \"Use `comments` or `loader` options in browser environments.\"\n );\n }\n\n return null;\n});\n\nexport default mrsfPlugin;\n"],
|
|
5
|
-
"mappings": ";AAYM,SAAU,eAAe,KAAiB;AAC9C,SAAO,IAAI,SAAS,IAAI,CAAC,OAAO;IAC9B,IAAI,EAAE;IACN,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,cAAc,EAAE,gBAAgB;IAChC,YAAY,EAAE,cAAc;IAC5B,eAAe,EAAE,iBAAiB;IAClC,UAAU,CAAC,CAAC,EAAE;IACd,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,aAAa;IAC1B;AACJ;AAKM,SAAU,YAAY,UAAuB;AACjD,QAAM,eAAe,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,QAAQ,IAAI;AACzE,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,QAAQ;AAEjD,QAAM,WAAW,oBAAI,IAAG;AACxB,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,SAAS,IAAI,EAAE,QAAS,KAAK,CAAA;AAC1C,SAAK,KAAK,CAAC;AACX,aAAS,IAAI,EAAE,UAAW,IAAI;EAChC;AAEA,QAAM,UAAmB,oBAAI,IAAG;AAChC,aAAW,KAAK,cAAc;AAC5B,UAAM,OAAO,EAAE;AACf,UAAM,UAAU,QAAQ,IAAI,IAAI,KAAK,CAAA;AACrC,YAAQ,KAAK;MACX,SAAS;MACT,SAAS,SAAS,IAAI,EAAE,EAAE,KAAK,CAAA;KAChC;AACD,YAAQ,IAAI,MAAM,OAAO;EAC3B;AAEA,SAAO;AACT;AAMM,SAAU,gBACd,QACA,SAA0B;AAE1B,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,QAAM,MAAM,OAAO,OAAO;AAC1B,MAAI,CAAC,OAAO,CAAC,IAAI,YAAY,IAAI,SAAS,WAAW,GAAG;AACtD,WAAO;EACT;AAEA,MAAI,WAAW,eAAe,GAAG;AACjC,MAAI,CAAC,cAAc;AACjB,eAAW,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ;EAC/C;AAEA,MAAI,SAAS,WAAW;AAAG,WAAO;AAElC,QAAM,UAAU,YAAY,QAAQ;AACpC,SAAO,EAAE,SAAS,SAAQ;AAC5B;;;
|
|
3
|
+
"sources": ["../../shared/src/comments.ts", "../src/rules/core.ts", "../src/rules/renderer.ts", "../src/shared.ts", "../src/browser.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared comment loading and grouping logic for MRSF rendering plugins.\n */\n\nimport type { MrsfDocument } from \"@mrsf/cli\";\nimport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\nexport type { CommentLoader };\n\n/**\n * Convert an MrsfDocument into a slim comment array.\n */\nexport function toSlimComments(doc: MrsfDocument): SlimComment[] {\n return doc.comments.map((c) => ({\n id: c.id,\n author: c.author || \"Unknown\",\n text: c.text || \"\",\n line: c.line ?? null,\n end_line: c.end_line ?? null,\n start_column: c.start_column ?? null,\n end_column: c.end_column ?? null,\n selected_text: c.selected_text || null,\n resolved: !!c.resolved,\n reply_to: c.reply_to || null,\n severity: c.severity || null,\n type: c.type || null,\n timestamp: c.timestamp || null,\n }));\n}\n\n/**\n * Group comments by line number, threading replies under parents.\n */\nexport function groupByLine(comments: SlimComment[]): LineMap {\n const rootComments = comments.filter((c) => !c.reply_to && c.line != null);\n const replies = comments.filter((c) => c.reply_to);\n\n const replyMap = new Map<string, SlimComment[]>();\n for (const r of replies) {\n const list = replyMap.get(r.reply_to!) || [];\n list.push(r);\n replyMap.set(r.reply_to!, list);\n }\n\n const lineMap: LineMap = new Map();\n for (const c of rootComments) {\n const line = c.line!;\n const threads = lineMap.get(line) || [];\n threads.push({\n comment: c,\n replies: replyMap.get(c.id) || [],\n });\n lineMap.set(line, threads);\n }\n\n return lineMap;\n}\n\n/**\n * Resolve options into a filtered LineMap ready for rendering.\n * Returns null if there are no comments to render.\n */\nexport function resolveComments(\n loader: CommentLoader,\n options: MrsfPluginOptions,\n): { lineMap: LineMap; comments: SlimComment[] } | null {\n const showResolved = options.showResolved ?? true;\n\n const doc = loader(options);\n if (!doc || !doc.comments || doc.comments.length === 0) {\n return null;\n }\n\n let comments = toSlimComments(doc);\n if (!showResolved) {\n comments = comments.filter((c) => !c.resolved);\n }\n\n if (comments.length === 0) return null;\n\n const lineMap = groupByLine(comments);\n return { lineMap, comments };\n}\n", "/**\n * MRSF markdown-it plugin \u2014 core rule (overlay gutter architecture).\n *\n * Walks the parsed token stream and annotates block tokens with\n * `data-mrsf-line` attributes. Adds `mrsf-line-highlight` class on\n * commented lines. Appends an `mrsf_data_script` token with serialized\n * comment data for the client-side MrsfController.\n *\n * NO visual tokens are injected \u2014 all badges, tooltips, and gutter\n * elements are created at runtime by the controller.\n */\n\nimport type Token from \"markdown-it/lib/token.mjs\";\nimport type { Nesting } from \"markdown-it/lib/token.mjs\";\nimport type StateCore from \"markdown-it/lib/rules_core/state_core.mjs\";\nimport type { LineMap, CommentThread } from \"../types.js\";\n\nexport interface CoreRuleOptions {\n /** Whether to add mrsf-line-highlight class on commented tokens. Default: false. */\n lineHighlight?: boolean;\n}\n\n/**\n * Install the MRSF core rule on a markdown-it instance.\n */\nexport function installCoreRule(\n md: { core: { ruler: { push: (name: string, fn: (state: StateCore) => void) => void } } },\n lineMap: LineMap,\n options: CoreRuleOptions = {},\n): void {\n md.core.ruler.push(\"mrsf_inject\", (state: StateCore) => {\n const tokens = state.tokens;\n const TokenCtor = state.Token;\n const processed = new Set<number>();\n\n for (let i = tokens.length - 1; i >= 0; i--) {\n const token = tokens[i];\n const map = token.map;\n if (!map) continue;\n\n // Skip inline tokens \u2014 process their parent block tokens only.\n if (token.type === \"inline\") continue;\n\n // Annotate the element with line range\n const startLine1 = map[0] + 1;\n const endLine1 = map[1];\n token.attrSet(\"data-mrsf-line\", String(startLine1));\n token.attrSet(\"data-mrsf-start-line\", String(startLine1));\n token.attrSet(\"data-mrsf-end-line\", String(endLine1));\n\n // Check each line in this token's range for comments\n for (let line0 = map[0]; line0 < map[1]; line0++) {\n const line = line0 + 1; // 1-based\n if (processed.has(line)) continue;\n processed.add(line);\n\n const threads = lineMap.get(line);\n if (threads && threads.length > 0) {\n if (options.lineHighlight) {\n const existingClass = token.attrGet(\"class\") || \"\";\n if (!existingClass.includes(\"mrsf-line-highlight\")) {\n token.attrSet(\n \"class\",\n existingClass\n ? `${existingClass} mrsf-line-highlight`\n : \"mrsf-line-highlight\",\n );\n }\n }\n token.attrSet(\"data-mrsf-line\", String(line));\n }\n }\n }\n\n // Append a data script token with all comment data\n const allThreads: CommentThread[] = [];\n for (const threads of lineMap.values()) {\n allThreads.push(...threads);\n }\n if (allThreads.length > 0) {\n const scriptToken = new TokenCtor(\"mrsf_data_script\", \"\", 0);\n scriptToken.meta = { threads: allThreads };\n tokens.push(scriptToken);\n }\n });\n}\n", "/**\n * MRSF markdown-it plugin \u2014 renderer rules (overlay gutter architecture).\n *\n * Adds a single custom renderer rule:\n * - mrsf_data_script: renders a <script type=\"application/mrsf+json\">\n * containing serialized comment data for the MrsfController.\n */\n\nimport type { CommentThread } from \"../types.js\";\n\n/**\n * Install MRSF renderer rules on a markdown-it instance.\n */\nexport function installRendererRules(\n md: { renderer: { rules: Record<string, ((...args: any[]) => string) | undefined> } },\n): void {\n md.renderer.rules[\"mrsf_data_script\"] = (\n tokens: { meta: { threads: CommentThread[] } }[],\n idx: number,\n ): string => {\n const { threads } = tokens[idx].meta;\n // Escape < to prevent script tag injection (XSS safety)\n const data = JSON.stringify({ threads }).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/mrsf+json\">${data}</script>`;\n };\n}\n", "/**\n * Shared logic for @mrsf/markdown-it-mrsf.\n *\n * Contains the plugin factory using shared comment loading/grouping logic.\n */\n\nimport type MarkdownIt from \"markdown-it\";\nimport type { MrsfPluginOptions, CommentLoader } from \"./types.js\";\nimport { resolveComments } from \"@mrsf/plugin-shared\";\nimport { installCoreRule } from \"./rules/core.js\";\nimport { installRendererRules } from \"./rules/renderer.js\";\n\n/**\n * Create a markdown-it plugin function with the given comment loader.\n *\n * @param loader - Function that resolves sidecar data from plugin options\n * @returns A markdown-it plugin function\n */\nexport function createMrsfPlugin(loader: CommentLoader) {\n return function mrsfPlugin(md: MarkdownIt, options: MrsfPluginOptions = {}): void {\n const result = resolveComments(loader, options);\n if (!result) return;\n\n installCoreRule(md, result.lineMap, { lineHighlight: options.lineHighlight ?? false });\n installRendererRules(md);\n };\n}\n", "/**\n * Browser-safe entry point for @mrsf/markdown-it-mrsf.\n *\n * This entry is automatically selected by bundlers (Vite, webpack, esbuild)\n * via the `\"browser\"` export condition in package.json. It supports the\n * `comments` and `loader` provisioning modes \u2014 no Node.js file-system APIs.\n *\n * You do NOT need to import this directly. Just use:\n * import { mrsfPlugin } from \"@mrsf/markdown-it-mrsf\";\n *\n * Your bundler will resolve to this file automatically when targeting\n * a browser environment.\n */\n\nimport type { MrsfPluginOptions } from \"./types.js\";\nimport { createMrsfPlugin } from \"./shared.js\";\n\nexport type { MrsfPluginOptions, SlimComment, CommentThread, LineMap, CommentLoader } from \"./types.js\";\n\n/**\n * The markdown-it plugin function (browser-safe).\n *\n * Supports `comments` (inline data) and `loader` (custom function) options.\n */\nexport const mrsfPlugin = createMrsfPlugin((options: MrsfPluginOptions) => {\n if (options.comments) {\n return options.comments;\n }\n\n if (options.loader) {\n try {\n return options.loader();\n } catch {\n return null;\n }\n }\n\n if (options.sidecarPath || options.documentPath) {\n console.warn(\n \"[@mrsf/markdown-it-mrsf] sidecarPath and documentPath require Node.js. \" +\n \"Use `comments` or `loader` options in browser environments.\"\n );\n }\n\n return null;\n});\n\nexport default mrsfPlugin;\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["mrsfPlugin"]
|
|
7
7
|
}
|
package/dist/controller.d.ts
CHANGED
|
@@ -1,23 +1,149 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidemark —
|
|
2
|
+
* Sidemark — MrsfController: overlay gutter architecture.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The controller is the runtime engine for interactive MRSF rendering.
|
|
5
|
+
* It reads comment data from the DOM (embedded `<script type="application/mrsf+json">`)
|
|
6
|
+
* or constructor options, creates gutter overlay columns, positions
|
|
7
|
+
* badges/buttons at the correct vertical offsets, and handles all
|
|
8
|
+
* user interactions (tooltips, selection, action buttons).
|
|
6
9
|
*
|
|
7
|
-
* Usage
|
|
8
|
-
* import "@mrsf/plugin-shared/controller";
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { MrsfController } from "@mrsf/plugin-shared/controller";
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* const ctrl = new MrsfController(document.querySelector(".my-content")!, {
|
|
14
|
+
* interactive: true,
|
|
15
|
+
* gutterPosition: "left",
|
|
16
|
+
* });
|
|
17
|
+
* // ctrl.destroy() to clean up
|
|
18
|
+
*
|
|
19
|
+
* Events dispatched on document:
|
|
20
|
+
* - mrsf:resolve { commentId, line, ... }
|
|
21
|
+
* - mrsf:unresolve { commentId, line, ... }
|
|
22
|
+
* - mrsf:reply { commentId, line, ... }
|
|
23
|
+
* - mrsf:edit { commentId, line, ... }
|
|
24
|
+
* - mrsf:delete { commentId, line, ... }
|
|
25
|
+
* - mrsf:navigate { commentId, line, ... }
|
|
26
|
+
* - mrsf:add { commentId: null, line, selectionText?, ... }
|
|
27
|
+
* - mrsf:submit { action, commentId, text?, line?, ... }
|
|
16
28
|
*/
|
|
17
|
-
|
|
29
|
+
import type { CommentThread } from "./types.js";
|
|
30
|
+
export type MrsfAction = "resolve" | "unresolve" | "reply" | "edit" | "delete" | "navigate" | "add";
|
|
18
31
|
export interface MrsfActionDetail {
|
|
19
|
-
commentId: string;
|
|
32
|
+
commentId: string | null;
|
|
20
33
|
line: number | null;
|
|
21
34
|
action: MrsfAction;
|
|
35
|
+
selectionText?: string | null;
|
|
36
|
+
start_line?: number | null;
|
|
37
|
+
end_line?: number | null;
|
|
38
|
+
start_column?: number | null;
|
|
39
|
+
end_column?: number | null;
|
|
40
|
+
}
|
|
41
|
+
export interface MrsfSubmitDetail {
|
|
42
|
+
action: "add" | "edit" | "reply" | "resolve" | "unresolve" | "delete";
|
|
43
|
+
commentId: string | null;
|
|
44
|
+
text: string;
|
|
45
|
+
type?: string | null;
|
|
46
|
+
severity?: "low" | "medium" | "high" | null;
|
|
47
|
+
line?: number | null;
|
|
48
|
+
end_line?: number | null;
|
|
49
|
+
start_column?: number | null;
|
|
50
|
+
end_column?: number | null;
|
|
51
|
+
selection_text?: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface MrsfControllerOptions {
|
|
54
|
+
/** Show gutter on left or right side. Default: "right". */
|
|
55
|
+
gutterPosition?: "left" | "right";
|
|
56
|
+
/** Enable interactive actions (add, resolve, reply, etc.). Default: false. */
|
|
57
|
+
interactive?: boolean;
|
|
58
|
+
/** Comment data passed directly (overrides embedded script). */
|
|
59
|
+
comments?: CommentThread[];
|
|
60
|
+
/**
|
|
61
|
+
* Render inline text highlights for comments that have `selected_text`.
|
|
62
|
+
* Wraps matching text in `<mark>` elements with hover tooltips.
|
|
63
|
+
* Default: true.
|
|
64
|
+
*/
|
|
65
|
+
inlineHighlights?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare class MrsfController {
|
|
68
|
+
private container;
|
|
69
|
+
private opts;
|
|
70
|
+
private threads;
|
|
71
|
+
private gutterLeft;
|
|
72
|
+
private gutterRight;
|
|
73
|
+
private activeTooltip;
|
|
74
|
+
private floatingAddButton;
|
|
75
|
+
private overlayEl;
|
|
76
|
+
private lastSelectionText;
|
|
77
|
+
private resizeObserver;
|
|
78
|
+
private styleInjected;
|
|
79
|
+
private inlineMarks;
|
|
80
|
+
private inlineTooltipEl;
|
|
81
|
+
private orphanedSection;
|
|
82
|
+
private handleResizeBound;
|
|
83
|
+
private handleClickBound;
|
|
84
|
+
private handleSelectionBound;
|
|
85
|
+
constructor(container: HTMLElement, options?: MrsfControllerOptions);
|
|
86
|
+
/** Remove all controller DOM and listeners. */
|
|
87
|
+
destroy(): void;
|
|
88
|
+
private loadCommentData;
|
|
89
|
+
private buildThreadMap;
|
|
90
|
+
private setupOverlayStructure;
|
|
91
|
+
private createGutter;
|
|
92
|
+
/** Build badge/add-button elements for each line in the gutter(s). */
|
|
93
|
+
private renderGutterItems;
|
|
94
|
+
private primaryGutter;
|
|
95
|
+
/** Collect all unique line numbers from data-mrsf-line elements, expanding multi-line ranges. */
|
|
96
|
+
private collectLines;
|
|
97
|
+
private createBadgeItem;
|
|
98
|
+
private createAddItem;
|
|
99
|
+
private createAddButton;
|
|
100
|
+
/** Measure [data-mrsf-line] elements and set gutter item Y offsets. */
|
|
101
|
+
positionGutterItems(): void;
|
|
102
|
+
/**
|
|
103
|
+
* Find the element whose start-line/end-line range contains the given line.
|
|
104
|
+
* Used for positioning gutter items on expanded multi-line elements.
|
|
105
|
+
*/
|
|
106
|
+
private findElementForLine;
|
|
107
|
+
/**
|
|
108
|
+
* Render orphaned comments (whose line doesn't match any DOM element)
|
|
109
|
+
* in a dedicated section at the bottom of the container.
|
|
110
|
+
*/
|
|
111
|
+
private renderOrphanedSection;
|
|
112
|
+
/**
|
|
113
|
+
* For comments with `selected_text`, find the matching text in the DOM
|
|
114
|
+
* and wrap it in a `<mark class="mrsf-inline-highlight">` element with
|
|
115
|
+
* hover/click behaviour to show the comment tooltip.
|
|
116
|
+
*/
|
|
117
|
+
private renderInlineHighlights;
|
|
118
|
+
/**
|
|
119
|
+
* Strip common inline markdown syntax so `selected_text` from source
|
|
120
|
+
* can be matched against rendered text content.
|
|
121
|
+
*/
|
|
122
|
+
private static stripInlineMarkdown;
|
|
123
|
+
/**
|
|
124
|
+
* Walk text nodes inside `root` to find `text`, then wrap the matching
|
|
125
|
+
* range in a `<mark>` element. Falls back to markdown-stripped matching.
|
|
126
|
+
*/
|
|
127
|
+
private wrapSelectedText;
|
|
128
|
+
private showInlineTooltip;
|
|
129
|
+
private hideInlineTimeout;
|
|
130
|
+
private scheduleHideInlineTooltip;
|
|
131
|
+
private cancelHideInlineTooltip;
|
|
132
|
+
private hideInlineTooltip;
|
|
133
|
+
/** Remove all inline marks, unwrapping their contents back to text. */
|
|
134
|
+
private removeInlineHighlights;
|
|
135
|
+
private toggleTooltip;
|
|
136
|
+
private showTooltip;
|
|
137
|
+
private hideTooltip;
|
|
138
|
+
private handleClick;
|
|
139
|
+
private handleSelectionChange;
|
|
140
|
+
private ensureFloatingAddButton;
|
|
141
|
+
private hideFloatingAddButton;
|
|
142
|
+
private showFloatingAddButton;
|
|
143
|
+
private injectStyles;
|
|
144
|
+
private closeOverlay;
|
|
145
|
+
private openForm;
|
|
146
|
+
private openConfirm;
|
|
22
147
|
}
|
|
148
|
+
export declare function autoInit(): void;
|
|
23
149
|
//# sourceMappingURL=controller.d.ts.map
|