@zeldrisho/pi-web-fetch 0.8.1 → 0.9.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/src/outline.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  const FENCE = /^[ \t]{0,3}(`{3,}|~{3,})/;
2
+
2
3
  const MAX_FALLBACK_HEADING_CHARACTERS = 60;
4
+
3
5
  export const MAX_OUTLINE_HEADINGS = 12;
6
+
4
7
  export const MAX_OUTLINE_HEADING_CHARACTERS = 120;
5
8
 
6
9
  /** One bounded heading exposed as untrusted document-shape metadata. */
@@ -41,15 +44,20 @@ interface MarkdownFence {
41
44
  length: number;
42
45
  }
43
46
 
47
+ /** Tracks whether the current Markdown line opens or closes a fenced code block. */
44
48
  function updateFence(line: string, fence: MarkdownFence | undefined): MarkdownFence | undefined {
45
49
  const match = FENCE.exec(line);
50
+
46
51
  if (!match) return fence;
47
52
  const marker = match[1];
53
+
48
54
  if (!fence) return { character: marker[0], length: marker.length };
55
+
49
56
  const closes =
50
57
  marker[0] === fence.character &&
51
58
  marker.length >= fence.length &&
52
59
  !line.slice(match[0].length).trim();
60
+
53
61
  return closes ? undefined : fence;
54
62
  }
55
63
 
@@ -66,15 +74,20 @@ function isSpaceOrTab(character: string | undefined): boolean {
66
74
  /** Parses the bounded ATX prefix with a linear scan to avoid backtracking on remote input. */
67
75
  function parseAtxHeading(line: string): ParsedAtxHeading | undefined {
68
76
  let index = 0;
77
+
69
78
  while (index < 3 && isSpaceOrTab(line[index])) index += 1;
79
+
70
80
  if (isSpaceOrTab(line[index])) return undefined;
71
81
 
72
82
  const markerStart = index;
83
+
73
84
  while (line[index] === "#") index += 1;
74
85
  const level = index - markerStart;
86
+
75
87
  if (level < 1 || level > 6 || !isSpaceOrTab(line[index])) return undefined;
76
88
 
77
89
  while (isSpaceOrTab(line[index])) index += 1;
90
+
78
91
  return { level, text: line.slice(index) };
79
92
  }
80
93
 
@@ -86,6 +99,7 @@ function parseAtxHeading(line: string): ParsedAtxHeading | undefined {
86
99
  */
87
100
  function countWords(value: string): number {
88
101
  const trimmed = value.trim();
102
+
89
103
  return trimmed ? trimmed.split(/\s+/).length : 0;
90
104
  }
91
105
 
@@ -97,10 +111,12 @@ function countWords(value: string): number {
97
111
  */
98
112
  function countDocumentWords(lines: string[]): number {
99
113
  let fence: MarkdownFence | undefined;
114
+
100
115
  return lines.reduce((total, line) => {
101
116
  const wasInsideFence = Boolean(fence);
102
117
  fence = updateFence(line, fence);
103
118
  const heading = wasInsideFence || fence ? undefined : parseAtxHeading(line);
119
+
104
120
  return total + countWords(heading ? cleanHeading(heading.text) : line);
105
121
  }, 0);
106
122
  }
@@ -113,12 +129,16 @@ function countDocumentWords(lines: string[]): number {
113
129
  */
114
130
  function cleanHeading(value: string): string {
115
131
  let contentEnd = value.length;
132
+
116
133
  while (contentEnd > 0 && isSpaceOrTab(value[contentEnd - 1])) contentEnd -= 1;
117
134
 
118
135
  let markerStart = contentEnd;
136
+
119
137
  while (markerStart > 0 && value[markerStart - 1] === "#") markerStart -= 1;
138
+
120
139
  if (markerStart < contentEnd && markerStart > 0 && isSpaceOrTab(value[markerStart - 1])) {
121
140
  contentEnd = markerStart - 1;
141
+
122
142
  while (contentEnd > 0 && isSpaceOrTab(value[contentEnd - 1])) contentEnd -= 1;
123
143
  }
124
144
 
@@ -139,16 +159,21 @@ function collectAtxHeadings(lines: string[]): HeadingCollection {
139
159
  for (const [lineNumber, line] of lines.entries()) {
140
160
  const wasInsideFence = Boolean(fence);
141
161
  fence = updateFence(line, fence);
162
+
142
163
  if (wasInsideFence || fence) continue;
143
164
  const heading = parseAtxHeading(line);
165
+
144
166
  if (!heading) continue;
145
167
  const text = cleanHeading(heading.text);
168
+
146
169
  if (!text) continue;
147
170
  total += 1;
171
+
148
172
  if (locations.length <= MAX_OUTLINE_HEADINGS) {
149
173
  locations.push({ level: heading.level, text, line: lineNumber, inferred: false });
150
174
  }
151
175
  }
176
+
152
177
  return { locations, total };
153
178
  }
154
179
 
@@ -162,6 +187,7 @@ function collectAtxHeadings(lines: string[]): HeadingCollection {
162
187
  function looksLikeFallbackHeading(line: string, nextLine: string): boolean {
163
188
  if (/^\s{4,}/.test(line)) return false;
164
189
  const candidate = line.trim();
190
+
165
191
  if (
166
192
  !candidate ||
167
193
  candidate.length > MAX_FALLBACK_HEADING_CHARACTERS ||
@@ -173,8 +199,10 @@ function looksLikeFallbackHeading(line: string, nextLine: string): boolean {
173
199
  ) {
174
200
  return false;
175
201
  }
202
+
176
203
  if (!/^[A-Z]/.test(candidate) && !/[`()._]/.test(candidate)) return false;
177
204
  const following = nextLine.trim();
205
+
178
206
  return following.length > MAX_FALLBACK_HEADING_CHARACTERS || /[.!?]$/.test(following);
179
207
  }
180
208
 
@@ -190,25 +218,32 @@ function collectFallbackHeadings(lines: string[]): HeadingCollection {
190
218
  const nextNonEmptyIndexes = new Int32Array(lines.length);
191
219
  nextNonEmptyIndexes.fill(-1);
192
220
  let nextNonEmptyIndex = -1;
221
+
193
222
  for (let lineNumber = lines.length - 1; lineNumber >= 0; lineNumber -= 1) {
194
223
  nextNonEmptyIndexes[lineNumber] = nextNonEmptyIndex;
224
+
195
225
  if (lines[lineNumber].trim()) nextNonEmptyIndex = lineNumber;
196
226
  }
227
+
197
228
  let fence: MarkdownFence | undefined;
198
229
 
199
230
  for (const [lineNumber, line] of lines.entries()) {
200
231
  const wasInsideFence = Boolean(fence);
201
232
  fence = updateFence(line, fence);
233
+
202
234
  if (wasInsideFence || fence) continue;
203
235
  const followingIndex = nextNonEmptyIndexes[lineNumber];
204
236
  const following = followingIndex === -1 ? "" : lines[followingIndex];
237
+
205
238
  if (looksLikeFallbackHeading(line, following)) {
206
239
  total += 1;
240
+
207
241
  if (locations.length <= MAX_OUTLINE_HEADINGS) {
208
242
  locations.push({ level: 2, text: cleanHeading(line), line: lineNumber, inferred: true });
209
243
  }
210
244
  }
211
245
  }
246
+
212
247
  return { locations, total };
213
248
  }
214
249
 
@@ -222,8 +257,10 @@ export function createDocumentOutline(markdown: string): DocumentOutline {
222
257
  const lines = markdown.split("\n");
223
258
  const explicit = collectAtxHeadings(lines);
224
259
  const collection = explicit.total > 0 ? explicit : collectFallbackHeadings(lines);
260
+
225
261
  const headings = collection.locations.slice(0, MAX_OUTLINE_HEADINGS).map((heading, index) => {
226
262
  const next = collection.locations[index + 1]?.line ?? lines.length;
263
+
227
264
  return {
228
265
  level: heading.level,
229
266
  text: heading.text,
@@ -231,6 +268,7 @@ export function createDocumentOutline(markdown: string): DocumentOutline {
231
268
  inferred: heading.inferred,
232
269
  };
233
270
  });
271
+
234
272
  return {
235
273
  totalWords: countDocumentWords(lines),
236
274
  totalHeadings: collection.total,
package/src/redact.ts CHANGED
@@ -17,14 +17,17 @@ const SENSITIVE_QUERY_KEYS = new Set([
17
17
  "token",
18
18
  ]);
19
19
 
20
+ /** Replaces sensitive query-parameter values and reports whether anything changed. */
20
21
  function redactSensitiveParams(params: URLSearchParams): boolean {
21
22
  let redacted = false;
23
+
22
24
  for (const key of params.keys()) {
23
25
  if (SENSITIVE_QUERY_KEYS.has(key.toLowerCase())) {
24
26
  params.set(key, "REDACTED");
25
27
  redacted = true;
26
28
  }
27
29
  }
30
+
28
31
  return redacted;
29
32
  }
30
33
 
@@ -37,6 +40,7 @@ function redactSensitiveParams(params: URLSearchParams): boolean {
37
40
  */
38
41
  export function redactUrlForDisplay(value: string | URL): string {
39
42
  let url: URL;
43
+
40
44
  try {
41
45
  url = value instanceof URL ? new URL(value.href) : new URL(value);
42
46
  } catch {
@@ -46,9 +50,12 @@ export function redactUrlForDisplay(value: string | URL): string {
46
50
  url.username = "";
47
51
  url.password = "";
48
52
  redactSensitiveParams(url.searchParams);
53
+
49
54
  if (url.hash.length > 1) {
50
55
  const fragmentParams = new URLSearchParams(url.hash.slice(1));
56
+
51
57
  if (redactSensitiveParams(fragmentParams)) url.hash = fragmentParams.toString();
52
58
  }
59
+
53
60
  return url.href;
54
61
  }
package/src/render.ts CHANGED
@@ -13,14 +13,17 @@ export function formatCollapsibleOutput(
13
13
  theme: RenderTheme,
14
14
  ): string {
15
15
  const lines = output.replace(/\r\n/g, "\n").split("\n");
16
+
16
17
  while (lines.at(-1) === "") lines.pop();
17
18
  const totalLines = lines.length;
18
19
  const visibleLines = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
19
20
  let text = visibleLines.map((line) => theme.fg("toolOutput", line)).join("\n");
20
21
 
21
22
  const remaining = totalLines - visibleLines.length;
23
+
22
24
  if (remaining > 0) {
23
25
  text += `${theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
24
26
  }
27
+
25
28
  return `\n${text}`;
26
29
  }