@kontourai/corpus 0.0.0-bootstrap.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/LICENSE +201 -0
- package/README.md +497 -0
- package/bin/corpus.mjs +4 -0
- package/bin/docs-kit.mjs +4 -0
- package/package.json +58 -3
- package/schemas/decision-record.schema.json +85 -0
- package/src/assets.mjs +67 -0
- package/src/build.mjs +535 -0
- package/src/built-on.mjs +238 -0
- package/src/bundle.mjs +174 -0
- package/src/check.mjs +520 -0
- package/src/cli.mjs +226 -0
- package/src/content-freshness.mjs +37 -0
- package/src/content.mjs +222 -0
- package/src/decisions.mjs +317 -0
- package/src/errors.mjs +24 -0
- package/src/git.mjs +171 -0
- package/src/hub.mjs +198 -0
- package/src/index.mjs +12 -0
- package/src/init.mjs +63 -0
- package/src/locate.mjs +341 -0
- package/src/manifest.mjs +589 -0
- package/src/markdown.mjs +547 -0
- package/src/mcp.mjs +583 -0
- package/src/paths.mjs +122 -0
- package/src/search.mjs +508 -0
- package/src/site-client.js +622 -0
- package/src/site-template.mjs +191 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* docs-kit selection toolbar and WebMCP tools. Written by `docs-kit build` as
|
|
3
|
+
* docs-kit.js at the site root and loaded by every page. Template output, not
|
|
4
|
+
* document content: it reads provenance from the data-* attributes on <main>
|
|
5
|
+
* and from data-src-line on rendered blocks, and it never puts selected text
|
|
6
|
+
* into the page as HTML (textContent and textarea values only).
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
var main = document.querySelector('main[data-src-path]');
|
|
12
|
+
if (!main) return;
|
|
13
|
+
var page = {
|
|
14
|
+
path: main.getAttribute('data-src-path') || '',
|
|
15
|
+
sha: main.getAttribute('data-src-sha') || '',
|
|
16
|
+
repo: main.getAttribute('data-repo') || '',
|
|
17
|
+
product: main.getAttribute('data-product') || '',
|
|
18
|
+
title: main.getAttribute('data-title') || '',
|
|
19
|
+
branch: main.getAttribute('data-branch') || 'main',
|
|
20
|
+
primary: main.getAttribute('data-selection-primary') === 'challenge-issue' ? 'challenge-issue' : 'ask',
|
|
21
|
+
};
|
|
22
|
+
var script = document.currentScript;
|
|
23
|
+
var siteRoot = new URL('./', script && script.src ? script.src : location.href).href;
|
|
24
|
+
var LINE = /^(\d+)-(\d+)$/;
|
|
25
|
+
// GitHub refuses very long URLs (about 8 KB); stay under it with room to spare.
|
|
26
|
+
var ISSUE_URL_MAX = 8000;
|
|
27
|
+
var EXCERPT_MAX = 60;
|
|
28
|
+
|
|
29
|
+
function encodePath(value) {
|
|
30
|
+
return value.split('/').map(encodeURIComponent).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pageUrl(anchor) {
|
|
34
|
+
var url = location.href.split('#')[0];
|
|
35
|
+
return anchor ? url + '#' + encodeURIComponent(anchor) : url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function headingText(heading) {
|
|
39
|
+
var text = '';
|
|
40
|
+
for (var i = 0; i < heading.childNodes.length; i++) {
|
|
41
|
+
var node = heading.childNodes[i];
|
|
42
|
+
if (node.nodeType === 1 && node.classList.contains('anchor')) continue;
|
|
43
|
+
text += node.textContent;
|
|
44
|
+
}
|
|
45
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when `range` selects visible text inside `element`. */
|
|
49
|
+
function selectsTextIn(range, element) {
|
|
50
|
+
if (!range.intersectsNode(element)) return false;
|
|
51
|
+
var inner = document.createRange();
|
|
52
|
+
inner.selectNodeContents(element);
|
|
53
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, inner) > 0) inner.setStart(range.startContainer, range.startOffset);
|
|
54
|
+
if (range.compareBoundaryPoints(Range.END_TO_END, inner) < 0) inner.setEnd(range.endContainer, range.endOffset);
|
|
55
|
+
return inner.toString().trim() !== '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The current selection with its provenance, or null when nothing in
|
|
60
|
+
* <main> is selected. Lines span every annotated block the selection
|
|
61
|
+
* touches; the heading is the last one at or before the selection start.
|
|
62
|
+
*/
|
|
63
|
+
function selectionContext() {
|
|
64
|
+
var selection = window.getSelection();
|
|
65
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return null;
|
|
66
|
+
var text = selection.toString();
|
|
67
|
+
if (!text.trim()) return null;
|
|
68
|
+
var range = selection.getRangeAt(0);
|
|
69
|
+
if (!main.contains(range.commonAncestorContainer)) return null;
|
|
70
|
+
var start = null;
|
|
71
|
+
var end = null;
|
|
72
|
+
var blocks = main.querySelectorAll('[data-src-line]');
|
|
73
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
74
|
+
var match = LINE.exec(blocks[i].getAttribute('data-src-line') || '');
|
|
75
|
+
if (!match || !selectsTextIn(range, blocks[i])) continue;
|
|
76
|
+
var from = Number(match[1]);
|
|
77
|
+
var to = Number(match[2]);
|
|
78
|
+
if (start === null || from < start) start = from;
|
|
79
|
+
if (end === null || to > end) end = to;
|
|
80
|
+
}
|
|
81
|
+
var heading = null;
|
|
82
|
+
var headings = main.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]');
|
|
83
|
+
for (var j = 0; j < headings.length; j++) {
|
|
84
|
+
var h = headings[j];
|
|
85
|
+
var before = h.compareDocumentPosition(range.startContainer) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
86
|
+
if (before || h.contains(range.startContainer)) heading = h;
|
|
87
|
+
else break;
|
|
88
|
+
}
|
|
89
|
+
var anchor = heading ? heading.id : null;
|
|
90
|
+
return {
|
|
91
|
+
product: page.product,
|
|
92
|
+
title: page.title,
|
|
93
|
+
repo: page.repo,
|
|
94
|
+
path: page.path,
|
|
95
|
+
sha: page.sha,
|
|
96
|
+
branch: page.branch,
|
|
97
|
+
startLine: start,
|
|
98
|
+
endLine: end,
|
|
99
|
+
heading: heading ? headingText(heading) : null,
|
|
100
|
+
anchor: anchor,
|
|
101
|
+
url: pageUrl(anchor),
|
|
102
|
+
text: text.trim(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sourceRef(context) {
|
|
107
|
+
var lines = context.startLine === null ? '' : ':' + context.startLine + '-' + context.endLine;
|
|
108
|
+
return context.path + lines + '@' + context.sha;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function quote(text) {
|
|
112
|
+
return text
|
|
113
|
+
.split(/\r?\n/)
|
|
114
|
+
.map(function (line) {
|
|
115
|
+
return line ? '> ' + line : '>';
|
|
116
|
+
})
|
|
117
|
+
.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function provenance(context) {
|
|
121
|
+
return [
|
|
122
|
+
'Product: ' + context.title + ' (' + context.product + '), repository ' + context.repo,
|
|
123
|
+
'Source: ' + sourceRef(context),
|
|
124
|
+
'Section: ' + (context.heading || '(top of page)'),
|
|
125
|
+
'Page: ' + context.url,
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function locateHint(context) {
|
|
130
|
+
return (
|
|
131
|
+
'First call the kontour-docs MCP `locate` tool with product "' + context.product + '" and this quote to find the exact source, ' +
|
|
132
|
+
'then read the actual source code it describes (at ' + context.sha.slice(0, 12) + ' and on current main) before you answer. ' +
|
|
133
|
+
'Do not answer from the documentation alone.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function askPrompt(context) {
|
|
138
|
+
return [
|
|
139
|
+
'I am reading the ' + context.title + ' docs and want to ask about this passage.',
|
|
140
|
+
'',
|
|
141
|
+
provenance(context),
|
|
142
|
+
'',
|
|
143
|
+
quote(context.text),
|
|
144
|
+
'',
|
|
145
|
+
locateHint(context),
|
|
146
|
+
'',
|
|
147
|
+
'Question: ',
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function challengeText(context) {
|
|
152
|
+
return (
|
|
153
|
+
"Is this accurate? Verify the claim against the implementation at " + context.sha + " and current main. " +
|
|
154
|
+
"If it's wrong or unclear, propose a doc fix (or a code fix if the doc describes intended behaviour)."
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function challengePrompt(context) {
|
|
159
|
+
return ['I want to challenge this passage in the ' + context.title + ' docs.', '', provenance(context), '', quote(context.text), '', challengeText(context), '', locateHint(context)].join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The quote as a fenced code block: an issue body is live Markdown, and a
|
|
164
|
+
* blockquote would turn @names into mentions and #123 into references.
|
|
165
|
+
* The fence is longer than any backtick run in the text.
|
|
166
|
+
*/
|
|
167
|
+
function fenced(text) {
|
|
168
|
+
var longest = 2;
|
|
169
|
+
(text.match(/`+/g) || []).forEach(function (run) {
|
|
170
|
+
longest = Math.max(longest, run.length);
|
|
171
|
+
});
|
|
172
|
+
var fence = new Array(longest + 2).join('`');
|
|
173
|
+
return fence + 'text\n' + text + '\n' + fence;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function issueUrl(context) {
|
|
177
|
+
var title = 'Docs challenge: ' + context.path + (context.startLine === null ? '' : ':' + context.startLine + '-' + context.endLine);
|
|
178
|
+
var build = function (text, minimal) {
|
|
179
|
+
var lines = [challengeText(context), '', fenced(text), '', '- Source: `' + sourceRef(context) + '`'];
|
|
180
|
+
if (!minimal) lines.push('- Section: ' + (context.heading || '(top of page)'), '- Page: ' + context.url);
|
|
181
|
+
return (
|
|
182
|
+
'https://github.com/' + context.repo + '/issues/new?title=' + encodeURIComponent(minimal ? title.slice(0, 200).replace(/[\uD800-\uDBFF]$/, '') : title) +
|
|
183
|
+
'&body=' + encodeURIComponent(lines.join('\n')) + '&labels=docs-challenge'
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
var text = context.text;
|
|
187
|
+
var url = build(text, false);
|
|
188
|
+
var keep = text.length;
|
|
189
|
+
// Shrink the quote until the URL fits. One UTF-16 unit encodes to at most
|
|
190
|
+
// nine characters (%E2%80%A6), so cutting overflow/9 never overshoots.
|
|
191
|
+
while (url.length > ISSUE_URL_MAX && keep > 0) {
|
|
192
|
+
keep = Math.max(0, keep - Math.max(1, Math.ceil((url.length - ISSUE_URL_MAX) / 9)));
|
|
193
|
+
// Never leave half a surrogate pair: encodeURIComponent throws on one.
|
|
194
|
+
text = context.text.slice(0, keep).replace(/[\uD800-\uDBFF]$/, '').replace(/\s+$/, '') + '\u2026';
|
|
195
|
+
url = build(text, false);
|
|
196
|
+
}
|
|
197
|
+
// Still too long without any quote (an enormous heading or page URL): drop them.
|
|
198
|
+
if (url.length > ISSUE_URL_MAX) url = build('\u2026', true);
|
|
199
|
+
return url;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function editUrl(context) {
|
|
203
|
+
var hash = context.startLine === null ? '' : '#L' + context.startLine;
|
|
204
|
+
return 'https://github.com/' + context.repo + '/edit/' + encodePath(context.branch) + '/' + encodePath(context.path) + hash;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function excerpt(text) {
|
|
208
|
+
var flat = text.replace(/\s+/g, ' ');
|
|
209
|
+
return flat.length > EXCERPT_MAX ? flat.slice(0, EXCERPT_MAX - 1) + '…' : flat;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- UI -------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
var toast = document.createElement('div');
|
|
215
|
+
toast.className = 'dk-toast';
|
|
216
|
+
toast.setAttribute('role', 'status');
|
|
217
|
+
toast.setAttribute('aria-live', 'polite');
|
|
218
|
+
toast.setAttribute('aria-atomic', 'true');
|
|
219
|
+
var toastTimer = null;
|
|
220
|
+
|
|
221
|
+
function showToast(message) {
|
|
222
|
+
toast.textContent = message;
|
|
223
|
+
toast.classList.add('dk-visible');
|
|
224
|
+
clearTimeout(toastTimer);
|
|
225
|
+
toastTimer = setTimeout(function () {
|
|
226
|
+
toast.classList.remove('dk-visible');
|
|
227
|
+
}, 3200);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var dialog = null;
|
|
231
|
+
function showPromptDialog(label, prompt) {
|
|
232
|
+
if (!dialog) {
|
|
233
|
+
dialog = document.createElement('dialog');
|
|
234
|
+
dialog.className = 'dk-dialog';
|
|
235
|
+
dialog.setAttribute('aria-labelledby', 'dk-dialog-title');
|
|
236
|
+
var heading = document.createElement('h2');
|
|
237
|
+
heading.id = 'dk-dialog-title';
|
|
238
|
+
var help = document.createElement('p');
|
|
239
|
+
help.textContent = 'Clipboard access is not available here. Select the text below and copy it, then paste it into your agent.';
|
|
240
|
+
var area = document.createElement('textarea');
|
|
241
|
+
area.readOnly = true;
|
|
242
|
+
area.setAttribute('aria-label', 'Prompt');
|
|
243
|
+
var actions = document.createElement('div');
|
|
244
|
+
actions.className = 'dk-actions';
|
|
245
|
+
var close = document.createElement('button');
|
|
246
|
+
close.type = 'button';
|
|
247
|
+
close.className = 'dk-primary';
|
|
248
|
+
close.textContent = 'Close';
|
|
249
|
+
close.addEventListener('click', function () {
|
|
250
|
+
if (typeof dialog.close === 'function') dialog.close();
|
|
251
|
+
else dialog.removeAttribute('open');
|
|
252
|
+
});
|
|
253
|
+
actions.appendChild(close);
|
|
254
|
+
dialog.appendChild(heading);
|
|
255
|
+
dialog.appendChild(help);
|
|
256
|
+
dialog.appendChild(area);
|
|
257
|
+
dialog.appendChild(actions);
|
|
258
|
+
document.body.appendChild(dialog);
|
|
259
|
+
}
|
|
260
|
+
dialog.querySelector('h2').textContent = label;
|
|
261
|
+
var textarea = dialog.querySelector('textarea');
|
|
262
|
+
textarea.value = prompt;
|
|
263
|
+
if (typeof dialog.showModal === 'function') {
|
|
264
|
+
if (!dialog.open) dialog.showModal();
|
|
265
|
+
} else dialog.setAttribute('open', '');
|
|
266
|
+
textarea.focus();
|
|
267
|
+
textarea.select();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function copy(label, prompt, context) {
|
|
271
|
+
var done = function () {
|
|
272
|
+
showToast('Copied ' + label.toLowerCase() + ' for “' + excerpt(context.text) + '”. Paste it into your agent.');
|
|
273
|
+
};
|
|
274
|
+
var fallback = function () {
|
|
275
|
+
showPromptDialog(label, prompt);
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
279
|
+
navigator.clipboard.writeText(prompt).then(done, fallback);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// fall through to the dialog
|
|
284
|
+
}
|
|
285
|
+
fallback();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function open(url) {
|
|
289
|
+
window.open(url, '_blank', 'noopener');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
var ACTIONS = {
|
|
293
|
+
ask: { label: 'Ask', shortcut: 'Alt+A', title: 'Copy a prompt that asks your agent about this passage', run: function (c) { copy('Ask prompt', askPrompt(c), c); } },
|
|
294
|
+
challenge: { label: 'Challenge', shortcut: 'Alt+C', title: 'Copy a prompt that asks your agent to verify this passage', run: function (c) { copy('Challenge prompt', challengePrompt(c), c); } },
|
|
295
|
+
'challenge-issue': { label: 'Open issue', title: 'Open a prefilled GitHub issue challenging this passage', run: function (c) { open(issueUrl(c)); } },
|
|
296
|
+
edit: { label: 'Edit', title: 'Edit the source of this passage on GitHub', run: function (c) { open(editUrl(c)); } },
|
|
297
|
+
};
|
|
298
|
+
var order = page.primary === 'challenge-issue' ? ['challenge-issue', 'challenge', 'ask', 'edit'] : ['ask', 'challenge', 'challenge-issue', 'edit'];
|
|
299
|
+
|
|
300
|
+
var toolbar = document.createElement('div');
|
|
301
|
+
toolbar.className = 'dk-toolbar';
|
|
302
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
303
|
+
toolbar.setAttribute('aria-label', 'Selected text actions');
|
|
304
|
+
toolbar.hidden = true;
|
|
305
|
+
var current = null;
|
|
306
|
+
// A tap on a touch screen can clear the selection before its click arrives,
|
|
307
|
+
// so the last shown context stays usable while the toolbar is being pressed.
|
|
308
|
+
var recent = null;
|
|
309
|
+
var pressing = false;
|
|
310
|
+
order.forEach(function (name, index) {
|
|
311
|
+
var action = ACTIONS[name];
|
|
312
|
+
var button = document.createElement('button');
|
|
313
|
+
button.type = 'button';
|
|
314
|
+
button.setAttribute('data-action', name);
|
|
315
|
+
button.title = action.title;
|
|
316
|
+
button.textContent = action.label;
|
|
317
|
+
button.setAttribute('aria-label', action.label);
|
|
318
|
+
if (index === 0) button.className = 'dk-primary';
|
|
319
|
+
if (action.shortcut) {
|
|
320
|
+
button.setAttribute('aria-keyshortcuts', action.shortcut);
|
|
321
|
+
var kbd = document.createElement('kbd');
|
|
322
|
+
kbd.textContent = action.shortcut;
|
|
323
|
+
button.appendChild(kbd);
|
|
324
|
+
}
|
|
325
|
+
button.tabIndex = index === 0 ? 0 : -1;
|
|
326
|
+
button.addEventListener('click', function () {
|
|
327
|
+
// The live selection wins: the toolbar's context can be up to one debounce old.
|
|
328
|
+
var context = selectionContext() || current || recent;
|
|
329
|
+
pressing = false;
|
|
330
|
+
if (context) action.run(context);
|
|
331
|
+
});
|
|
332
|
+
toolbar.appendChild(button);
|
|
333
|
+
});
|
|
334
|
+
// Keep the selection when a toolbar button is pressed with a mouse.
|
|
335
|
+
toolbar.addEventListener('mousedown', function (event) {
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
});
|
|
338
|
+
toolbar.addEventListener('pointerdown', function () {
|
|
339
|
+
pressing = true;
|
|
340
|
+
});
|
|
341
|
+
toolbar.addEventListener('pointercancel', function () {
|
|
342
|
+
pressing = false;
|
|
343
|
+
});
|
|
344
|
+
window.addEventListener('pointerup', function () {
|
|
345
|
+
if (pressing)
|
|
346
|
+
setTimeout(function () {
|
|
347
|
+
pressing = false;
|
|
348
|
+
}, 400);
|
|
349
|
+
});
|
|
350
|
+
toolbar.addEventListener('keydown', function (event) {
|
|
351
|
+
var buttons = toolbar.querySelectorAll('button');
|
|
352
|
+
var index = Array.prototype.indexOf.call(buttons, document.activeElement);
|
|
353
|
+
var next = null;
|
|
354
|
+
if (event.key === 'ArrowRight') next = (index + 1) % buttons.length;
|
|
355
|
+
else if (event.key === 'ArrowLeft') next = (index - 1 + buttons.length) % buttons.length;
|
|
356
|
+
else if (event.key === 'Home') next = 0;
|
|
357
|
+
else if (event.key === 'End') next = buttons.length - 1;
|
|
358
|
+
else if (event.key === 'Escape') {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
hide();
|
|
361
|
+
main.focus({ preventScroll: true });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (next === null) return;
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
for (var i = 0; i < buttons.length; i++) buttons[i].tabIndex = i === next ? 0 : -1;
|
|
367
|
+
buttons[next].focus();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
var coarse = window.matchMedia ? window.matchMedia('(pointer: coarse)') : { matches: false };
|
|
371
|
+
var reduced = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : { matches: false };
|
|
372
|
+
|
|
373
|
+
function place() {
|
|
374
|
+
var selection = window.getSelection();
|
|
375
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
376
|
+
var rects = selection.getRangeAt(0).getClientRects();
|
|
377
|
+
var rect = rects.length ? rects[coarse.matches ? rects.length - 1 : 0] : selection.getRangeAt(0).getBoundingClientRect();
|
|
378
|
+
var width = toolbar.offsetWidth;
|
|
379
|
+
var height = toolbar.offsetHeight;
|
|
380
|
+
var gap = 8;
|
|
381
|
+
// Touch browsers draw their own selection menu above the selection, so go below it there.
|
|
382
|
+
var top = coarse.matches || rect.top - height - gap < 0 ? rect.bottom + gap : rect.top - height - gap;
|
|
383
|
+
var left = Math.min(Math.max(gap, rect.left), document.documentElement.clientWidth - width - gap);
|
|
384
|
+
toolbar.style.top = Math.round(top + window.scrollY) + 'px';
|
|
385
|
+
toolbar.style.left = Math.round(Math.max(gap, left) + window.scrollX) + 'px';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function show(context) {
|
|
389
|
+
current = context;
|
|
390
|
+
recent = context;
|
|
391
|
+
var wasHidden = toolbar.hidden;
|
|
392
|
+
toolbar.hidden = false;
|
|
393
|
+
place();
|
|
394
|
+
if (wasHidden && !reduced.matches) {
|
|
395
|
+
toolbar.classList.add('dk-entering');
|
|
396
|
+
requestAnimationFrame(function () {
|
|
397
|
+
toolbar.classList.remove('dk-entering');
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function hide() {
|
|
403
|
+
toolbar.hidden = true;
|
|
404
|
+
current = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
var pending = null;
|
|
408
|
+
document.addEventListener('selectionchange', function () {
|
|
409
|
+
clearTimeout(pending);
|
|
410
|
+
pending = setTimeout(function () {
|
|
411
|
+
if (pressing || toolbar.contains(document.activeElement)) return;
|
|
412
|
+
var context = selectionContext();
|
|
413
|
+
if (context) show(context);
|
|
414
|
+
else hide();
|
|
415
|
+
}, 80);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
document.addEventListener('keydown', function (event) {
|
|
419
|
+
if (!event.altKey || event.ctrlKey || event.metaKey) return;
|
|
420
|
+
var name = event.code === 'KeyA' ? 'ask' : event.code === 'KeyC' ? 'challenge' : event.code === 'KeyT' ? 'focus' : null;
|
|
421
|
+
if (!name) return;
|
|
422
|
+
var context = selectionContext() || current;
|
|
423
|
+
if (!context) return;
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
if (name === 'focus') {
|
|
426
|
+
show(context);
|
|
427
|
+
var first = toolbar.querySelector('button[tabindex="0"]') || toolbar.querySelector('button');
|
|
428
|
+
first.focus();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
ACTIONS[name].run(context);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
window.addEventListener('resize', function () {
|
|
435
|
+
if (!toolbar.hidden) place();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (!main.hasAttribute('tabindex')) main.setAttribute('tabindex', '-1');
|
|
439
|
+
document.body.appendChild(toolbar);
|
|
440
|
+
document.body.appendChild(toast);
|
|
441
|
+
|
|
442
|
+
// ---- WebMCP (progressive; Chromium-only draft API) -------------------
|
|
443
|
+
|
|
444
|
+
var bundlePromise = null;
|
|
445
|
+
function loadBundle(signal) {
|
|
446
|
+
if (!bundlePromise) {
|
|
447
|
+
bundlePromise = fetch(new URL('docs-bundle.json', siteRoot).href, { signal: signal, credentials: 'same-origin' }).then(function (response) {
|
|
448
|
+
if (!response.ok) throw new Error('docs-bundle.json: HTTP ' + response.status);
|
|
449
|
+
return response.json();
|
|
450
|
+
});
|
|
451
|
+
bundlePromise.catch(function () {
|
|
452
|
+
bundlePromise = null;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return bundlePromise;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function words(text) {
|
|
459
|
+
var seen = {};
|
|
460
|
+
return String(text)
|
|
461
|
+
.toLowerCase()
|
|
462
|
+
.split(/[^\p{L}\p{N}_]+/u)
|
|
463
|
+
.filter(function (word) {
|
|
464
|
+
if (!word || seen[word]) return false;
|
|
465
|
+
seen[word] = true;
|
|
466
|
+
return true;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function count(haystack, needle) {
|
|
471
|
+
var total = 0;
|
|
472
|
+
for (var at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) total++;
|
|
473
|
+
return total;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function searchBundle(bundle, query, limit) {
|
|
477
|
+
var terms = words(query);
|
|
478
|
+
var items = bundle.topics
|
|
479
|
+
.map(function (topic) {
|
|
480
|
+
return { kind: 'topic', id: topic.id, title: topic.title, source: topic.source, url: topic.url, meta: topic.id + ' ' + topic.source, markdown: topic.markdown };
|
|
481
|
+
})
|
|
482
|
+
.concat(
|
|
483
|
+
bundle.documents.map(function (document) {
|
|
484
|
+
return { kind: 'document', title: document.title, source: document.source, url: document.url, meta: document.source, markdown: document.markdown };
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
var results = [];
|
|
488
|
+
items.forEach(function (item) {
|
|
489
|
+
var title = item.title.toLowerCase();
|
|
490
|
+
var meta = item.meta.toLowerCase();
|
|
491
|
+
var body = item.markdown.toLowerCase();
|
|
492
|
+
var score = 0;
|
|
493
|
+
var matched = 0;
|
|
494
|
+
terms.forEach(function (term) {
|
|
495
|
+
var termScore = count(title, term) * 5 + count(meta, term) * 3 + Math.min(count(body, term), 10);
|
|
496
|
+
if (termScore > 0) matched++;
|
|
497
|
+
score += termScore;
|
|
498
|
+
});
|
|
499
|
+
if (!matched) return;
|
|
500
|
+
var at = Math.max(0, body.indexOf(terms[0]) - 80);
|
|
501
|
+
results.push({
|
|
502
|
+
kind: item.kind,
|
|
503
|
+
id: item.id,
|
|
504
|
+
title: item.title,
|
|
505
|
+
source: item.source,
|
|
506
|
+
url: item.url,
|
|
507
|
+
score: Math.round(score * (matched / terms.length) * 100) / 100,
|
|
508
|
+
snippet: item.markdown.slice(at, at + 220).replace(/\s+/g, ' ').trim(),
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
results.sort(function (a, b) {
|
|
512
|
+
return b.score - a.score || (a.kind === b.kind ? 0 : a.kind === 'topic' ? -1 : 1);
|
|
513
|
+
});
|
|
514
|
+
return results.slice(0, limit);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function registerWebMcp() {
|
|
518
|
+
var mc;
|
|
519
|
+
try {
|
|
520
|
+
mc = document.modelContext || navigator.modelContext;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (!mc || typeof mc.registerTool !== 'function') return;
|
|
525
|
+
var controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
526
|
+
var handles = [];
|
|
527
|
+
// The current draft resolves registerTool with a Promise and serializes any
|
|
528
|
+
// return value to JSON; the older synchronous API expected MCP-style content.
|
|
529
|
+
var legacy = false;
|
|
530
|
+
var reply = function (data) {
|
|
531
|
+
return legacy ? { content: [{ type: 'text', text: JSON.stringify(data) }] } : data;
|
|
532
|
+
};
|
|
533
|
+
var safely = function (run) {
|
|
534
|
+
return function (input, options) {
|
|
535
|
+
return Promise.resolve()
|
|
536
|
+
.then(function () {
|
|
537
|
+
return run(input || {}, (options && options.signal) || undefined);
|
|
538
|
+
})
|
|
539
|
+
.then(reply, function (error) {
|
|
540
|
+
return reply({ error: String((error && error.message) || error) });
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
var base = { product: page.product, repo: page.repo, sha: page.sha };
|
|
545
|
+
var tools = [
|
|
546
|
+
{
|
|
547
|
+
name: 'get_selection_context',
|
|
548
|
+
title: 'Selected docs text',
|
|
549
|
+
description: 'The text the user has selected on this ' + page.title + ' docs page, with its source provenance: repository, file path, 1-based line range, commit sha, enclosing heading and anchor, and page URL.',
|
|
550
|
+
inputSchema: { type: 'object', properties: {} },
|
|
551
|
+
annotations: { readOnlyHint: true },
|
|
552
|
+
execute: safely(function () {
|
|
553
|
+
var context = selectionContext() || current;
|
|
554
|
+
return context ? { selection: context } : { selection: null, page: { product: page.product, repo: page.repo, path: page.path, sha: page.sha, url: pageUrl(null) }, reason: 'no text is selected in the page content' };
|
|
555
|
+
}),
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: 'search_docs',
|
|
559
|
+
title: 'Search these docs',
|
|
560
|
+
description: 'Keyword search over the ' + page.title + ' documentation bundle (topics and documents) served by this site.',
|
|
561
|
+
inputSchema: {
|
|
562
|
+
type: 'object',
|
|
563
|
+
properties: { query: { type: 'string', description: 'Words to look for.' }, limit: { type: 'integer', minimum: 1, maximum: 50, description: 'Maximum results (default 10).' } },
|
|
564
|
+
required: ['query'],
|
|
565
|
+
},
|
|
566
|
+
annotations: { readOnlyHint: true },
|
|
567
|
+
execute: safely(function (input, signal) {
|
|
568
|
+
if (!words(input.query || '').length) return { error: 'query must contain at least one word' };
|
|
569
|
+
var limit = Math.min(50, Math.max(1, Math.floor(Number(input.limit) || 10)));
|
|
570
|
+
return loadBundle(signal).then(function (bundle) {
|
|
571
|
+
return Object.assign({ query: input.query, results: searchBundle(bundle, input.query, limit) }, base, { sha: bundle.sha });
|
|
572
|
+
});
|
|
573
|
+
}),
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'get_topic',
|
|
577
|
+
title: 'Read a docs topic',
|
|
578
|
+
description: 'Read one curated ' + page.title + ' documentation topic as Markdown, by id. search_docs returns topic ids.',
|
|
579
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Topic id.' } }, required: ['id'] },
|
|
580
|
+
annotations: { readOnlyHint: true },
|
|
581
|
+
execute: safely(function (input, signal) {
|
|
582
|
+
return loadBundle(signal).then(function (bundle) {
|
|
583
|
+
var topic = bundle.topics.filter(function (entry) {
|
|
584
|
+
return entry.id === input.id;
|
|
585
|
+
})[0];
|
|
586
|
+
if (!topic) return { error: 'unknown topic "' + input.id + '"', availableTopics: bundle.topics.map(function (entry) { return entry.id; }) };
|
|
587
|
+
return Object.assign({}, base, { sha: bundle.sha, topic: topic });
|
|
588
|
+
});
|
|
589
|
+
}),
|
|
590
|
+
},
|
|
591
|
+
];
|
|
592
|
+
tools.forEach(function (tool) {
|
|
593
|
+
try {
|
|
594
|
+
var result = controller ? mc.registerTool(tool, { signal: controller.signal }) : mc.registerTool(tool);
|
|
595
|
+
if (result && typeof result.then === 'function') result.then(null, function () {});
|
|
596
|
+
else {
|
|
597
|
+
legacy = true;
|
|
598
|
+
if (result && typeof result.unregister === 'function') handles.push(result);
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
// An unsupported or changed API must never break the page.
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
window.addEventListener('pagehide', function (event) {
|
|
605
|
+
if (event.persisted) return;
|
|
606
|
+
try {
|
|
607
|
+
if (controller) controller.abort();
|
|
608
|
+
handles.forEach(function (handle) {
|
|
609
|
+
handle.unregister();
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
// ignore
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
registerWebMcp();
|
|
619
|
+
} catch (error) {
|
|
620
|
+
// WebMCP is optional.
|
|
621
|
+
}
|
|
622
|
+
})();
|