@mrsf/markdown-it-mrsf 0.1.0 → 0.2.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.
@@ -1,149 +0,0 @@
1
- /**
2
- * MRSF markdown-it plugin — core rule.
3
- *
4
- * Walks the parsed token stream and injects `mrsf_badge` tokens
5
- * at lines that have comments, and wraps `selected_text` matches
6
- * with `mrsf_highlight_open`/`close` tokens (with tooltip metadata).
7
- */
8
- /**
9
- * Find the token index for a given 1-based source line.
10
- * markdown-it tokens use `token.map = [startLine, endLine]` (0-based).
11
- */
12
- function findTokenIndexForLine(tokens, line) {
13
- const line0 = line - 1; // convert to 0-based
14
- for (let i = 0; i < tokens.length; i++) {
15
- const map = tokens[i].map;
16
- if (map && map[0] <= line0 && line0 < map[1]) {
17
- return i;
18
- }
19
- }
20
- return -1;
21
- }
22
- /**
23
- * Wrap occurrences of `text` inside inline token children with
24
- * highlight open/close tokens. Stores thread data on the tokens
25
- * so the renderer can produce an inline tooltip.
26
- */
27
- function highlightInlineText(token, thread, interactive, Token) {
28
- if (!token.children)
29
- return false;
30
- const text = thread.comment.selected_text;
31
- if (!text)
32
- return false;
33
- const trimmed = text.trim();
34
- if (!trimmed)
35
- return false;
36
- for (let i = 0; i < token.children.length; i++) {
37
- const child = token.children[i];
38
- if (child.type !== "text")
39
- continue;
40
- const idx = child.content.indexOf(trimmed);
41
- if (idx === -1)
42
- continue;
43
- // Split the text token into: before + highlight_open + matched + highlight_close + after
44
- const newTokens = [];
45
- // Text before match
46
- if (idx > 0) {
47
- const before = new Token("text", "", 0);
48
- before.content = child.content.slice(0, idx);
49
- newTokens.push(before);
50
- }
51
- // Highlight open — carries thread metadata for inline tooltip
52
- const open = new Token("mrsf_highlight_open", "mark", 1);
53
- open.attrSet("class", "mrsf-highlight");
54
- open.attrSet("data-mrsf-comment-id", thread.comment.id);
55
- open.meta = { thread };
56
- newTokens.push(open);
57
- // Matched text
58
- const matched = new Token("text", "", 0);
59
- matched.content = trimmed;
60
- newTokens.push(matched);
61
- // Highlight close — carries thread metadata for inline tooltip
62
- const close = new Token("mrsf_highlight_close", "mark", -1);
63
- close.meta = { thread, interactive };
64
- newTokens.push(close);
65
- // Text after match
66
- if (idx + trimmed.length < child.content.length) {
67
- const after = new Token("text", "", 0);
68
- after.content = child.content.slice(idx + trimmed.length);
69
- newTokens.push(after);
70
- }
71
- // Replace the original child with our new tokens
72
- token.children.splice(i, 1, ...newTokens);
73
- return true;
74
- }
75
- return false;
76
- }
77
- /**
78
- * Install the MRSF core rule on a markdown-it instance.
79
- */
80
- export function installCoreRule(md, lineMap, options) {
81
- const { interactive, gutterPosition, gutterForInline, inlineHighlights } = options;
82
- md.core.ruler.push("mrsf_inject", (state) => {
83
- const tokens = state.tokens;
84
- const TokenCtor = state.Token;
85
- // Track which lines we've already processed (to avoid double-injection
86
- // when multiple tokens share the same source line).
87
- const processed = new Set();
88
- // We iterate backwards so that inserting tokens doesn't shift indices
89
- // for tokens we haven't visited yet.
90
- for (let i = tokens.length - 1; i >= 0; i--) {
91
- const token = tokens[i];
92
- const map = token.map;
93
- if (!map)
94
- continue;
95
- // Skip inline tokens — they share the same map as their parent block.
96
- // We must process the parent block (paragraph_open, heading_open, etc.)
97
- // so the badge and class land on the correct rendered element.
98
- if (token.type === "inline")
99
- continue;
100
- // Check each line in this token's range for comments
101
- for (let line0 = map[0]; line0 < map[1]; line0++) {
102
- const line = line0 + 1; // 1-based
103
- if (processed.has(line))
104
- continue;
105
- const threads = lineMap.get(line);
106
- if (!threads || threads.length === 0)
107
- continue;
108
- processed.add(line);
109
- // Add line-highlight class to the block token
110
- const existingClass = token.attrGet("class") || "";
111
- token.attrSet("class", existingClass
112
- ? `${existingClass} mrsf-line-highlight`
113
- : "mrsf-line-highlight");
114
- token.attrSet("data-mrsf-line", String(line));
115
- // Determine whether all threads on this line have inline highlights
116
- const allHaveInline = inlineHighlights &&
117
- threads.every((t) => !!t.comment.selected_text);
118
- // Inject badge unless gutterForInline is false AND all threads are inline-highlighted
119
- const showBadge = gutterForInline || !allHaveInline || !inlineHighlights;
120
- if (showBadge) {
121
- const badgeToken = new TokenCtor("mrsf_badge", "", 0);
122
- badgeToken.meta = { line, threads, interactive, gutterPosition };
123
- tokens.splice(i + 1, 0, badgeToken);
124
- }
125
- // Highlight selected_text in the following inline token
126
- if (inlineHighlights) {
127
- const offset = showBadge ? 2 : 1;
128
- const inlineAfter = tokens[i + offset];
129
- if (inlineAfter && inlineAfter.type === "inline") {
130
- for (const thread of threads) {
131
- if (thread.comment.selected_text) {
132
- highlightInlineText(inlineAfter, thread, interactive, TokenCtor);
133
- }
134
- }
135
- }
136
- }
137
- }
138
- }
139
- // Wrap the entire output in a gutter container when using left gutter
140
- if (gutterPosition === "left" && processed.size > 0) {
141
- const openToken = new TokenCtor("mrsf_gutter_open", "div", 1);
142
- openToken.attrSet("class", "mrsf-gutter-container");
143
- tokens.unshift(openToken);
144
- const closeToken = new TokenCtor("mrsf_gutter_close", "div", -1);
145
- tokens.push(closeToken);
146
- }
147
- });
148
- }
149
- //# sourceMappingURL=core.js.map
@@ -1,159 +0,0 @@
1
- /**
2
- * MRSF markdown-it plugin — renderer rules.
3
- *
4
- * Adds custom renderer rules for MRSF tokens:
5
- * - mrsf_badge: renders badge + tooltip HTML (gutter)
6
- * - mrsf_highlight_open/close: renders <mark> wrappers with inline tooltips
7
- */
8
- function escapeHtml(str) {
9
- return str
10
- .replace(/&/g, "&amp;")
11
- .replace(/</g, "&lt;")
12
- .replace(/>/g, "&gt;")
13
- .replace(/"/g, "&quot;");
14
- }
15
- function formatTime(iso) {
16
- if (!iso)
17
- return "";
18
- try {
19
- const d = new Date(iso);
20
- return d.toLocaleDateString(undefined, {
21
- year: "numeric",
22
- month: "short",
23
- day: "numeric",
24
- });
25
- }
26
- catch {
27
- return "";
28
- }
29
- }
30
- function renderCommentHtml(comment, isReply, interactive) {
31
- const resolvedClass = comment.resolved ? " mrsf-resolved" : "";
32
- const replyClass = isReply ? " mrsf-reply" : "";
33
- let html = `<span class="mrsf-comment${resolvedClass}${replyClass}" data-mrsf-comment-id="${escapeHtml(comment.id)}">`;
34
- // Header
35
- html += `<span class="mrsf-comment-header">`;
36
- html += `<span class="mrsf-author">${escapeHtml(comment.author)}</span>`;
37
- if (comment.timestamp) {
38
- html += `<span class="mrsf-date">${escapeHtml(formatTime(comment.timestamp))}</span>`;
39
- }
40
- if (comment.severity) {
41
- html += `<span class="mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}">${escapeHtml(comment.severity)}</span>`;
42
- }
43
- if (comment.type) {
44
- html += `<span class="mrsf-type">${escapeHtml(comment.type)}</span>`;
45
- }
46
- if (comment.resolved) {
47
- html += `<span class="mrsf-resolved-badge">✓ resolved</span>`;
48
- }
49
- html += `</span>`;
50
- // Selected text quote
51
- if (comment.selected_text) {
52
- html += `<span class="mrsf-selected-text">${escapeHtml(comment.selected_text)}</span>`;
53
- }
54
- // Body
55
- html += `<span class="mrsf-comment-body">${escapeHtml(comment.text)}</span>`;
56
- // Action buttons (interactive mode)
57
- if (interactive) {
58
- const line = comment.line != null ? String(comment.line) : "";
59
- html += `<span class="mrsf-actions">`;
60
- if (comment.resolved) {
61
- html += `<button class="mrsf-action-btn" data-mrsf-action="unresolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Unresolve</button>`;
62
- }
63
- else {
64
- html += `<button class="mrsf-action-btn" data-mrsf-action="resolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Resolve</button>`;
65
- }
66
- html += `<button class="mrsf-action-btn" data-mrsf-action="reply" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Reply</button>`;
67
- html += `<button class="mrsf-action-btn" data-mrsf-action="edit" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Edit</button>`;
68
- html += `</span>`;
69
- }
70
- html += `</span>`;
71
- return html;
72
- }
73
- function renderThreadHtml(thread, interactive) {
74
- let html = `<span class="mrsf-thread">`;
75
- html += renderCommentHtml(thread.comment, false, interactive);
76
- if (thread.replies.length > 0) {
77
- html += `<span class="mrsf-replies">`;
78
- for (const reply of thread.replies) {
79
- html += renderCommentHtml(reply, true, interactive);
80
- }
81
- html += `</span>`;
82
- }
83
- html += `</span>`;
84
- return html;
85
- }
86
- /**
87
- * Install MRSF renderer rules on a markdown-it instance.
88
- */
89
- export function installRendererRules(md) {
90
- // Badge + tooltip (gutter)
91
- md.renderer.rules["mrsf_badge"] = (tokens, idx) => {
92
- const { line, threads, interactive, gutterPosition } = tokens[idx].meta;
93
- const total = threads.reduce((n, t) => n + 1 + t.replies.length, 0);
94
- const allResolved = threads.every((t) => t.comment.resolved);
95
- const highestSeverity = threads.reduce((sev, t) => {
96
- if (t.comment.severity === "high" || sev === "high")
97
- return "high";
98
- if (t.comment.severity === "medium" || sev === "medium")
99
- return "medium";
100
- if (t.comment.severity === "low" || sev === "low")
101
- return "low";
102
- return sev;
103
- }, null);
104
- const resolvedClass = allResolved ? " mrsf-badge-resolved" : "";
105
- const severityClass = highestSeverity === "high" || highestSeverity === "medium"
106
- ? ` mrsf-badge-severity-${highestSeverity}`
107
- : "";
108
- const positionClass = ` mrsf-gutter-${gutterPosition}`;
109
- const icon = allResolved ? "✓" : "💬";
110
- let html = `<span class="mrsf-tooltip-anchor${positionClass}">`;
111
- // Badge
112
- 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>`;
113
- // Tooltip
114
- html += `<span class="mrsf-tooltip" data-mrsf-line="${line}">`;
115
- for (const thread of threads) {
116
- html += renderThreadHtml(thread, interactive);
117
- }
118
- html += `</span>`;
119
- html += `</span>`;
120
- return html;
121
- };
122
- // Highlight open — wraps in tooltip-anchor for inline hover (feature D)
123
- md.renderer.rules["mrsf_highlight_open"] = (tokens, idx) => {
124
- const token = tokens[idx];
125
- const cls = token.attrGet("class") || "mrsf-highlight";
126
- const commentId = token.attrGet("data-mrsf-comment-id") || "";
127
- const thread = token.meta?.thread;
128
- let html = "";
129
- if (thread) {
130
- html += `<span class="mrsf-tooltip-anchor mrsf-inline-anchor">`;
131
- }
132
- html += `<mark class="${escapeHtml(cls)}" data-mrsf-comment-id="${escapeHtml(commentId)}" tabindex="0">`;
133
- return html;
134
- };
135
- // Highlight close — adds inline tooltip and closes tooltip-anchor
136
- md.renderer.rules["mrsf_highlight_close"] = (tokens, idx) => {
137
- const token = tokens[idx];
138
- const meta = token.meta;
139
- let html = "</mark>";
140
- if (meta?.thread) {
141
- const thread = meta.thread;
142
- const interactive = meta.interactive;
143
- html += `<span class="mrsf-tooltip mrsf-inline-tooltip">`;
144
- html += renderThreadHtml(thread, interactive);
145
- html += `</span>`;
146
- html += `</span>`;
147
- }
148
- return html;
149
- };
150
- // Gutter container wrapper (left gutter mode)
151
- md.renderer.rules["mrsf_gutter_open"] = (tokens, idx) => {
152
- const cls = tokens[idx].attrGet("class") || "mrsf-gutter-container";
153
- return `<div class="${escapeHtml(cls)}">`;
154
- };
155
- md.renderer.rules["mrsf_gutter_close"] = () => {
156
- return `</div>`;
157
- };
158
- }
159
- //# sourceMappingURL=renderer.js.map
package/dist/shared.js DELETED
@@ -1,86 +0,0 @@
1
- /**
2
- * Shared logic for @mrsf/markdown-it-mrsf.
3
- *
4
- * Contains the plugin factory, comment transformation, and grouping logic
5
- * used by both the Node.js (index.ts) and browser (browser.ts) entry points.
6
- */
7
- import { installCoreRule } from "./rules/core.js";
8
- import { installRendererRules } from "./rules/renderer.js";
9
- /**
10
- * Convert an MrsfDocument into a slim comment array.
11
- */
12
- export function toSlimComments(doc) {
13
- return doc.comments.map((c) => ({
14
- id: c.id,
15
- author: c.author || "Unknown",
16
- text: c.text || "",
17
- line: c.line ?? null,
18
- end_line: c.end_line ?? null,
19
- start_column: c.start_column ?? null,
20
- end_column: c.end_column ?? null,
21
- selected_text: c.selected_text || null,
22
- resolved: !!c.resolved,
23
- reply_to: c.reply_to || null,
24
- severity: c.severity || null,
25
- type: c.type || null,
26
- timestamp: c.timestamp || null,
27
- }));
28
- }
29
- /**
30
- * Group comments by line number, threading replies under parents.
31
- */
32
- export function groupByLine(comments) {
33
- const rootComments = comments.filter((c) => !c.reply_to && c.line != null);
34
- const replies = comments.filter((c) => c.reply_to);
35
- const replyMap = new Map();
36
- for (const r of replies) {
37
- const list = replyMap.get(r.reply_to) || [];
38
- list.push(r);
39
- replyMap.set(r.reply_to, list);
40
- }
41
- const lineMap = new Map();
42
- for (const c of rootComments) {
43
- const line = c.line;
44
- const threads = lineMap.get(line) || [];
45
- threads.push({
46
- comment: c,
47
- replies: replyMap.get(c.id) || [],
48
- });
49
- lineMap.set(line, threads);
50
- }
51
- return lineMap;
52
- }
53
- /**
54
- * Create a markdown-it plugin function with the given comment loader.
55
- *
56
- * @param loader - Function that resolves sidecar data from plugin options
57
- * @returns A markdown-it plugin function
58
- */
59
- export function createMrsfPlugin(loader) {
60
- return function mrsfPlugin(md, options = {}) {
61
- const showResolved = options.showResolved ?? true;
62
- const interactive = options.interactive ?? false;
63
- const gutterPosition = options.gutterPosition ?? "right";
64
- const gutterForInline = options.gutterForInline ?? true;
65
- const inlineHighlights = options.inlineHighlights ?? true;
66
- const doc = loader(options);
67
- if (!doc || !doc.comments || doc.comments.length === 0) {
68
- return;
69
- }
70
- let comments = toSlimComments(doc);
71
- if (!showResolved) {
72
- comments = comments.filter((c) => !c.resolved);
73
- }
74
- if (comments.length === 0)
75
- return;
76
- const lineMap = groupByLine(comments);
77
- installCoreRule(md, lineMap, {
78
- interactive,
79
- gutterPosition,
80
- gutterForInline,
81
- inlineHighlights,
82
- });
83
- installRendererRules(md);
84
- };
85
- }
86
- //# sourceMappingURL=shared.js.map
package/dist/types.js DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * MRSF markdown-it plugin — type definitions.
3
- */
4
- export {};
5
- //# sourceMappingURL=types.js.map