PageSourceSearch

https://smalldocs.org/public/sdocs-docwalk.js?v=0ef0401827

js smalldocs.org collected 2026-09-29 16:35:56 UTC 7,691 bytes, 222 lines download raw bytes

1// sdocs-docwalk.js - pure model for an agent-authored Markdown walkthrough.
2//
3// The CLI stores ordered, 1-based source-line annotations in front matter.
4// This module uses marked's top-level lexer tokens to translate those source
5// lines into rendered-document targets. Ordinary prose resolves through the
6// same tag:index block vocabulary as comments. Rich fenced blocks resolve to
7// their finished reader wrappers. Ordinary code fences retain their source-line
8// geometry so the reader can place a card directly below the relevant line.
9(function (exports) {
10'use strict';
11
12var MAX_STEPS = 300;
13
14var RICH_FENCES = {
15  chart:   { kind: 'block', type: 'chart' },
16  mermaid: { kind: 'rich', selector: '.sdoc-mermaid', label: 'diagram' },
17  cells:   { kind: 'rich', selector: '.sdoc-cells', label: 'sheet' },
18  slide:   { kind: 'rich', selector: '.sdoc-slide', label: 'slide' },
19  slides:  { kind: 'rich', selector: '.sdoc-slide', label: 'slide' },
20  form:    { kind: 'rich', selector: '.sdoc-form-host', label: 'form' },
21  video:   { kind: 'rich', selector: '.sdoc-video', label: 'video' },
22  'sdoc-app': { kind: 'rich', selector: '.sdoc-app', label: 'runnable component' },
23};
24
25function str(value) { return typeof value === 'string' ? value : ''; }
26
27function truthy(value) {
28  if (value === true) return true;
29  value = str(value).toLowerCase().trim();
30  return value === 'true' || value === 'yes' || value === 'on' || value === '1';
31}
32
33function isDocwalk(meta) {
34  return truthy(meta && meta.docwalk);
35}
36
37function lineAt(text, offset) {
38  var line = 1;
39  for (var i = 0; i < offset && i < text.length; i++) {
40    if (text.charAt(i) === '\n') line++;
41  }
42  return line;
43}
44
45function nextIndex(counters, key) {
46  var n = counters[key] || 0;
47  counters[key] = n + 1;
48  return n;
49}
50
51function descriptorFor(token, counters) {
52  if (!token) return null;
53  if (token.type === 'heading') {
54    var heading = 'h' + parseInt(token.depth, 10);
55    return { kind: 'block', type: heading, index: nextIndex(counters, heading) };
56  }
57  if (token.type === 'paragraph' || token.type === 'text') {
58    return { kind: 'block', type: 'p', index: nextIndex(counters, 'p'), inline: true };
59  }
60  if (token.type === 'blockquote') {
61    return { kind: 'block', type: 'blockquote', index: nextIndex(counters, 'blockquote') };
62  }
63  if (token.type === 'list') {
64    var list = token.ordered ? 'ol' : 'ul';
65    return { kind: 'block', type: list, index: nextIndex(counters, list) };
66  }
67  if (token.type === 'table') {
68    return { kind: 'block', type: 'table', index: nextIndex(counters, 'table') };
69  }
70  if (token.type === 'hr') {
71    return { kind: 'rich', selector: 'hr', label: 'divider', index: nextIndex(counters, 'hr') };
72  }
73  if (token.type === 'sdocsMathBlock') {
74    var mathSelector = '.sdocs-math-display';
75    return { kind: 'rich', selector: mathSelector, label: 'math block',
76      index: nextIndex(counters, mathSelector) };
77  }
78  if (token.type === 'code') {
79    var language = str(token.lang).trim().split(/\s+/)[0].toLowerCase();
80    var rich = RICH_FENCES[language];
81    if (rich) {
82      var richKey = rich.kind === 'block' ? rich.type : rich.selector;
83      return Object.assign({}, rich, { index: nextIndex(counters, richKey) });
84    }
85    var codeText = str(token.text).replace(/\n$/, '');
86    return {
87      kind: 'block', type: 'pre', index: nextIndex(counters, 'pre'),
88      code: true, codeLineCount: Math.max(1, codeText.split('\n').length),
89    };
90  }
91  return null;
92}
93
94function tokenTargets(body, lexer) {
95  var tokens;
96  try { tokens = lexer(body); } catch (_) { return []; }
97  if (!Array.isArray(tokens)) return [];
98
99  var counters = Object.create(null);
100  var cursor = 0;
101  var out = [];
102  for (var i = 0; i < tokens.length; i++) {
103    var token = tokens[i];
104    var raw = str(token && token.raw);
105    var at = raw ? body.indexOf(raw, cursor) : cursor;
106    if (at < 0) at = cursor;
107    var desc = descriptorFor(token, counters);
108    if (desc) {
109      var content = raw.replace(/\n+$/, '');
110      var startLine = lineAt(body, at);
111      var endOffset = at + Math.max(0, content.length - 1);
112      var endLine = lineAt(body, endOffset);
113      out.push({
114        startLine: startLine,
115        endLine: Math.max(startLine, endLine),
116        raw: raw,
117        descriptor: desc,
118      });
119    }
120    cursor = Math.max(cursor, at + raw.length);
121  }
122  return out;
123}
124
125function selectedSource(body, target, startLine, endLine) {
126  if (!target.descriptor.inline) return '';
127  var lines = body.replace(/\r\n?/g, '\n').split('\n');
128  var from = Math.max(startLine, target.startLine) - 1;
129  var to = Math.min(endLine, target.endLine);
130  return lines.slice(from, to).join('\n').trim();
131}
132
133function targetKey(target) {
134  var d = target.descriptor;
135  return d.kind === 'block'
136    ? 'block:' + d.type + ':' + d.index
137    : 'rich:' + d.selector + ':' + d.index;
138}
139
140function targetsForRange(body, targets, startLine, endLine, annotation) {
141  var hits = targets.filter(function (target) {
142    return target.startLine <= endLine && target.endLine >= startLine;
143  });
144  // An annotation on a blank line belongs to the next rendered block. If it is
145  // after the final block, use the preceding one. This keeps source line
146  // numbers useful without turning whitespace into an orphaned step.
147  if (!hits.length) {
148    var next = targets.find(function (target) { return target.startLine > startLine; });
149    if (next) hits = [next];
150    else if (targets.length) hits = [targets[targets.length - 1]];
151  }
152
153  var seen = Object.create(null);
154  var out = [];
155  hits.forEach(function (target) {
156    var key = targetKey(target);
157    if (seen[key]) return;
158    seen[key] = true;
159    var d = Object.assign({}, target.descriptor);
160    d.startLine = target.startLine;
161    d.endLine = target.endLine;
162    var quote = str(annotation && annotation.quote).trim();
163    d.source = quote || selectedSource(body, target, startLine, endLine);
164    if (d.code) {
165      d.codeLine = Math.max(1, Math.min(d.codeLineCount, startLine - target.startLine));
166      d.codeEndLine = Math.max(d.codeLine,
167        Math.min(d.codeLineCount, endLine - target.startLine));
168      d.quote = quote;
169    }
170    out.push(d);
171  });
172  return out;
173}
174
175function build(meta, body, lexer) {
176  meta = meta || {};
177  body = str(body).replace(/\r\n?/g, '\n');
178  if (!isDocwalk(meta) || typeof lexer !== 'function') {
179    return { steps: [], total: 0 };
180  }
181  var sourceTargets = tokenTargets(body, lexer);
182  var lineCount = body ? body.split('\n').length : 0;
183  var raw = Array.isArray(meta.annotations) ? meta.annotations : [];
184  var steps = [];
185  for (var i = 0; i < raw.length && steps.length < MAX_STEPS; i++) {
186    var annotation = raw[i];
187    if (!annotation) continue;
188    var line = parseInt(annotation.line, 10);
189    if (!(line >= 1) || line > lineCount) continue;
190    var endLine = parseInt(annotation.endLine, 10);
191    if (!(endLine >= line)) endLine = line;
192    endLine = Math.min(endLine, lineCount);
193    var text = str(annotation.text);
194    if (!text.trim()) continue;
195    var targets = targetsForRange(body, sourceTargets, line, endLine, annotation);
196    if (!targets.length) continue;
197    steps.push({
198      line: line,
199      endLine: endLine,
200      text: text,
201      targets: targets,
202      index: steps.length,
203    });
204  }
205  return { steps: steps, total: steps.length };
206}
207
208function clamp(index, total) {
209  if (total <= 0) return -1;
210  if (index < 0) return 0;
211  if (index >= total) return total - 1;
212  return index;
213}
214
215exports.build = build;
216exports.clamp = clamp;
217exports.isDocwalk = isDocwalk;
218exports.MAX_STEPS = MAX_STEPS;
219
220})(typeof module !== 'undefined' && module.exports
221  ? module.exports
222  : (window.SDocDocwalk = {}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.