-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathremark-replace.ts
More file actions
102 lines (91 loc) · 3.63 KB
/
remark-replace.ts
File metadata and controls
102 lines (91 loc) · 3.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { visit } from "unist-util-visit";
import micromatch from "micromatch";
/**
* A replacement rule applied to MDX/Markdown files.
*
* - Use `include` to match files via glob(s).
* - Or use `includeFn` for custom logic (receives the normalized file path or undefined).
* - `replacements` maps placeholders like `{{key}}` → "value".
* - If `exclusive` is true, the first matching rule stops further replacements for that node.
*/
export type Rule = {
include?: string | string[];
includeFn?: (info: { path?: string }) => boolean;
replacements: Record<string, string>;
exclusive?: boolean;
};
/**
* Multi-rule remark plugin to replace placeholders in text, inlineCode and code blocks.
*
* Placeholders are written as `{{key}}` and replaced with the corresponding string
* from the active rule's `replacements` map.
*
* Safe defaults:
* - If `file.path` is missing (virtual content), rules are considered active unless
* their `includeFn` explicitly returns false.
* - Empty/whitespace patterns are ignored; if no valid patterns are left,
* the rule falls back to "match all".
*
* Example usage (Fumadocs / MDX config):
*
* import { defineConfig } from "fumadocs-mdx/config";
* import remarkReplaceMulti, { Rule } from "@/lib/remark-replace-multi";
*
* const rules: Rule[] = [
* { include: "content/docs/**", replacements: { version: "3.0.0", api: "/docs/api" }, exclusive: true },
* { include: "content/guides/**", replacements: { version: "2.2.0", api: "/guides/api" }, exclusive: true },
* ];
*
* export default defineConfig({
* mdxOptions: {
* remarkPlugins: (prev) => [[remarkReplaceMulti, rules], ...prev],
* },
* });
*/
export default function remarkReplaceMulti(rules: Rule[]) {
// --- helpers ---------------------------------------------------------------
/** Normalize vfile path (may be missing in virtual files). */
const getPath = (file?: any): string | undefined => {
const raw =
(typeof file?.path === "string" && file.path) ||
(Array.isArray(file?.history) && file.history[0]) ||
undefined;
return raw ? raw.replace(/\\/g, "/") : undefined;
};
/** Turn include into a clean, non-empty patterns array (or ["** /*"]). */
const toPatterns = (include?: string | string[]): string[] => {
const arr = Array.isArray(include) ? include : include ? [include] : [];
const cleaned = arr.map((p) => p.trim()).filter((p) => p.length > 0);
return cleaned.length ? cleaned : ["**/*"];
};
/** Does this rule apply to the given file path? */
const isRuleActive = (rule: Rule, path?: string): boolean => {
if (rule.includeFn) return rule.includeFn({ path });
// If we don't have a path, default to active (virtual files).
if (!path) return true;
return micromatch.isMatch(path, toPatterns(rule.include));
};
/** Replace all `{{key}}` with values from `map`. */
const replaceAll = (input: string, map: Record<string, string>): string => {
let out = input;
for (const [k, v] of Object.entries(map)) {
out = out.split(`{{${k}}}`).join(String(v));
}
return out;
};
// --- plugin ---------------------------------------------------------------
return (tree: any, file?: any) => {
const path = getPath(file);
// Collect rules that apply to this file.
const active = rules.filter((r) => isRuleActive(r, path));
if (active.length === 0) return;
// Apply replacements to text & code nodes.
visit(tree, ["text", "inlineCode", "code"], (node: any) => {
if (typeof node.value !== "string") return;
for (const rule of active) {
node.value = replaceAll(node.value, rule.replacements);
if (rule.exclusive) break; // stop after first exclusive rule
}
});
};
}