@pi-archimedes/core 0.2.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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Strip common leading whitespace and trailing blank lines from every fenced
3
+ * code block in `text`. Returns the transformed string.
4
+ *
5
+ * Algorithm per block:
6
+ * 0. Normalize `\r\n` → `\n` in the full input text before processing.
7
+ * 1. Find the minimum leading whitespace across all non-empty lines.
8
+ * 2. Strip that many characters from the start of every line
9
+ * (empty lines stay empty — they contribute no content but preserve structure).
10
+ * 3. Pop trailing empty lines.
11
+ *
12
+ * Trailing blank lines are ALWAYS stripped, even from 0-indent blocks.
13
+ * Leading whitespace is only stripped when minIndent > 0.
14
+ * Blocks containing only whitespace are left untouched.
15
+ *
16
+ * Known limitations:
17
+ * - Only space-based indentation is handled (tabs are not expanded).
18
+ * - If one line has 0 indent and others have N indent, nothing is stripped
19
+ * (minIndent === 0). This is standard textwrap.dedent behavior.
20
+ * - Fenced code blocks using 4+ backticks may be misparsed. The regex uses a
21
+ * negative lookahead to reject 4+ backtick openings (`/^(```(?!`))/
22
+ * - Line endings in the output are always `\n` (CRLF input is normalized).
23
+ */
24
+ export function unindentCodeBlocks(text: string): string {
25
+ // Step 0: normalize CRLF → LF
26
+ text = text.replace(/\r\n/g, "\n");
27
+
28
+ // Regex matches fenced code blocks with 3 backticks (not 4+).
29
+ // Group 1: opening fence
30
+ // Group 2: language tag (may be empty)
31
+ // Group 3: block content (everything between fences)
32
+ // Group 4: closing fence
33
+ const regex = /^(```(?!`))([^\n]*)\n([\s\S]*?)^(```(?!`))[ \t]*$/gm;
34
+
35
+ return text.replace(regex, (match, opening, lang, content, closing) => {
36
+ const lines = content.split("\n");
37
+
38
+ // Check if all lines are empty or whitespace-only → leave untouched
39
+ const allWhitespace = lines.every((line: string) => line.trim() === "");
40
+ if (allWhitespace) {
41
+ return match;
42
+ }
43
+
44
+ // Find minimum leading space count across non-empty lines
45
+ let minIndent = Infinity;
46
+ for (const line of lines) {
47
+ if (line.length === 0) continue; // skip empty lines
48
+ const m = line.match(/^( +)/);
49
+ if (m) {
50
+ minIndent = Math.min(minIndent, m[1].length);
51
+ } else {
52
+ // Line has no leading spaces → minIndent is 0
53
+ minIndent = 0;
54
+ break;
55
+ }
56
+ }
57
+
58
+ // Strip common leading whitespace (only if minIndent > 0)
59
+ if (minIndent > 0 && minIndent !== Infinity) {
60
+ for (let i = 0; i < lines.length; i++) {
61
+ if (lines[i].length >= minIndent) {
62
+ lines[i] = lines[i].slice(minIndent);
63
+ }
64
+ }
65
+ }
66
+
67
+ // Pop trailing empty lines
68
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
69
+ lines.pop();
70
+ }
71
+
72
+ return `${opening}${lang}\n${lines.join("\n")}\n${closing}`;
73
+ });
74
+ }