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.