coding-friend-cli 1.6.0 → 1.7.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/dist/{chunk-ESIIWKPD.js → chunk-DHPWBSF5.js} +1 -1
- package/dist/{chunk-FWHEMJS3.js → chunk-HSQX3PKW.js} +1 -1
- package/dist/{chunk-JWAJ4XPK.js → chunk-WXBI2HUL.js} +1 -0
- package/dist/{dev-EWSTIVM7.js → dev-QW6VPG4G.js} +2 -2
- package/dist/{host-SQEDE3NN.js → host-EERZVOHY.js} +1 -1
- package/dist/index.js +12 -12
- package/dist/{init-HX5T5DBV.js → init-5BJVESH7.js} +102 -41
- package/dist/{install-RZFSIPFD.js → install-ORIDNWRW.js} +3 -3
- package/dist/{mcp-QRPBL4ML.js → mcp-3MUUQZQD.js} +1 -1
- package/dist/{statusline-WGPSURDC.js → statusline-BWGI5PQ5.js} +2 -2
- package/dist/{update-VAFEWOLA.js → update-GW37S23M.js} +3 -3
- package/lib/learn-host/CHANGELOG.md +6 -0
- package/lib/learn-host/package.json +1 -1
- package/lib/learn-host/src/app/[category]/[slug]/page.tsx +3 -1
- package/lib/learn-host/src/app/globals.css +20 -0
- package/lib/learn-host/src/components/TableOfContents.tsx +15 -1
- package/lib/learn-host/src/lib/docs.ts +2 -1
- package/package.json +1 -1
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
} from "./chunk-MRTR7TJ4.js";
|
|
5
5
|
import {
|
|
6
6
|
ensureStatusline
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HSQX3PKW.js";
|
|
8
8
|
import {
|
|
9
9
|
ensureShellCompletion
|
|
10
10
|
} from "./chunk-4PLV2ENL.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-WXBI2HUL.js";
|
|
12
12
|
import {
|
|
13
13
|
commandExists,
|
|
14
14
|
run
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ program.name("cf").description(
|
|
|
14
14
|
"coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
|
|
15
15
|
).version(pkg.version, "-v, --version");
|
|
16
16
|
program.command("install").description("Install the Coding Friend plugin into Claude Code").action(async () => {
|
|
17
|
-
const { installCommand } = await import("./install-
|
|
17
|
+
const { installCommand } = await import("./install-ORIDNWRW.js");
|
|
18
18
|
await installCommand();
|
|
19
19
|
});
|
|
20
20
|
program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").action(async () => {
|
|
@@ -22,23 +22,23 @@ program.command("uninstall").description("Uninstall the Coding Friend plugin fro
|
|
|
22
22
|
await uninstallCommand();
|
|
23
23
|
});
|
|
24
24
|
program.command("init").description("Initialize coding-friend in current project").action(async () => {
|
|
25
|
-
const { initCommand } = await import("./init-
|
|
25
|
+
const { initCommand } = await import("./init-5BJVESH7.js");
|
|
26
26
|
await initCommand();
|
|
27
27
|
});
|
|
28
28
|
program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
|
|
29
|
-
const { hostCommand } = await import("./host-
|
|
29
|
+
const { hostCommand } = await import("./host-EERZVOHY.js");
|
|
30
30
|
await hostCommand(path, opts);
|
|
31
31
|
});
|
|
32
32
|
program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
|
|
33
|
-
const { mcpCommand } = await import("./mcp-
|
|
33
|
+
const { mcpCommand } = await import("./mcp-3MUUQZQD.js");
|
|
34
34
|
await mcpCommand(path);
|
|
35
35
|
});
|
|
36
36
|
program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
|
|
37
|
-
const { statuslineCommand } = await import("./statusline-
|
|
37
|
+
const { statuslineCommand } = await import("./statusline-BWGI5PQ5.js");
|
|
38
38
|
await statuslineCommand();
|
|
39
39
|
});
|
|
40
40
|
program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").action(async (opts) => {
|
|
41
|
-
const { updateCommand } = await import("./update-
|
|
41
|
+
const { updateCommand } = await import("./update-GW37S23M.js");
|
|
42
42
|
await updateCommand(opts);
|
|
43
43
|
});
|
|
44
44
|
var dev = program.command("dev").description("Development mode commands");
|
|
@@ -54,35 +54,35 @@ Dev subcommands:
|
|
|
54
54
|
dev update [path] Update local dev plugin to latest version`
|
|
55
55
|
);
|
|
56
56
|
dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
|
|
57
|
-
const { devOnCommand } = await import("./dev-
|
|
57
|
+
const { devOnCommand } = await import("./dev-QW6VPG4G.js");
|
|
58
58
|
await devOnCommand(path);
|
|
59
59
|
});
|
|
60
60
|
dev.command("off").description("Switch back to remote marketplace").action(async () => {
|
|
61
|
-
const { devOffCommand } = await import("./dev-
|
|
61
|
+
const { devOffCommand } = await import("./dev-QW6VPG4G.js");
|
|
62
62
|
await devOffCommand();
|
|
63
63
|
});
|
|
64
64
|
dev.command("status").description("Show current dev mode").action(async () => {
|
|
65
|
-
const { devStatusCommand } = await import("./dev-
|
|
65
|
+
const { devStatusCommand } = await import("./dev-QW6VPG4G.js");
|
|
66
66
|
await devStatusCommand();
|
|
67
67
|
});
|
|
68
68
|
dev.command("sync").description(
|
|
69
69
|
"Copy local source files to plugin cache (no version bump needed)"
|
|
70
70
|
).action(async () => {
|
|
71
|
-
const { devSyncCommand } = await import("./dev-
|
|
71
|
+
const { devSyncCommand } = await import("./dev-QW6VPG4G.js");
|
|
72
72
|
await devSyncCommand();
|
|
73
73
|
});
|
|
74
74
|
dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
|
|
75
75
|
"[path]",
|
|
76
76
|
"path to local coding-friend repo (default: saved path or cwd)"
|
|
77
77
|
).action(async (path) => {
|
|
78
|
-
const { devRestartCommand } = await import("./dev-
|
|
78
|
+
const { devRestartCommand } = await import("./dev-QW6VPG4G.js");
|
|
79
79
|
await devRestartCommand(path);
|
|
80
80
|
});
|
|
81
81
|
dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
|
|
82
82
|
"[path]",
|
|
83
83
|
"path to local coding-friend repo (default: saved path or cwd)"
|
|
84
84
|
).action(async (path) => {
|
|
85
|
-
const { devUpdateCommand } = await import("./dev-
|
|
85
|
+
const { devUpdateCommand } = await import("./dev-QW6VPG4G.js");
|
|
86
86
|
await devUpdateCommand(path);
|
|
87
87
|
});
|
|
88
88
|
program.parse();
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
saveStatuslineConfig,
|
|
5
5
|
selectStatuslineComponents,
|
|
6
6
|
writeStatuslineSettings
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HSQX3PKW.js";
|
|
8
8
|
import {
|
|
9
9
|
ensureShellCompletion,
|
|
10
10
|
hasShellCompletion
|
|
11
11
|
} from "./chunk-4PLV2ENL.js";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_CONFIG
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-WXBI2HUL.js";
|
|
15
15
|
import {
|
|
16
16
|
run
|
|
17
17
|
} from "./chunk-UFGNO6CW.js";
|
|
@@ -31,8 +31,10 @@ import {
|
|
|
31
31
|
|
|
32
32
|
// src/commands/init.ts
|
|
33
33
|
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
34
|
-
import {
|
|
34
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
35
35
|
import { homedir } from "os";
|
|
36
|
+
var GITIGNORE_START = "# >>> coding-friend managed";
|
|
37
|
+
var GITIGNORE_END = "# <<< coding-friend managed";
|
|
36
38
|
function isGitRepo() {
|
|
37
39
|
return run("git", ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
38
40
|
}
|
|
@@ -42,13 +44,29 @@ function checkDocsFolders() {
|
|
|
42
44
|
}
|
|
43
45
|
function checkGitignore() {
|
|
44
46
|
if (!existsSync(".gitignore")) return false;
|
|
45
|
-
|
|
47
|
+
const content = readFileSync(".gitignore", "utf-8");
|
|
48
|
+
return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
|
|
46
49
|
}
|
|
47
|
-
function
|
|
50
|
+
function checkDocsLanguage() {
|
|
48
51
|
const local = readJson(localConfigPath());
|
|
49
52
|
const global = readJson(globalConfigPath());
|
|
50
53
|
return !!(local?.language || global?.language);
|
|
51
54
|
}
|
|
55
|
+
async function selectLanguage(message) {
|
|
56
|
+
const choice = await select({
|
|
57
|
+
message,
|
|
58
|
+
choices: [
|
|
59
|
+
{ name: "English", value: "en" },
|
|
60
|
+
{ name: "Vietnamese", value: "vi" },
|
|
61
|
+
{ name: "Other", value: "_other" }
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
if (choice === "_other") {
|
|
65
|
+
const lang = await input({ message: "Enter language name:" });
|
|
66
|
+
return lang || "en";
|
|
67
|
+
}
|
|
68
|
+
return choice;
|
|
69
|
+
}
|
|
52
70
|
function checkLearnConfig() {
|
|
53
71
|
const local = readJson(localConfigPath());
|
|
54
72
|
const global = readJson(globalConfigPath());
|
|
@@ -123,34 +141,38 @@ async function setupGitignore() {
|
|
|
123
141
|
}
|
|
124
142
|
}
|
|
125
143
|
const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
const block = `${GITIGNORE_START}
|
|
145
|
+
${entries.join("\n")}
|
|
146
|
+
${GITIGNORE_END}`;
|
|
147
|
+
const managedBlockRe = new RegExp(
|
|
148
|
+
`${escapeRegExp(GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(GITIGNORE_END)}`
|
|
149
|
+
);
|
|
150
|
+
const legacyBlockRe = /# coding-friend\n([\w/.]+\n)*/;
|
|
151
|
+
let updated;
|
|
152
|
+
if (managedBlockRe.test(existing)) {
|
|
153
|
+
updated = existing.replace(managedBlockRe, block);
|
|
154
|
+
log.success(`Updated .gitignore: ${entries.join(", ")}`);
|
|
155
|
+
} else if (legacyBlockRe.test(existing)) {
|
|
156
|
+
updated = existing.replace(legacyBlockRe, block);
|
|
157
|
+
log.success(`Migrated .gitignore block: ${entries.join(", ")}`);
|
|
158
|
+
} else {
|
|
159
|
+
updated = existing.trimEnd() + "\n\n" + block + "\n";
|
|
160
|
+
log.success(`Added to .gitignore: ${entries.join(", ")}`);
|
|
130
161
|
}
|
|
131
|
-
|
|
132
|
-
# coding-friend
|
|
133
|
-
${newEntries.join("\n")}
|
|
134
|
-
`;
|
|
135
|
-
appendFileSync(".gitignore", block);
|
|
136
|
-
log.success(`Added to .gitignore: ${newEntries.join(", ")}`);
|
|
162
|
+
writeFileSync(".gitignore", updated);
|
|
137
163
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
]
|
|
146
|
-
});
|
|
147
|
-
if (choice === "_other") {
|
|
148
|
-
const lang = await input({ message: "Enter language name:" });
|
|
149
|
-
return lang || "en";
|
|
150
|
-
}
|
|
151
|
-
return choice;
|
|
164
|
+
function escapeRegExp(str) {
|
|
165
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
166
|
+
}
|
|
167
|
+
async function setupDocsLanguage() {
|
|
168
|
+
return selectLanguage(
|
|
169
|
+
"What language should generated docs be written in? (plans, memory, research, ask)"
|
|
170
|
+
);
|
|
152
171
|
}
|
|
153
172
|
async function setupLearnConfig(gitAvailable = true) {
|
|
173
|
+
const language = await selectLanguage(
|
|
174
|
+
"What language should /cf-learn notes be written in?"
|
|
175
|
+
);
|
|
154
176
|
const locationChoice = await select({
|
|
155
177
|
message: "Where to store learning docs?",
|
|
156
178
|
choices: [
|
|
@@ -178,21 +200,46 @@ async function setupLearnConfig(gitAvailable = true) {
|
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
}
|
|
203
|
+
const existingConfig = readJson(localConfigPath()) ?? readJson(globalConfigPath());
|
|
204
|
+
const existingCats = existingConfig?.learn?.categories;
|
|
205
|
+
const defaultNames = DEFAULT_CONFIG.learn.categories.map((c) => c.name).join(", ");
|
|
206
|
+
const choices = [
|
|
207
|
+
{
|
|
208
|
+
name: `Use defaults (${defaultNames})`,
|
|
209
|
+
value: "defaults"
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
if (existingCats && existingCats.length > 0) {
|
|
213
|
+
const existingNames = existingCats.map((c) => c.name).join(", ");
|
|
214
|
+
choices.push({
|
|
215
|
+
name: `Keep current (${existingNames})`,
|
|
216
|
+
value: "existing"
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
choices.push({ name: "Customize", value: "custom" });
|
|
181
220
|
const catChoice = await select({
|
|
182
221
|
message: "Categories for organizing learning docs?",
|
|
183
|
-
choices
|
|
184
|
-
{
|
|
185
|
-
name: "Use defaults (concepts, patterns, languages, tools, debugging)",
|
|
186
|
-
value: "defaults"
|
|
187
|
-
},
|
|
188
|
-
{ name: "Customize", value: "custom" }
|
|
189
|
-
]
|
|
222
|
+
choices
|
|
190
223
|
});
|
|
191
224
|
let categories = DEFAULT_CONFIG.learn.categories;
|
|
192
|
-
if (catChoice === "
|
|
225
|
+
if (catChoice === "existing" && existingCats) {
|
|
226
|
+
categories = existingCats;
|
|
227
|
+
} else if (catChoice === "custom") {
|
|
228
|
+
console.log();
|
|
229
|
+
if (existingCats && existingCats.length > 0) {
|
|
230
|
+
console.log("Current categories in config.json:");
|
|
231
|
+
for (const c of existingCats) {
|
|
232
|
+
log.dim(` ${c.name}: ${c.description}`);
|
|
233
|
+
}
|
|
234
|
+
console.log();
|
|
235
|
+
}
|
|
193
236
|
console.log(
|
|
194
237
|
'Enter categories (format: "name: description"). Empty line to finish.'
|
|
195
238
|
);
|
|
239
|
+
log.dim(
|
|
240
|
+
"Tip: you can also edit config.json later \u2014 see https://cf.dinhanhthi.com/docs/cli/cf-init/"
|
|
241
|
+
);
|
|
242
|
+
console.log();
|
|
196
243
|
const customCats = [];
|
|
197
244
|
let keepGoing = true;
|
|
198
245
|
while (keepGoing) {
|
|
@@ -229,7 +276,14 @@ async function setupLearnConfig(gitAvailable = true) {
|
|
|
229
276
|
let readmeIndex = false;
|
|
230
277
|
if (indexChoice === "single") readmeIndex = true;
|
|
231
278
|
else if (indexChoice === "per-category") readmeIndex = "per-category";
|
|
232
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
language,
|
|
281
|
+
outputDir,
|
|
282
|
+
categories,
|
|
283
|
+
autoCommit,
|
|
284
|
+
readmeIndex,
|
|
285
|
+
isExternal
|
|
286
|
+
};
|
|
233
287
|
}
|
|
234
288
|
async function setupClaudePermissions(outputDir, autoCommit) {
|
|
235
289
|
const resolved = resolvePath(outputDir);
|
|
@@ -286,6 +340,7 @@ function isDefaultConfig(config) {
|
|
|
286
340
|
if (config.language && config.language !== "en") return false;
|
|
287
341
|
if (config.learn) {
|
|
288
342
|
const l = config.learn;
|
|
343
|
+
if (l.language && l.language !== "en") return false;
|
|
289
344
|
if (l.outputDir && l.outputDir !== "docs/learn") return false;
|
|
290
345
|
if (l.autoCommit) return false;
|
|
291
346
|
if (l.readmeIndex) return false;
|
|
@@ -339,7 +394,11 @@ async function initCommand() {
|
|
|
339
394
|
done: checkGitignore()
|
|
340
395
|
}
|
|
341
396
|
] : [],
|
|
342
|
-
{
|
|
397
|
+
{
|
|
398
|
+
name: "docsLanguage",
|
|
399
|
+
label: "Set docs language (plans, memory, research, ask)",
|
|
400
|
+
done: checkDocsLanguage()
|
|
401
|
+
},
|
|
343
402
|
{ name: "learn", label: "Configure /cf-learn", done: checkLearnConfig() },
|
|
344
403
|
{
|
|
345
404
|
name: "completion",
|
|
@@ -408,14 +467,16 @@ async function initCommand() {
|
|
|
408
467
|
case "gitignore":
|
|
409
468
|
await setupGitignore();
|
|
410
469
|
break;
|
|
411
|
-
case "
|
|
412
|
-
const lang = await
|
|
470
|
+
case "docsLanguage": {
|
|
471
|
+
const lang = await setupDocsLanguage();
|
|
413
472
|
config.language = lang;
|
|
414
473
|
break;
|
|
415
474
|
}
|
|
416
475
|
case "learn": {
|
|
417
476
|
const learn = await setupLearnConfig(gitAvailable);
|
|
418
477
|
config.learn = {
|
|
478
|
+
...config.learn,
|
|
479
|
+
language: learn.language,
|
|
419
480
|
outputDir: learn.outputDir,
|
|
420
481
|
categories: learn.categories,
|
|
421
482
|
autoCommit: learn.autoCommit,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getLatestVersion,
|
|
3
3
|
semverCompare
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DHPWBSF5.js";
|
|
5
5
|
import {
|
|
6
6
|
isMarketplaceRegistered
|
|
7
7
|
} from "./chunk-MRTR7TJ4.js";
|
|
8
8
|
import {
|
|
9
9
|
getInstalledVersion
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-HSQX3PKW.js";
|
|
11
11
|
import "./chunk-4PLV2ENL.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-WXBI2HUL.js";
|
|
13
13
|
import {
|
|
14
14
|
commandExists,
|
|
15
15
|
run
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
saveStatuslineConfig,
|
|
5
5
|
selectStatuslineComponents,
|
|
6
6
|
writeStatuslineSettings
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HSQX3PKW.js";
|
|
8
8
|
import {
|
|
9
9
|
ALL_COMPONENT_IDS
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WXBI2HUL.js";
|
|
11
11
|
import "./chunk-WHCJT7E2.js";
|
|
12
12
|
import {
|
|
13
13
|
log
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
getLatestVersion,
|
|
3
3
|
semverCompare,
|
|
4
4
|
updateCommand
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-DHPWBSF5.js";
|
|
6
|
+
import "./chunk-HSQX3PKW.js";
|
|
7
7
|
import "./chunk-4PLV2ENL.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-WXBI2HUL.js";
|
|
9
9
|
import "./chunk-UFGNO6CW.js";
|
|
10
10
|
import "./chunk-WHCJT7E2.js";
|
|
11
11
|
import "./chunk-6DUFTBTO.js";
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog (Learn Host)
|
|
2
2
|
|
|
3
|
+
## v0.2.1 (2026-03-05)
|
|
4
|
+
|
|
5
|
+
- Add package manager tabs (npm, yarn, pnpm) to website ([#72e9e05](https://github.com/dinhanhthi/coding-friend/commit/72e9e05))
|
|
6
|
+
- Fix TOC heading text stripping markdown links from slug generation ([#9a8fb5c](https://github.com/dinhanhthi/coding-friend/commit/9a8fb5c))
|
|
7
|
+
- Decorate inline codes for TOC ([#573d7b0](https://github.com/dinhanhthi/coding-friend/commit/573d7b0))
|
|
8
|
+
|
|
3
9
|
## v0.2.0 (2026-03-03)
|
|
4
10
|
|
|
5
11
|
- Add dedicated tag pages for filtering docs by tag ([#06f5847](https://github.com/dinhanhthi/coding-friend/commit/06f5847))
|
|
@@ -37,7 +37,9 @@ export default async function DocPage({
|
|
|
37
37
|
/>
|
|
38
38
|
|
|
39
39
|
<header className="mb-8">
|
|
40
|
-
<h1 className="mb-2 text-3xl font-bold">
|
|
40
|
+
<h1 className="text-accent mb-2 text-3xl font-bold">
|
|
41
|
+
{doc.frontmatter.title}
|
|
42
|
+
</h1>
|
|
41
43
|
{(doc.frontmatter.created || doc.frontmatter.updated) && (
|
|
42
44
|
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500 dark:text-slate-400">
|
|
43
45
|
{doc.frontmatter.created && (
|
|
@@ -119,3 +119,23 @@ pre {
|
|
|
119
119
|
padding: 1px 5px;
|
|
120
120
|
border-radius: 4px;
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/* Tighter prose list spacing */
|
|
124
|
+
.prose
|
|
125
|
+
:where(ul, ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
126
|
+
margin-top: 0.5em;
|
|
127
|
+
margin-bottom: 0.5em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.prose :where(li):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
131
|
+
margin-top: 0.25em;
|
|
132
|
+
margin-bottom: 0.25em;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.prose
|
|
136
|
+
:where(li > p, li > ul, li > ol, li > pre, li > blockquote, li > div):not(
|
|
137
|
+
:where([class~="not-prose"], [class~="not-prose"] *)
|
|
138
|
+
) {
|
|
139
|
+
margin-top: 0.35em;
|
|
140
|
+
margin-bottom: 0.35em;
|
|
141
|
+
}
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import type { TocItem } from "@/lib/types";
|
|
5
5
|
|
|
6
|
+
function renderText(text: string) {
|
|
7
|
+
const stripped = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
8
|
+
const parts = stripped.split(/(`[^`]+`)/g);
|
|
9
|
+
return parts.map((part, i) =>
|
|
10
|
+
part.startsWith("`") && part.endsWith("`") ? (
|
|
11
|
+
<code key={i} className="rounded bg-slate-700/60 px-1 py-0.5 text-xs">
|
|
12
|
+
{part.slice(1, -1)}
|
|
13
|
+
</code>
|
|
14
|
+
) : (
|
|
15
|
+
part
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
interface Props {
|
|
7
21
|
headings: TocItem[];
|
|
8
22
|
}
|
|
@@ -51,7 +65,7 @@ export default function TableOfContents({ headings }: Props) {
|
|
|
51
65
|
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
52
66
|
}`}
|
|
53
67
|
>
|
|
54
|
-
{h.text}
|
|
68
|
+
{renderText(h.text)}
|
|
55
69
|
</a>
|
|
56
70
|
</li>
|
|
57
71
|
))}
|
|
@@ -189,7 +189,8 @@ export function extractHeadings(content: string): TocItem[] {
|
|
|
189
189
|
const regex = /^(#{2,3})\s+(.+)$/gm;
|
|
190
190
|
let match;
|
|
191
191
|
while ((match = regex.exec(content)) !== null) {
|
|
192
|
-
const
|
|
192
|
+
const raw = match[2].trim();
|
|
193
|
+
const text = raw.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
193
194
|
const id = text
|
|
194
195
|
.toLowerCase()
|
|
195
196
|
.replace(/[^a-z0-9]+/g, "-")
|