@saifymatteo/pi-extensions 0.0.0-stage → 0.1.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 +661 -0
- package/NOTICE.md +20 -0
- package/README.md +151 -2
- package/inventory.json +73 -0
- package/package.json +74 -4
- package/packages/pi-bg-bash/LICENSE +661 -0
- package/packages/pi-bg-bash/NOTICE.md +3 -0
- package/packages/pi-bg-bash/README.md +41 -0
- package/packages/pi-bg-bash/examples/settings.example.json +6 -0
- package/packages/pi-bg-bash/package.json +38 -0
- package/packages/pi-bg-bash/src/index.ts +969 -0
- package/packages/pi-bg-bash/src/lib/async-work.ts +27 -0
- package/packages/pi-bg-bash/test/bg-bash.test.mjs +34 -0
- package/packages/pi-claude-usage/LICENSE +661 -0
- package/packages/pi-claude-usage/NOTICE.md +3 -0
- package/packages/pi-claude-usage/README.md +27 -0
- package/packages/pi-claude-usage/package.json +35 -0
- package/packages/pi-claude-usage/src/index.ts +130 -0
- package/packages/pi-clipboard-image/LICENSE +661 -0
- package/packages/pi-clipboard-image/NOTICE.md +3 -0
- package/packages/pi-clipboard-image/README.md +27 -0
- package/packages/pi-clipboard-image/package.json +36 -0
- package/packages/pi-clipboard-image/src/index.ts +527 -0
- package/packages/pi-codeblock-borders/LICENSE +661 -0
- package/packages/pi-codeblock-borders/NOTICE.md +3 -0
- package/packages/pi-codeblock-borders/README.md +30 -0
- package/packages/pi-codeblock-borders/package.json +35 -0
- package/packages/pi-codeblock-borders/src/index.ts +302 -0
- package/packages/pi-codegraph/LICENSE +661 -0
- package/packages/pi-codegraph/NOTICE.md +5 -0
- package/packages/pi-codegraph/README.md +35 -0
- package/packages/pi-codegraph/examples/codegraph.example.json +4 -0
- package/packages/pi-codegraph/package.json +37 -0
- package/packages/pi-codegraph/src/index.ts +573 -0
- package/packages/pi-codegraph/test/codegraph.test.mjs +21 -0
- package/packages/pi-compact-tools/LICENSE +661 -0
- package/packages/pi-compact-tools/NOTICE.md +3 -0
- package/packages/pi-compact-tools/README.md +27 -0
- package/packages/pi-compact-tools/package.json +36 -0
- package/packages/pi-compact-tools/src/index.ts +1161 -0
- package/packages/pi-decisions/LICENSE +661 -0
- package/packages/pi-decisions/NOTICE.md +3 -0
- package/packages/pi-decisions/README.md +37 -0
- package/packages/pi-decisions/examples/service-models.example.json +27 -0
- package/packages/pi-decisions/package.json +37 -0
- package/packages/pi-decisions/src/index.ts +364 -0
- package/packages/pi-decisions/src/lib/service-models.ts +95 -0
- package/packages/pi-decisions/test/service-models.test.mjs +71 -0
- package/packages/pi-load-pi-dotenv/LICENSE +661 -0
- package/packages/pi-load-pi-dotenv/NOTICE.md +3 -0
- package/packages/pi-load-pi-dotenv/README.md +27 -0
- package/packages/pi-load-pi-dotenv/package.json +32 -0
- package/packages/pi-load-pi-dotenv/src/index.ts +66 -0
- package/packages/pi-model-go/LICENSE +661 -0
- package/packages/pi-model-go/NOTICE.md +5 -0
- package/packages/pi-model-go/README.md +33 -0
- package/packages/pi-model-go/package.json +36 -0
- package/packages/pi-model-go/src/index.ts +171 -0
- package/packages/pi-model-go/src/lib/dialog.ts +31 -0
- package/packages/pi-model-go/src/lib/model-go.ts +239 -0
- package/packages/pi-model-go/test/behavior.test.mjs +17 -0
- package/packages/pi-prestart-working/LICENSE +661 -0
- package/packages/pi-prestart-working/NOTICE.md +3 -0
- package/packages/pi-prestart-working/README.md +25 -0
- package/packages/pi-prestart-working/package.json +35 -0
- package/packages/pi-prestart-working/src/index.ts +86 -0
- package/packages/pi-prestart-working/test/behavior.test.mjs +56 -0
- package/packages/pi-root-guard/LICENSE +661 -0
- package/packages/pi-root-guard/NOTICE.md +3 -0
- package/packages/pi-root-guard/README.md +27 -0
- package/packages/pi-root-guard/package.json +32 -0
- package/packages/pi-root-guard/src/index.ts +202 -0
- package/packages/pi-session-finder/LICENSE +661 -0
- package/packages/pi-session-finder/NOTICE.md +3 -0
- package/packages/pi-session-finder/README.md +33 -0
- package/packages/pi-session-finder/package.json +36 -0
- package/packages/pi-session-finder/src/index.ts +306 -0
- package/packages/pi-session-finder/src/lib/dialog.ts +31 -0
- package/packages/pi-session-finder/test/behavior.test.mjs +18 -0
- package/packages/pi-session-id/LICENSE +661 -0
- package/packages/pi-session-id/NOTICE.md +3 -0
- package/packages/pi-session-id/README.md +31 -0
- package/packages/pi-session-id/package.json +35 -0
- package/packages/pi-session-id/src/index.ts +20 -0
- package/packages/pi-session-id/test/behavior.test.mjs +25 -0
- package/packages/pi-session-nav/LICENSE +661 -0
- package/packages/pi-session-nav/NOTICE.md +3 -0
- package/packages/pi-session-nav/README.md +32 -0
- package/packages/pi-session-nav/package.json +36 -0
- package/packages/pi-session-nav/src/index.ts +203 -0
- package/packages/pi-session-nav/src/lib/dialog.ts +31 -0
- package/packages/pi-session-nav/test/behavior.test.mjs +36 -0
- package/packages/pi-session-rename/LICENSE +661 -0
- package/packages/pi-session-rename/NOTICE.md +3 -0
- package/packages/pi-session-rename/README.md +31 -0
- package/packages/pi-session-rename/package.json +35 -0
- package/packages/pi-session-rename/src/index.ts +456 -0
- package/packages/pi-session-rename/test/behavior.test.mjs +10 -0
- package/packages/pi-session-retention/LICENSE +661 -0
- package/packages/pi-session-retention/NOTICE.md +3 -0
- package/packages/pi-session-retention/README.md +62 -0
- package/packages/pi-session-retention/package.json +36 -0
- package/packages/pi-session-retention/src/README.md +42 -0
- package/packages/pi-session-retention/src/core.ts +333 -0
- package/packages/pi-session-retention/src/dialog.ts +118 -0
- package/packages/pi-session-retention/src/index.ts +311 -0
- package/packages/pi-session-retention/src/lib/dialog.ts +31 -0
- package/packages/pi-session-retention/src/safety.ts +50 -0
- package/packages/pi-session-retention/test/behavior.test.mjs +42 -0
- package/packages/pi-skill-usage-log/LICENSE +661 -0
- package/packages/pi-skill-usage-log/NOTICE.md +3 -0
- package/packages/pi-skill-usage-log/README.md +32 -0
- package/packages/pi-skill-usage-log/package.json +36 -0
- package/packages/pi-skill-usage-log/src/index.ts +316 -0
- package/packages/pi-skill-usage-log/src/lib/dialog.ts +31 -0
- package/packages/pi-skill-usage-log/src/lib/usage-log.ts +62 -0
- package/packages/pi-skill-usage-log/test/behavior.test.mjs +34 -0
- package/packages/pi-tool-usage-log/LICENSE +661 -0
- package/packages/pi-tool-usage-log/NOTICE.md +5 -0
- package/packages/pi-tool-usage-log/README.md +32 -0
- package/packages/pi-tool-usage-log/package.json +36 -0
- package/packages/pi-tool-usage-log/src/index.ts +233 -0
- package/packages/pi-tool-usage-log/src/lib/dialog.ts +31 -0
- package/packages/pi-tool-usage-log/src/lib/usage-log.ts +62 -0
- package/packages/pi-tool-usage-log/src/skill-usage-log.ts +316 -0
- package/packages/pi-tool-usage-log/test/behavior.test.mjs +23 -0
- package/packages/pi-tts/LICENSE +661 -0
- package/packages/pi-tts/NOTICE.md +3 -0
- package/packages/pi-tts/README.md +37 -0
- package/packages/pi-tts/examples/service-models.example.json +27 -0
- package/packages/pi-tts/package.json +37 -0
- package/packages/pi-tts/src/index.ts +135 -0
- package/packages/pi-tts/src/lib/service-models.ts +95 -0
- package/packages/pi-tts/test/tts-output.test.mjs +47 -0
- package/packages/pi-tts/test/tts.test.mjs +57 -0
- package/packages/pi-user-image-previews/LICENSE +661 -0
- package/packages/pi-user-image-previews/NOTICE.md +3 -0
- package/packages/pi-user-image-previews/README.md +27 -0
- package/packages/pi-user-image-previews/package.json +36 -0
- package/packages/pi-user-image-previews/src/index.ts +168 -0
- package/packages/pi-vision/LICENSE +661 -0
- package/packages/pi-vision/NOTICE.md +5 -0
- package/packages/pi-vision/README.md +43 -0
- package/packages/pi-vision/examples/vision-toolkit.example.json +12 -0
- package/packages/pi-vision/package.json +38 -0
- package/packages/pi-vision/skills/vision-tools/LICENSE +21 -0
- package/packages/pi-vision/skills/vision-tools/SKILL.md +134 -0
- package/packages/pi-vision/skills/vision-tools/agents/openai.yaml +4 -0
- package/packages/pi-vision/skills/vision-tools/bin/crop +94 -0
- package/packages/pi-vision/skills/vision-tools/bin/detect +18 -0
- package/packages/pi-vision/skills/vision-tools/bin/glance +98 -0
- package/packages/pi-vision/skills/vision-tools/bin/ground +18 -0
- package/packages/pi-vision/skills/vision-tools/bin/trace +134 -0
- package/packages/pi-vision/skills/vision-tools/bin/vision-doctor +168 -0
- package/packages/pi-vision/skills/vision-tools/detect.py +56 -0
- package/packages/pi-vision/skills/vision-tools/ground.py +216 -0
- package/packages/pi-vision/skills/vision-tools/references/gui.md +93 -0
- package/packages/pi-vision/skills/vision-tools/references/long-screenshot-ocr.md +103 -0
- package/packages/pi-vision/skills/vision-tools/references/restore-graphic.md +92 -0
- package/packages/pi-vision/skills/vision-tools/references/restore-structure.md +47 -0
- package/packages/pi-vision/skills/vision-tools/references/restore-ui.md +200 -0
- package/packages/pi-vision/skills/vision-tools/scripts/dominant_colors.py +224 -0
- package/packages/pi-vision/skills/vision-tools/scripts/extract_fg.py +278 -0
- package/packages/pi-vision/skills/vision-tools/scripts/html_shot.py +111 -0
- package/packages/pi-vision/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
- package/packages/pi-vision/skills/vision-tools/scripts/pixel_diff.py +88 -0
- package/packages/pi-vision/skills/vision-tools/vision_client.py +581 -0
- package/packages/pi-vision/src/index.ts +728 -0
- package/packages/pi-vision/test/vision.test.mjs +30 -0
- package/packages/pi-wait/LICENSE +661 -0
- package/packages/pi-wait/NOTICE.md +3 -0
- package/packages/pi-wait/README.md +37 -0
- package/packages/pi-wait/package.json +37 -0
- package/packages/pi-wait/src/index.ts +88 -0
- package/packages/pi-wait/src/lib/async-work.ts +27 -0
- package/packages/pi-wait/test/wait.test.mjs +43 -0
- package/packages/pi-zvec/LICENSE +661 -0
- package/packages/pi-zvec/NOTICE.md +3 -0
- package/packages/pi-zvec/README.md +37 -0
- package/packages/pi-zvec/examples/zvec.example.json +5 -0
- package/packages/pi-zvec/package.json +37 -0
- package/packages/pi-zvec/src/index.ts +754 -0
- package/packages/pi-zvec/test/zvec.test.mjs +19 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pi-codeblock-borders
|
|
2
|
+
|
|
3
|
+
Render markdown code fences as full-width rules spanning the content area, and upgrade the fence info string with language decoration, titles, and line numbers that Pi's built-in renderer does not support.
|
|
4
|
+
|
|
5
|
+
## Running locally
|
|
6
|
+
|
|
7
|
+
From the monorepo root:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi -e ./packages/pi-codeblock-borders
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Warning:** this extension uses your existing Pi resources and settings unless you load it in an isolated home (see the root README). Do not load two copies of the same extension (for example an older standalone file alongside this package): commands and hooks can duplicate. The package never installs or relocates personal settings.
|
|
14
|
+
|
|
15
|
+
## Behavior
|
|
16
|
+
|
|
17
|
+
- Replaces the literal ``` fence markers with horizontal rules sized to the terminal width.
|
|
18
|
+
- Decorates the opening info string: `lang`, `lang` separators, optional `title`, and `:line-numbers`.
|
|
19
|
+
- Patches the theme's `codeBlockBorder`/`highlightCode` callbacks for the session and restores the originals on `session_shutdown`.
|
|
20
|
+
- Applies to every markdown code block; there is no per-block opt-out.
|
|
21
|
+
|
|
22
|
+
## Settings and storage
|
|
23
|
+
|
|
24
|
+
None. The extension holds no configuration file and reads no user data beyond the rendered token stream.
|
|
25
|
+
|
|
26
|
+
## Compatibility
|
|
27
|
+
|
|
28
|
+
Node.js >=24 for development checks; run extensions through Pi's TypeScript loader. Host imports are wildcard peers, not bundled runtime copies. The patch reaches into Pi-TUI `Markdown` internals, so host-version changes can break it; verify against your target host before relying on it. Overlapping UI patches (for example from pi-atelier) need integration testing; load one renderer patch at a time in an isolated session first.
|
|
29
|
+
|
|
30
|
+
See [NOTICE.md](NOTICE.md) for provenance. Tests patch a stubbed renderer and never contact services or write to user-home resources.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saifymatteo/pi-codeblock-borders",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
5
|
+
"description": "Decorated code fences",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24"
|
|
8
|
+
},
|
|
9
|
+
"pi": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./src/index.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"NOTICE.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
22
|
+
"@earendil-works/pi-tui": "*"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"pi-package"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/saifymatteo/pi-extensions.git",
|
|
33
|
+
"directory": "packages/pi-codeblock-borders"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codeblock borders for pi.
|
|
3
|
+
*
|
|
4
|
+
* Renders markdown code fences as full-width rules spanning the content area
|
|
5
|
+
* instead of the literal ``` markers, and upgrades the fence info string with
|
|
6
|
+
* features pi's built-in renderer does not support:
|
|
7
|
+
*
|
|
8
|
+
* bash── deploy.sh ──────────────────── ```bash title="deploy.sh"
|
|
9
|
+
* #!/usr/bin/env bash (title embedded in top rule)
|
|
10
|
+
* ────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* ts────────────────────────────────── ```ts:line-numbers
|
|
13
|
+
* 1 │ const a = 1; (line numbers)
|
|
14
|
+
* ────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* bash──────────────────────────────── ```bash {2,4-6}
|
|
17
|
+
* const a = 1; (lines 2, 4-6 highlighted;
|
|
18
|
+
* ... the rest dimmed)
|
|
19
|
+
*
|
|
20
|
+
* Supported info-string syntax (all combinable, any order):
|
|
21
|
+
* lang - first word: bash, ts, diff, ...
|
|
22
|
+
* :line-numbers - prefix each code line with a dim line number;
|
|
23
|
+
* optional start offset: :line-numbers=5
|
|
24
|
+
* {2,4-6} - keep those lines at full color, dim the rest
|
|
25
|
+
* title="..." - embed a title in the top rule (quotes optional for
|
|
26
|
+
* single words; also title='...' and title=word)
|
|
27
|
+
*
|
|
28
|
+
* Why an info string like "bash {2,4-6}" degrades rendering without this
|
|
29
|
+
* extension: marked keeps the FULL info string in token.lang, pi passes it to
|
|
30
|
+
* highlight.js as the language name, which then fails to match and falls back
|
|
31
|
+
* to unhighlighted code. This extension parses the info string itself and
|
|
32
|
+
* feeds highlight.js only the leading language word, restoring highlighting.
|
|
33
|
+
*
|
|
34
|
+
* `diff` fences are natively supported by pi (+/- lines are colored via the
|
|
35
|
+
* toolDiffAdded / toolDiffRemoved theme colors) and are left untouched.
|
|
36
|
+
*
|
|
37
|
+
* Implementation: wraps Markdown.prototype.renderToken from the bundled
|
|
38
|
+
* @earendil-works/pi-tui, swapping theme.codeBlockBorder and
|
|
39
|
+
* theme.highlightCode for the duration of each call. Widths come from the
|
|
40
|
+
* actual render call, so rules and gutters size correctly in nested contexts
|
|
41
|
+
* (blockquotes, list items). Caching, streaming, and theme colors are
|
|
42
|
+
* untouched. Display-only: session content and LLM context keep the original
|
|
43
|
+
* fences.
|
|
44
|
+
*
|
|
45
|
+
* Remove this file to restore the previous rendering, then run /reload or
|
|
46
|
+
* restart pi.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
50
|
+
import { Markdown, visibleWidth } from "@earendil-works/pi-tui";
|
|
51
|
+
|
|
52
|
+
/** Character used for the rule lines. Use "-" for plain ASCII dashes. */
|
|
53
|
+
const BORDER_CHAR = "─";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set to false for plain rules on fenced blocks too. When true, fenced blocks
|
|
57
|
+
* with a language show it as a prefix of the top rule, e.g. `bash──────`.
|
|
58
|
+
*/
|
|
59
|
+
const SHOW_LANGUAGE = true;
|
|
60
|
+
|
|
61
|
+
/** Separator between the line number and the code in the gutter. */
|
|
62
|
+
const GUTTER_SEP = " │ ";
|
|
63
|
+
|
|
64
|
+
/** ANSI dim (SGR 2) used for gutter numbers and non-focused lines. */
|
|
65
|
+
const DIM = "\x1b[2m";
|
|
66
|
+
const DIM_OFF = "\x1b[22m";
|
|
67
|
+
|
|
68
|
+
interface FenceInfo {
|
|
69
|
+
/** Leading language word, e.g. "bash" from "bash:line-numbers {1}". */
|
|
70
|
+
lang: string;
|
|
71
|
+
/** Title from title="..." / title='...' / title=word, if present. */
|
|
72
|
+
title?: string;
|
|
73
|
+
/** :line-numbers flag present. */
|
|
74
|
+
lineNumbers: boolean;
|
|
75
|
+
/** Start value for line numbers (:line-numbers=5). */
|
|
76
|
+
lineNumbersStart: number;
|
|
77
|
+
/** Focus specification, resolved only after the highlighted line count is known. */
|
|
78
|
+
focus?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stripAnsi(text: string): string {
|
|
82
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dimText(text: string): string {
|
|
86
|
+
// Re-assert dim after every ANSI reset so syntax colors inside the line do
|
|
87
|
+
// not cancel it; finish with DIM_OFF so nothing leaks past the line.
|
|
88
|
+
return DIM + text.replaceAll("\x1b[0m", `\x1b[0m${DIM}`) + DIM_OFF;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseFocus(spec: string, lineCount: number): Set<number> | undefined {
|
|
92
|
+
const set = new Set<number>();
|
|
93
|
+
let valid = false;
|
|
94
|
+
for (const part of spec.split(",")) {
|
|
95
|
+
const seg = part.trim();
|
|
96
|
+
if (!seg) continue;
|
|
97
|
+
const range = /^([0-9]+)\s*-\s*([0-9]+)$/.exec(seg);
|
|
98
|
+
if (range) {
|
|
99
|
+
const a = Number(range[1]);
|
|
100
|
+
const b = Number(range[2]);
|
|
101
|
+
if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b)) continue;
|
|
102
|
+
valid = true;
|
|
103
|
+
// Never enumerate outside the rendered block, even for reversed ranges.
|
|
104
|
+
const end = Math.min(lineCount, Math.max(a, b));
|
|
105
|
+
for (let n = Math.max(1, Math.min(a, b)); n <= end; n++) set.add(n);
|
|
106
|
+
} else if (/^[0-9]+$/.test(seg)) {
|
|
107
|
+
const n = Number(seg);
|
|
108
|
+
if (!Number.isSafeInteger(n)) continue;
|
|
109
|
+
valid = true;
|
|
110
|
+
if (n >= 1 && n <= lineCount) set.add(n);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// A valid but out-of-bounds selection still dims every line, as before.
|
|
114
|
+
return valid ? set : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseFenceInfo(info: string): FenceInfo {
|
|
118
|
+
const parsed: FenceInfo = { lang: "", lineNumbers: false, lineNumbersStart: 1 };
|
|
119
|
+
let rest = (info ?? "").trim();
|
|
120
|
+
if (!rest) return parsed;
|
|
121
|
+
|
|
122
|
+
// title="..." | title='...' | title=word (extract first so flag searches
|
|
123
|
+
// below cannot match inside the title text)
|
|
124
|
+
const titleMatch = /(?:^|\s)title=(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(rest);
|
|
125
|
+
if (titleMatch) {
|
|
126
|
+
const value = (titleMatch[1] ?? titleMatch[2] ?? titleMatch[3] ?? "").trim();
|
|
127
|
+
if (value) parsed.title = value;
|
|
128
|
+
rest = (rest.slice(0, titleMatch.index) + " " + rest.slice(titleMatch.index + titleMatch[0].length)).trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// {2,4-6} line focus
|
|
132
|
+
const braces = /\{\s*([0-9][0-9\s,-]*)\}/.exec(rest);
|
|
133
|
+
if (braces) {
|
|
134
|
+
parsed.focus = braces[1];
|
|
135
|
+
rest = (rest.slice(0, braces.index) + " " + rest.slice(braces.index + braces[0].length)).trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// :line-numbers with optional start offset. Matches when the flag attaches
|
|
139
|
+
// directly to the language ("bash:line-numbers") or stands alone.
|
|
140
|
+
const lineNums = /:(?:line-numbers)(?:=([0-9]+))?(?![\w-])/.exec(rest);
|
|
141
|
+
if (lineNums) {
|
|
142
|
+
parsed.lineNumbers = true;
|
|
143
|
+
if (lineNums[1]) parsed.lineNumbersStart = Math.max(1, Number(lineNums[1]));
|
|
144
|
+
rest = (rest.slice(0, lineNums.index) + " " + rest.slice(lineNums.index + lineNums[0].length)).trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Language: first word of what remains (stops at space, ":" or "{").
|
|
148
|
+
const lang = /^[^ \t:{}]+/.exec(rest);
|
|
149
|
+
if (lang) parsed.lang = lang[0];
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Top rule: language prefix, optional embedded title, dashes to full width. */
|
|
154
|
+
function composeTopRule(width: number, parsed: FenceInfo): string {
|
|
155
|
+
const w = Math.max(1, Math.floor(Number.isFinite(width) ? width : 1));
|
|
156
|
+
|
|
157
|
+
const lang = /[\r\n]/.test(parsed.lang) ? "" : parsed.lang;
|
|
158
|
+
if (parsed.title && !/[\r\n]/.test(parsed.title)) {
|
|
159
|
+
const label = SHOW_LANGUAGE && lang ? `${lang}${BORDER_CHAR.repeat(2)} ` : `${BORDER_CHAR.repeat(2)} `;
|
|
160
|
+
const candidate = `${label}${parsed.title} `;
|
|
161
|
+
const candidateWidth = visibleWidth(candidate);
|
|
162
|
+
if (candidateWidth + 1 <= w) {
|
|
163
|
+
return candidate + BORDER_CHAR.repeat(w - candidateWidth);
|
|
164
|
+
}
|
|
165
|
+
// Title too long for the rule; fall through to language-only rule.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const langWidth = visibleWidth(lang);
|
|
169
|
+
if (SHOW_LANGUAGE && lang && langWidth < w) {
|
|
170
|
+
return lang + BORDER_CHAR.repeat(w - langWidth);
|
|
171
|
+
}
|
|
172
|
+
return BORDER_CHAR.repeat(w);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface HighlightCodeFn {
|
|
176
|
+
(code: string, lang: string): string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ThemeLike {
|
|
180
|
+
codeBlockBorder?: (text: string) => string;
|
|
181
|
+
highlightCode?: HighlightCodeFn;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface MarkdownLike {
|
|
185
|
+
theme?: ThemeLike;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type RenderTokenFn = (
|
|
189
|
+
this: MarkdownLike,
|
|
190
|
+
token: unknown,
|
|
191
|
+
width: number,
|
|
192
|
+
nextTokenType?: string,
|
|
193
|
+
styleContext?: unknown,
|
|
194
|
+
) => string[];
|
|
195
|
+
|
|
196
|
+
/** Apply :line-numbers and {2,4-6} to highlighted code lines. */
|
|
197
|
+
function decorateLines(lines: string[], parsed: FenceInfo): string[] {
|
|
198
|
+
let out = lines;
|
|
199
|
+
|
|
200
|
+
if (parsed.lineNumbers && out.length > 0) {
|
|
201
|
+
// Drop one trailing empty line so a numbered blank line does not sit
|
|
202
|
+
// against the bottom rule.
|
|
203
|
+
const last = out[out.length - 1];
|
|
204
|
+
if (out.length > 1 && stripAnsi(last).trim() === "") {
|
|
205
|
+
out = out.slice(0, -1);
|
|
206
|
+
}
|
|
207
|
+
const start = parsed.lineNumbersStart;
|
|
208
|
+
const gutterWidth = String(start + out.length - 1).length;
|
|
209
|
+
out = out.map((line, i) => {
|
|
210
|
+
const n = String(start + i).padStart(gutterWidth, " ");
|
|
211
|
+
return dimText(`${n}${GUTTER_SEP}`) + line;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const focus = parsed.focus ? parseFocus(parsed.focus, out.length) : undefined;
|
|
216
|
+
if (focus) {
|
|
217
|
+
out = out.map((line, i) => (focus.has(i + 1) ? line : dimText(line)));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default function (pi: ExtensionAPI) {
|
|
224
|
+
const proto = Markdown.prototype as unknown as {
|
|
225
|
+
renderToken?: RenderTokenFn;
|
|
226
|
+
__codeblockBordersPatched?: boolean | RenderTokenFn;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Legacy boolean markers cannot be safely unwrapped: restart once to migrate.
|
|
230
|
+
// New markers identify the owner so stale shutdowns cannot remove a reload.
|
|
231
|
+
if (proto.__codeblockBordersPatched) return;
|
|
232
|
+
|
|
233
|
+
const originalRenderToken = proto.renderToken;
|
|
234
|
+
if (typeof originalRenderToken !== "function") {
|
|
235
|
+
// pi-tui internals changed; stay inert instead of breaking rendering.
|
|
236
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
237
|
+
ctx.ui.notify(
|
|
238
|
+
"codeblock-borders: Markdown.renderToken not found, extension inactive",
|
|
239
|
+
"warning",
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let active = true;
|
|
246
|
+
const renderToken: RenderTokenFn = function (token, width, nextTokenType, styleContext) {
|
|
247
|
+
// A later wrapper may retain us in its chain after shutdown.
|
|
248
|
+
if (!active) return originalRenderToken.call(this, token, width, nextTokenType, styleContext);
|
|
249
|
+
const theme = this.theme;
|
|
250
|
+
const originalBorder = theme?.codeBlockBorder;
|
|
251
|
+
const originalHighlight = theme?.highlightCode;
|
|
252
|
+
if (!theme || typeof originalBorder !== "function") {
|
|
253
|
+
return originalRenderToken.call(this, token, width, nextTokenType, styleContext);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// codeBlockBorder is called once for the opening fence ("```info") and
|
|
257
|
+
// once for the closing fence ("```"). Nested renders (fenced code inside a
|
|
258
|
+
// list/blockquote) swap this wrapper again while the outer swap is still
|
|
259
|
+
// active, so the inner wrapper's composed rule can arrive here as "text"
|
|
260
|
+
// through the outer wrapper's originalBorder — it does not start with
|
|
261
|
+
// "```", so pass it through unchanged instead of re-composing it at the
|
|
262
|
+
// outer (larger) width, which overflowed the nested content width.
|
|
263
|
+
theme.codeBlockBorder = (text: string) => {
|
|
264
|
+
if (!text.startsWith("```")) {
|
|
265
|
+
return originalBorder(text);
|
|
266
|
+
}
|
|
267
|
+
return originalBorder(composeTopRule(width, parseFenceInfo(text.slice(3))));
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (typeof originalHighlight === "function") {
|
|
271
|
+
// The renderer passes the full info string as the language. Feed
|
|
272
|
+
// highlight.js only the leading language word so decorated fences
|
|
273
|
+
// ("bash:line-numbers {2}" etc.) keep syntax highlighting, then
|
|
274
|
+
// apply the gutter/focus decorations.
|
|
275
|
+
theme.highlightCode = (code: string, infoLang: string) => {
|
|
276
|
+
const parsed = parseFenceInfo(infoLang ?? "");
|
|
277
|
+
const lines = originalHighlight(code, parsed.lang || infoLang || "");
|
|
278
|
+
return decorateLines(lines, parsed);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
return originalRenderToken.call(this, token, width, nextTokenType, styleContext);
|
|
284
|
+
} finally {
|
|
285
|
+
theme.codeBlockBorder = originalBorder;
|
|
286
|
+
if (theme && typeof originalHighlight === "function") {
|
|
287
|
+
theme.highlightCode = originalHighlight;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
proto.renderToken = renderToken;
|
|
293
|
+
proto.__codeblockBordersPatched = renderToken;
|
|
294
|
+
pi.on("session_shutdown", () => {
|
|
295
|
+
if (!active) return;
|
|
296
|
+
active = false;
|
|
297
|
+
// Do not overwrite another extension's wrapper. Our retained wrapper is
|
|
298
|
+
// now pass-through, and a replacement can safely wrap the current chain.
|
|
299
|
+
if (proto.renderToken === renderToken) proto.renderToken = originalRenderToken;
|
|
300
|
+
if (proto.__codeblockBordersPatched === renderToken) delete proto.__codeblockBordersPatched;
|
|
301
|
+
});
|
|
302
|
+
}
|