@gavin-hu/my-pi-agent 0.8.0 → 0.8.1
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/README.md
CHANGED
|
@@ -11,7 +11,7 @@ context across sessions. `my-pi-agent` is a single [Pi](https://pi.dev) package
|
|
|
11
11
|
that adds sixteen extensions and two themes — install the pack, turn off what
|
|
12
12
|
you do not want, and keep using Pi unchanged.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+

|
|
15
15
|
|
|
16
16
|
## Contents
|
|
17
17
|
|
package/README.zh-CN.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
`my-pi-agent` 是一个 [Pi](https://pi.dev) 包,新增十六个扩展和两个主题 —— 安装
|
|
11
11
|
整个包,关掉你不需要的部分,其余照常使用 Pi。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+

|
|
14
14
|
|
|
15
15
|
## 目录
|
|
16
16
|
|
|
@@ -36,9 +36,11 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
36
36
|
tables, code, inline and reference links, and images, with a one-click
|
|
37
37
|
**raw** view (`?raw=1`). Tables size their columns to the content and scroll
|
|
38
38
|
when wide, and named and numeric character references are decoded. Fenced code
|
|
39
|
-
blocks are syntax-highlighted when the fence names a supported language.
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
blocks are syntax-highlighted when the fence names a supported language.
|
|
40
|
+
Headings get stable anchor ids, so in-document `#anchor` links and a table of
|
|
41
|
+
contents work, and a document with four or more headings gets an **On this
|
|
42
|
+
page** outline. Raw HTML in the source is escaped, never executed, and
|
|
43
|
+
external images are shown as links rather than fetched.
|
|
42
44
|
- **Sortable listings.** The Name / Size / Modified headers sort the current
|
|
43
45
|
directory, directories first, and the choice is remembered per browser.
|
|
44
46
|
- **Copy and wrap.** Copy a file's root-relative path or its contents, and
|
|
@@ -209,8 +211,12 @@ document and the project document (project wins):
|
|
|
209
211
|
the full `[text][id]`, collapsed `[text][]`, and shortcut `[text]` forms, and
|
|
210
212
|
numeric and common named character references are resolved. The Rendered/Raw
|
|
211
213
|
switch is a server-side `?raw=1`
|
|
212
|
-
link, so it works without JavaScript.
|
|
213
|
-
the
|
|
214
|
+
link, so it works without JavaScript. Headings are demoted one level so the
|
|
215
|
+
card title stays the page's only `h1`, and each gets a slug `id` (unique in
|
|
216
|
+
the document); `renderMarkdownDocument` returns the heading list and `html.ts`
|
|
217
|
+
renders an **On this page** disclosure for four or more headings. Set
|
|
218
|
+
`markdown: false` to fall back to the plain-text view. A fenced code block
|
|
219
|
+
whose info string names a supported
|
|
214
220
|
language is highlighted through `lib/highlight.ts` (either an extension or a
|
|
215
221
|
long-form alias, `fenceLanguage`); an absent or unknown language stays plain.
|
|
216
222
|
- **Syntax highlighting.** `lib/highlight.ts` is a first-party, escape-first
|
|
@@ -22,7 +22,7 @@ import { formatSize, type DirEntry, type FileView, type Listing, type TreeNode }
|
|
|
22
22
|
import type { GitStatus } from "./git.ts";
|
|
23
23
|
import { detectLanguage, highlightLines, type HighlightLanguage } from "../../lib/highlight.ts";
|
|
24
24
|
import { FOLDER_ICON, iconHref, languageOf, SPRITE, type IconSpec } from "./icons.ts";
|
|
25
|
-
import {
|
|
25
|
+
import { renderMarkdownDocument, type MarkdownHeading } from "./markdown.ts";
|
|
26
26
|
import { encodePath } from "./paths.ts";
|
|
27
27
|
|
|
28
28
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
@@ -78,6 +78,13 @@ ${WEB_COMPONENTS_CSS}
|
|
|
78
78
|
.browse-broken{color:var(--muted);text-decoration:line-through}
|
|
79
79
|
.browse-link-mark{color:var(--muted);font-size:var(--fs-xs)}
|
|
80
80
|
.browse-when{display:none}
|
|
81
|
+
.browse-md-outline{max-width:80ch;margin:0 0 var(--space-3) var(--space-4);border:1px solid var(--border);border-radius:var(--radius-md);font-size:var(--fs-sm)}
|
|
82
|
+
.browse-md-outline>summary{padding:var(--space-2) var(--space-3);cursor:pointer;color:var(--muted);font-family:var(--font-mono);font-size:var(--fs-xs)}
|
|
83
|
+
.browse-md-outline ul{list-style:none;margin:0;padding:0 var(--space-3) var(--space-2)}
|
|
84
|
+
.browse-md-outline a{display:block;padding:2px 0}
|
|
85
|
+
.browse-md-outline a.lvl1{padding-left:var(--space-3)}
|
|
86
|
+
.browse-md-outline a.lvl2{padding-left:var(--space-5)}
|
|
87
|
+
.browse-md-outline a.lvl3{padding-left:var(--space-7)}
|
|
81
88
|
body.palette-open{overflow:hidden}
|
|
82
89
|
@media (max-width:520px){
|
|
83
90
|
.bf-toolbar .bf-theme-toggle{margin-left:auto}
|
|
@@ -253,6 +260,20 @@ function renderCodeLines(lines: string[], language?: HighlightLanguage): string
|
|
|
253
260
|
return rendered.map((html) => `<span class="bf-code__line">${html.length > 0 ? html : " "}</span>`).join("");
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
/** An "on this page" outline from the headings the Markdown renderer emitted. */
|
|
264
|
+
function renderOutline(headings: MarkdownHeading[]): string {
|
|
265
|
+
if (headings.length < 4) return "";
|
|
266
|
+
const base = Math.min(...headings.map((heading) => heading.level));
|
|
267
|
+
const items = headings
|
|
268
|
+
.map((heading) => {
|
|
269
|
+
const depth = Math.min(heading.level - base, 3);
|
|
270
|
+
const label = heading.text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[`*_~]/g, "");
|
|
271
|
+
return `<li><a class="lvl${depth}" href="#${heading.id}">${escapeHtml(label)}</a></li>`;
|
|
272
|
+
})
|
|
273
|
+
.join("");
|
|
274
|
+
return `<details class="browse-md-outline"><summary>On this page</summary><ul>${items}</ul></details>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
256
277
|
/** The card-head controls: Markdown mode, line wrapping, and copy actions. */
|
|
257
278
|
function renderFileActions(page: FilePage): string {
|
|
258
279
|
const isMarkdown = page.file.kind === "markdown";
|
|
@@ -275,7 +296,7 @@ function renderFileActions(page: FilePage): string {
|
|
|
275
296
|
}
|
|
276
297
|
if (page.file.kind === "text" || isMarkdown) {
|
|
277
298
|
groups.push(
|
|
278
|
-
`<button type="button" class="bf-btn" data-copy="content" data-href="${escapeHtml(rawHref)}" title="Copy the file contents">copy</button>`,
|
|
299
|
+
`<button type="button" class="bf-btn" data-copy="content" data-href="${escapeHtml(rawHref)}" title="Copy the file contents">copy contents</button>`,
|
|
279
300
|
);
|
|
280
301
|
}
|
|
281
302
|
groups.push(
|
|
@@ -296,10 +317,12 @@ export function renderFilePage(page: FilePage): string {
|
|
|
296
317
|
let content: string;
|
|
297
318
|
let note = "";
|
|
298
319
|
if (page.file.kind === "text" || page.file.kind === "markdown") {
|
|
299
|
-
|
|
300
|
-
page.file.
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
if (page.file.kind === "markdown" && !page.markdownRaw) {
|
|
321
|
+
const rendered = renderMarkdownDocument(page.file.lines.join("\n"), markdownBaseDir(page.rel));
|
|
322
|
+
content = `${renderOutline(rendered.headings)}<article class="bf-md">${rendered.html}</article>`;
|
|
323
|
+
} else {
|
|
324
|
+
content = `<pre class="bf-code bf-code--numbered"><code>${renderCodeLines(page.file.lines, language)}</code></pre>`;
|
|
325
|
+
}
|
|
303
326
|
if (page.file.truncated)
|
|
304
327
|
note = `<p class="bf-note">Showing the first ${page.file.lines.length} of ${page.file.totalLines} lines.</p>`;
|
|
305
328
|
} else if (page.file.kind === "image") {
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* External images are rendered as links (the page CSP is `img-src 'self' data:`)
|
|
15
15
|
* and non-http(s) schemes such as `javascript:` are dropped.
|
|
16
16
|
*
|
|
17
|
+
* Headings get a stable anchor id and are demoted one level so the embedding
|
|
18
|
+
* page keeps the only `h1`; `renderMarkdownDocument` returns them for an
|
|
19
|
+
* outline.
|
|
20
|
+
*
|
|
17
21
|
* Kept free of the page renderer: it imports only `lib/escape.ts`,
|
|
18
22
|
* `lib/highlight.ts`, and `paths.ts`, so `html.ts` can compose it without a
|
|
19
23
|
* module cycle.
|
|
@@ -35,16 +39,69 @@ interface LinkDefinition {
|
|
|
35
39
|
title?: string;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
/**
|
|
42
|
+
/** A heading the renderer emitted, in document order. */
|
|
43
|
+
export interface MarkdownHeading {
|
|
44
|
+
/** The rendered tag level, already demoted and capped at `h6`. */
|
|
45
|
+
level: number;
|
|
46
|
+
/** The anchor id, unique within the document. */
|
|
47
|
+
id: string;
|
|
48
|
+
/** The heading's raw inline Markdown. */
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-document render state: the resolution base, the reference definitions,
|
|
54
|
+
* and the headings emitted so far.
|
|
55
|
+
*/
|
|
39
56
|
interface Context {
|
|
40
57
|
baseDir: string;
|
|
41
58
|
defs: Map<string, LinkDefinition>;
|
|
59
|
+
headings: MarkdownHeading[];
|
|
60
|
+
slug(text: string): string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render a Markdown source string to a safe HTML fragment, plus the headings it
|
|
65
|
+
* emitted so a caller can build an outline. Headings carry an anchor id and are
|
|
66
|
+
* demoted one level, so the embedding page keeps the only `h1`.
|
|
67
|
+
*/
|
|
68
|
+
export function renderMarkdownDocument(source: string, baseDir = ""): { html: string; headings: MarkdownHeading[] } {
|
|
69
|
+
const { defs, body } = collectDefinitions(source.split(/\r\n|\r|\n/));
|
|
70
|
+
const context = createContext(baseDir, defs);
|
|
71
|
+
return { html: renderBlocks(body, context), headings: context.headings };
|
|
42
72
|
}
|
|
43
73
|
|
|
44
74
|
/** Render a Markdown source string to a safe HTML fragment. */
|
|
45
75
|
export function renderMarkdown(source: string, baseDir = ""): string {
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
return renderMarkdownDocument(source, baseDir).html;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Turn heading text into a stable anchor id, dropping Markdown punctuation. */
|
|
80
|
+
function headingSlug(text: string): string {
|
|
81
|
+
const plain = text
|
|
82
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
83
|
+
.replace(/[`*_~]/g, "")
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
|
86
|
+
.replace(/[\s-]+/g, "-")
|
|
87
|
+
.replace(/^-+|-+$/g, "");
|
|
88
|
+
return plain || "section";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Build the per-document render state, suffixing repeated heading slugs. */
|
|
92
|
+
function createContext(baseDir: string, defs: Map<string, LinkDefinition>): Context {
|
|
93
|
+
const seen = new Map<string, number>();
|
|
94
|
+
return {
|
|
95
|
+
baseDir,
|
|
96
|
+
defs,
|
|
97
|
+
headings: [],
|
|
98
|
+
slug(text: string): string {
|
|
99
|
+
const base = headingSlug(text);
|
|
100
|
+
const count = seen.get(base) ?? 0;
|
|
101
|
+
seen.set(base, count + 1);
|
|
102
|
+
return count === 0 ? base : `${base}-${count}`;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
48
105
|
}
|
|
49
106
|
|
|
50
107
|
/** Fold a reference label the way CommonMark does: trim, collapse, case-fold. */
|
|
@@ -135,7 +192,10 @@ function renderBlocks(lines: string[], ctx: Context): string {
|
|
|
135
192
|
|
|
136
193
|
const heading = matchHeading(line);
|
|
137
194
|
if (heading) {
|
|
138
|
-
|
|
195
|
+
const level = Math.min(heading.level + 1, 6);
|
|
196
|
+
const id = ctx.slug(heading.text);
|
|
197
|
+
ctx.headings.push({ level, id, text: heading.text });
|
|
198
|
+
out.push(`<h${level} id="${id}">${renderInline(heading.text, ctx)}</h${level}>`);
|
|
139
199
|
i++;
|
|
140
200
|
continue;
|
|
141
201
|
}
|
|
@@ -403,7 +463,7 @@ function renderListItem(contentLines: string[], ctx: Context): string {
|
|
|
403
463
|
const tight = !source.some(isBlank);
|
|
404
464
|
let body = renderBlocks(source, ctx);
|
|
405
465
|
if (tight) body = body.replace(/<\/?p>/g, "");
|
|
406
|
-
const mark = task ? `<
|
|
466
|
+
const mark = task ? `<input class="md-check" type="checkbox" disabled${done ? " checked" : ""}> ` : "";
|
|
407
467
|
return `<li${task ? (done ? ` class="task done"` : ` class="task"`) : ""}>${mark}${body}</li>`;
|
|
408
468
|
}
|
|
409
469
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"themes": [
|
|
91
91
|
"./themes/*.json"
|
|
92
92
|
],
|
|
93
|
-
"image": "
|
|
93
|
+
"image": "https://raw.githubusercontent.com/gavin-hu/my-pi-agent/main/assets/bytefly-chrome.png"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"test": "bun test --parallel=4 --timeout 30000",
|