@hackersheet/next-document-content-components 0.1.0-alpha.26 → 0.1.0-alpha.27
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/cjs/components/code-block/parse-tree-output.js +35 -30
- package/dist/cjs/components/code-block/parse-tree-output.js.map +1 -1
- package/dist/esm/components/code-block/parse-tree-output.mjs +35 -30
- package/dist/esm/components/code-block/parse-tree-output.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -35,43 +35,48 @@ function isSummaryLine(line) {
|
|
|
35
35
|
function normalizeTabs(line) {
|
|
36
36
|
return line.replace(/│\t/g, "\u2502 ").replace(/\|\t/g, "| ").replace(/\t/g, " ");
|
|
37
37
|
}
|
|
38
|
-
const PREFIX_UNIT_PATTERN = /^(│ {3}|\| {3}| {4})/;
|
|
39
38
|
const CONNECTOR_PATTERNS = [
|
|
40
|
-
|
|
41
|
-
// Unicode branch
|
|
42
|
-
|
|
43
|
-
// Unicode last branch
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
/^\+-- ?/,
|
|
49
|
-
// ASCII branch
|
|
50
|
-
/^\\-- ?/
|
|
51
|
-
// ASCII last branch
|
|
39
|
+
/├──? ?/,
|
|
40
|
+
// Unicode branch (├── or ├─)
|
|
41
|
+
/└──? ?/,
|
|
42
|
+
// Unicode last branch (└── or └─)
|
|
43
|
+
/\+--? ?/,
|
|
44
|
+
// ASCII branch (+-- or +-)
|
|
45
|
+
/\\--? ?/
|
|
46
|
+
// ASCII last branch (\-- or \-)
|
|
52
47
|
];
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
function findConnector(line) {
|
|
49
|
+
for (const pattern of CONNECTOR_PATTERNS) {
|
|
50
|
+
const match = line.match(pattern);
|
|
51
|
+
if (match && match.index !== void 0) {
|
|
52
|
+
return { index: match.index, length: match[0].length };
|
|
53
|
+
}
|
|
57
54
|
}
|
|
58
|
-
return
|
|
55
|
+
return null;
|
|
59
56
|
}
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
function countVerticalLines(prefix) {
|
|
58
|
+
const matches = prefix.match(/[│|]/g);
|
|
59
|
+
return matches ? matches.length : 0;
|
|
60
|
+
}
|
|
61
|
+
function countPrefixUnits(prefix) {
|
|
62
|
+
const verticalLineCount = countVerticalLines(prefix);
|
|
63
|
+
const lengthBasedCount = Math.floor(prefix.length / 4);
|
|
64
|
+
return Math.max(verticalLineCount, lengthBasedCount);
|
|
67
65
|
}
|
|
68
66
|
function parseLine(line) {
|
|
69
67
|
const normalized = normalizeTabs(line);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
const connectorInfo = findConnector(normalized);
|
|
69
|
+
if (connectorInfo) {
|
|
70
|
+
const prefix = normalized.slice(0, connectorInfo.index);
|
|
71
|
+
const afterConnector = normalized.slice(connectorInfo.index + connectorInfo.length);
|
|
72
|
+
const prefixUnits2 = countPrefixUnits(prefix);
|
|
73
|
+
const level = prefixUnits2 + 1;
|
|
74
|
+
const name2 = afterConnector.trim();
|
|
75
|
+
return { level, name: name2 };
|
|
76
|
+
}
|
|
77
|
+
const prefixUnits = countPrefixUnits(normalized);
|
|
78
|
+
const name = normalized.replace(/^[│├└─|+\\\-\s]+/, "").trim() || normalized.trim();
|
|
79
|
+
return { level: prefixUnits, name };
|
|
75
80
|
}
|
|
76
81
|
function computeNextLevels(parsedLines) {
|
|
77
82
|
return parsedLines.map((_, index) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/code-block/parse-tree-output.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\nexport type TreeNode = {\n id: string;\n name: string;\n type: 'file' | 'directory';\n extension?: string;\n level: number;\n children?: TreeNode[];\n};\n\n/**\n * Computes the SHA-256 hash of a given string and returns it as a hex string.\n *\n * @param str - The input string to hash\n * @returns The SHA-256 digest in hexadecimal format\n */\nfunction hashSha256(str: string): string {\n const hash = createHash('sha256');\n hash.update(str);\n return hash.digest('hex');\n}\n\n/**\n * Checks if a line is a tree summary line (e.g., \"3 directories, 5 files\").\n */\nfunction isSummaryLine(line: string): boolean {\n const trimmed = line.trim();\n return /^(?:\\d+\\s+directories?,\\s*\\d+\\s+files?|\\d+\\s+directories?|\\d+\\s+files?)$/i.test(trimmed);\n}\n\n/**\n * Normalizes tab characters in a line.\n * - Replaces │\\t or |\\t with vertical line + 3 spaces (4 chars total)\n * - Replaces remaining tabs with 4 spaces\n */\nfunction normalizeTabs(line: string): string {\n return line.replace(/│\\t/g, '│ ').replace(/\\|\\t/g, '| ').replace(/\\t/g, ' ');\n}\n\n/**\n * Pattern for 4-character prefix units before the connector.\n * Unicode: \"│ \" or \" \"\n * ASCII: \"| \" or \" \"\n */\nconst PREFIX_UNIT_PATTERN = /^(│ {3}|\\| {3}| {4})/;\n\n/**\n * Patterns for tree connectors.\n */\nconst CONNECTOR_PATTERNS = [\n /^├── ?/, // Unicode branch\n /^└── ?/, // Unicode last branch\n /^├─ ?/, // Shorter Unicode variant\n /^└─ ?/, // Shorter Unicode variant\n /^\\+-- ?/, // ASCII branch\n /^\\\\-- ?/, // ASCII last branch\n];\n\n/**\n * Recursively counts and removes prefix units from the beginning of a line.\n */\nfunction countAndRemovePrefixes(line: string, count: number = 0): { remaining: string; prefixCount: number } {\n const match = line.match(PREFIX_UNIT_PATTERN);\n if (match) {\n return countAndRemovePrefixes(line.slice(match[0].length), count + 1);\n }\n return { remaining: line, prefixCount: count };\n}\n\n/**\n * Checks if the line starts with a connector and removes it.\n */\nfunction checkAndRemoveConnector(line: string): { remaining: string; hasConnector: boolean } {\n const matchingPattern = CONNECTOR_PATTERNS.find((pattern) => pattern.test(line));\n if (matchingPattern) {\n const match = line.match(matchingPattern);\n return { remaining: line.slice(match![0].length), hasConnector: true };\n }\n return { remaining: line, hasConnector: false };\n}\n\n/**\n * Parses a single line from tree command output.\n * Returns the indentation level and the node name.\n *\n * Standard tree format:\n * - Root: no prefix\n * - Level 1: ├── or └── (preceded by nothing)\n * - Level 2: │ ├── or │ └── or ├── or └── (one 4-char prefix)\n * - Level 3: │ │ ├── etc. (two 4-char prefixes)\n */\nfunction parseLine(line: string): { level: number; name: string } {\n const normalized = normalizeTabs(line);\n const { remaining: afterPrefixes, prefixCount } = countAndRemovePrefixes(normalized);\n const { remaining: afterConnector, hasConnector } = checkAndRemoveConnector(afterPrefixes);\n\n // Calculate level: prefix count + 1 if there's a connector\n const level = hasConnector ? prefixCount + 1 : prefixCount;\n\n // Clean any remaining box-drawing characters and trim\n const name = afterConnector.replace(/^[│├└─|+\\\\\\-\\s]+/, '').trim() || afterConnector.trim();\n\n return { level, name };\n}\n\ntype ParsedLine = { level: number; name: string };\n\n/**\n * Computes the next non-empty line's level for each line.\n * Used to determine if a node is a directory (has children).\n */\nfunction computeNextLevels(parsedLines: ParsedLine[]): (number | null)[] {\n return parsedLines.map((_, index) => {\n const nextLineWithName = parsedLines.slice(index + 1).find((line) => line.name);\n return nextLineWithName?.level ?? null;\n });\n}\n\n/**\n * Gets file extension from a filename.\n */\nfunction getExtension(filename: string): string | undefined {\n return filename.match(/\\.(\\w+)$/)?.[1];\n}\n\n/**\n * Creates a TreeNode from parsed line data.\n */\nfunction createNode(parsedLine: ParsedLine, index: number, idPrefix: string, isDirectory: boolean): TreeNode {\n return {\n id: index === 0 ? idPrefix + 'root' : idPrefix + 'node-' + index,\n name: parsedLine.name,\n type: isDirectory ? 'directory' : 'file',\n extension: isDirectory ? undefined : getExtension(parsedLine.name),\n level: parsedLine.level,\n children: isDirectory ? [] : undefined,\n };\n}\n\n/**\n * Finds the parent node for a given level by traversing the stack.\n * Returns a new stack with nodes popped until we find the correct parent.\n */\nfunction findParentStack(\n stack: Array<{ node: TreeNode; level: number }>,\n level: number\n): Array<{ node: TreeNode; level: number }> {\n if (stack.length <= 1 || stack[stack.length - 1].level < level) {\n return stack;\n }\n return findParentStack(stack.slice(0, -1), level);\n}\n\n/**\n * Builds the tree structure from parsed lines using reduce.\n */\nfunction buildTree(parsedLines: ParsedLine[], nextLevels: (number | null)[], idPrefix: string): TreeNode {\n const rootLine = parsedLines[0];\n const rootNode = createNode({ ...rootLine, level: 0 }, 0, idPrefix, true);\n\n const result = parsedLines.slice(1).reduce(\n (acc, parsedLine, idx) => {\n const index = idx + 1; // Adjust for slice(1)\n if (!parsedLine.name) return acc;\n\n const nextLevel = nextLevels[index];\n const isDirectory = nextLevel !== null && nextLevel > parsedLine.level;\n const newNode = createNode(parsedLine, index, idPrefix, isDirectory);\n\n // Find the correct parent\n const newStack = findParentStack(acc.stack, parsedLine.level);\n const parent = newStack[newStack.length - 1].node;\n\n // Add to parent's children (mutating children array is acceptable here\n // since we're building the tree and the node is not yet exposed)\n parent.children = parent.children ?? [];\n parent.children.push(newNode);\n\n // Update stack if this is a directory\n const updatedStack = isDirectory ? [...newStack, { node: newNode, level: parsedLine.level }] : newStack;\n\n return { ...acc, stack: updatedStack };\n },\n { root: rootNode, stack: [{ node: rootNode, level: 0 }] as Array<{ node: TreeNode; level: number }> }\n );\n\n return result.root;\n}\n\n/**\n * Parses the output of a `tree` command (including box-drawing characters)\n * and returns a TreeNode tree structure.\n *\n * @remarks\n * - Supports both Unicode box-drawing (│, ├, └, ─) and ASCII (|, +, \\, -) formats.\n * - Handles mixed tabs and spaces in indentation.\n * - Automatically excludes summary lines at the end (e.g., `3 directories, 5 files`).\n * - Automatically excludes empty lines.\n * - Uses the rule that only lines with children are considered directories\n * (lines without children are treated as files).\n * - Returns `null` if an exception occurs during parsing\n * (callers should handle fallback rendering).\n *\n * @param treeOutput - The complete text output from the `tree` command\n * @returns The root `TreeNode` on success, or `null` on failure or empty input\n *\n * @complexity O(n) - Processes in a single pass over n input lines\n * (with an additional reverse pass, so memory usage is O(n)).\n */\nexport function parseTreeOutput(treeOutput: string): TreeNode | null {\n // Normalize input: remove carriage returns and trim\n const raw = treeOutput.replace(/\\r/g, '').trim();\n if (!raw) return null;\n\n const lines = raw.split('\\n');\n\n // Filter out empty lines and summary lines\n const filteredLines = lines.filter((line) => {\n const trimmed = line.trim();\n return trimmed !== '' && !isSummaryLine(line);\n });\n\n if (filteredLines.length === 0) return null;\n\n const idPrefix = 'tree-' + hashSha256(raw) + '-';\n\n // Parse all lines to extract level and name\n const parsedLines = filteredLines.map(parseLine);\n\n // Pre-compute next line's level for directory detection\n const nextLevels = computeNextLevels(parsedLines);\n\n try {\n return buildTree(parsedLines, nextLevels, idPrefix);\n } catch {\n return null;\n }\n}\n\nexport default parseTreeOutput;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/code-block/parse-tree-output.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\nexport type TreeNode = {\n id: string;\n name: string;\n type: 'file' | 'directory';\n extension?: string;\n level: number;\n children?: TreeNode[];\n};\n\n/**\n * Computes the SHA-256 hash of a given string and returns it as a hex string.\n *\n * @param str - The input string to hash\n * @returns The SHA-256 digest in hexadecimal format\n */\nfunction hashSha256(str: string): string {\n const hash = createHash('sha256');\n hash.update(str);\n return hash.digest('hex');\n}\n\n/**\n * Checks if a line is a tree summary line (e.g., \"3 directories, 5 files\").\n */\nfunction isSummaryLine(line: string): boolean {\n const trimmed = line.trim();\n return /^(?:\\d+\\s+directories?,\\s*\\d+\\s+files?|\\d+\\s+directories?|\\d+\\s+files?)$/i.test(trimmed);\n}\n\n/**\n * Normalizes tab characters in a line.\n * - Replaces │\\t or |\\t with vertical line + 3 spaces (4 chars total)\n * - Replaces remaining tabs with 4 spaces\n */\nfunction normalizeTabs(line: string): string {\n return line.replace(/│\\t/g, '│ ').replace(/\\|\\t/g, '| ').replace(/\\t/g, ' ');\n}\n\n/**\n * Patterns for tree connectors (branch and last-branch indicators).\n * Supports both Unicode box-drawing characters and ASCII equivalents.\n */\nconst CONNECTOR_PATTERNS = [\n /├──? ?/, // Unicode branch (├── or ├─)\n /└──? ?/, // Unicode last branch (└── or └─)\n /\\+--? ?/, // ASCII branch (+-- or +-)\n /\\\\--? ?/, // ASCII last branch (\\-- or \\-)\n];\n\n/**\n * Finds the first connector in the line and returns its position and length.\n * Returns null if no connector is found.\n */\nfunction findConnector(line: string): { index: number; length: number } | null {\n for (const pattern of CONNECTOR_PATTERNS) {\n const match = line.match(pattern);\n if (match && match.index !== undefined) {\n return { index: match.index, length: match[0].length };\n }\n }\n return null;\n}\n\n/**\n * Counts the number of vertical line characters in the prefix portion.\n * Supports both Unicode (│) and ASCII (|) vertical lines.\n */\nfunction countVerticalLines(prefix: string): number {\n const matches = prefix.match(/[│|]/g);\n return matches ? matches.length : 0;\n}\n\n/**\n * Counts prefix units in a string.\n * A prefix unit is typically 4 characters wide and can be:\n * - A vertical line (│ or |) followed by spaces (e.g., \"│ \")\n * - 4 consecutive spaces (e.g., \" \")\n *\n * The algorithm uses the maximum of:\n * - Number of vertical lines (for standard tree output)\n * - Total prefix length / 4 (for space-based indentation)\n *\n * This handles various cases:\n * - \"│ │ \" → 2 units (2 vertical lines)\n * - \" \" → 2 units (8 chars / 4)\n * - \"│ \" → 2 units (8 chars / 4, even with only 1 vertical line)\n */\nfunction countPrefixUnits(prefix: string): number {\n const verticalLineCount = countVerticalLines(prefix);\n const lengthBasedCount = Math.floor(prefix.length / 4);\n\n // Use the maximum to handle cases where vertical lines are sparse\n // (e.g., \"│ \" should be 2 units, not 1)\n return Math.max(verticalLineCount, lengthBasedCount);\n}\n\n/**\n * Parses a single line from tree command output.\n * Returns the indentation level and the node name.\n *\n * The level is determined by analyzing the prefix portion before the connector:\n * - Count vertical line characters (│ or |) when present\n * - Fall back to counting 4-space units for indentation-only formats\n *\n * @example\n * - \"root\" → level 0\n * - \"├── file\" → level 1 (0 prefix units + connector)\n * - \"│ ├── file\" → level 2 (1 vertical line + connector)\n * - \" └── file\" → level 2 (4 spaces = 1 unit + connector)\n * - \"│ │ └── file\" → level 3 (2 vertical lines + connector)\n */\nfunction parseLine(line: string): { level: number; name: string } {\n const normalized = normalizeTabs(line);\n const connectorInfo = findConnector(normalized);\n\n if (connectorInfo) {\n const prefix = normalized.slice(0, connectorInfo.index);\n const afterConnector = normalized.slice(connectorInfo.index + connectorInfo.length);\n const prefixUnits = countPrefixUnits(prefix);\n\n // Level = number of prefix units + 1 (for the connector itself)\n const level = prefixUnits + 1;\n const name = afterConnector.trim();\n\n return { level, name };\n }\n\n // No connector found - this is likely the root or a line with only indentation\n const prefixUnits = countPrefixUnits(normalized);\n const name = normalized.replace(/^[│├└─|+\\\\\\-\\s]+/, '').trim() || normalized.trim();\n\n return { level: prefixUnits, name };\n}\n\ntype ParsedLine = { level: number; name: string };\n\n/**\n * Computes the next non-empty line's level for each line.\n * Used to determine if a node is a directory (has children).\n */\nfunction computeNextLevels(parsedLines: ParsedLine[]): (number | null)[] {\n return parsedLines.map((_, index) => {\n const nextLineWithName = parsedLines.slice(index + 1).find((line) => line.name);\n return nextLineWithName?.level ?? null;\n });\n}\n\n/**\n * Gets file extension from a filename.\n */\nfunction getExtension(filename: string): string | undefined {\n return filename.match(/\\.(\\w+)$/)?.[1];\n}\n\n/**\n * Creates a TreeNode from parsed line data.\n */\nfunction createNode(parsedLine: ParsedLine, index: number, idPrefix: string, isDirectory: boolean): TreeNode {\n return {\n id: index === 0 ? idPrefix + 'root' : idPrefix + 'node-' + index,\n name: parsedLine.name,\n type: isDirectory ? 'directory' : 'file',\n extension: isDirectory ? undefined : getExtension(parsedLine.name),\n level: parsedLine.level,\n children: isDirectory ? [] : undefined,\n };\n}\n\n/**\n * Finds the parent node for a given level by traversing the stack.\n * Returns a new stack with nodes popped until we find the correct parent.\n */\nfunction findParentStack(\n stack: Array<{ node: TreeNode; level: number }>,\n level: number\n): Array<{ node: TreeNode; level: number }> {\n if (stack.length <= 1 || stack[stack.length - 1].level < level) {\n return stack;\n }\n return findParentStack(stack.slice(0, -1), level);\n}\n\n/**\n * Builds the tree structure from parsed lines using reduce.\n */\nfunction buildTree(parsedLines: ParsedLine[], nextLevels: (number | null)[], idPrefix: string): TreeNode {\n const rootLine = parsedLines[0];\n const rootNode = createNode({ ...rootLine, level: 0 }, 0, idPrefix, true);\n\n const result = parsedLines.slice(1).reduce(\n (acc, parsedLine, idx) => {\n const index = idx + 1; // Adjust for slice(1)\n if (!parsedLine.name) return acc;\n\n const nextLevel = nextLevels[index];\n const isDirectory = nextLevel !== null && nextLevel > parsedLine.level;\n const newNode = createNode(parsedLine, index, idPrefix, isDirectory);\n\n // Find the correct parent\n const newStack = findParentStack(acc.stack, parsedLine.level);\n const parent = newStack[newStack.length - 1].node;\n\n // Add to parent's children (mutating children array is acceptable here\n // since we're building the tree and the node is not yet exposed)\n parent.children = parent.children ?? [];\n parent.children.push(newNode);\n\n // Update stack if this is a directory\n const updatedStack = isDirectory ? [...newStack, { node: newNode, level: parsedLine.level }] : newStack;\n\n return { ...acc, stack: updatedStack };\n },\n { root: rootNode, stack: [{ node: rootNode, level: 0 }] as Array<{ node: TreeNode; level: number }> }\n );\n\n return result.root;\n}\n\n/**\n * Parses the output of a `tree` command (including box-drawing characters)\n * and returns a TreeNode tree structure.\n *\n * @remarks\n * - Supports both Unicode box-drawing (│, ├, └, ─) and ASCII (|, +, \\, -) formats.\n * - Handles mixed tabs and spaces in indentation.\n * - Automatically excludes summary lines at the end (e.g., `3 directories, 5 files`).\n * - Automatically excludes empty lines.\n * - Uses the rule that only lines with children are considered directories\n * (lines without children are treated as files).\n * - Returns `null` if an exception occurs during parsing\n * (callers should handle fallback rendering).\n *\n * @param treeOutput - The complete text output from the `tree` command\n * @returns The root `TreeNode` on success, or `null` on failure or empty input\n *\n * @complexity O(n) - Processes in a single pass over n input lines\n * (with an additional reverse pass, so memory usage is O(n)).\n */\nexport function parseTreeOutput(treeOutput: string): TreeNode | null {\n // Normalize input: remove carriage returns and trim\n const raw = treeOutput.replace(/\\r/g, '').trim();\n if (!raw) return null;\n\n const lines = raw.split('\\n');\n\n // Filter out empty lines and summary lines\n const filteredLines = lines.filter((line) => {\n const trimmed = line.trim();\n return trimmed !== '' && !isSummaryLine(line);\n });\n\n if (filteredLines.length === 0) return null;\n\n const idPrefix = 'tree-' + hashSha256(raw) + '-';\n\n // Parse all lines to extract level and name\n const parsedLines = filteredLines.map(parseLine);\n\n // Pre-compute next line's level for directory detection\n const nextLevels = computeNextLevels(parsedLines);\n\n try {\n return buildTree(parsedLines, nextLevels, idPrefix);\n } catch {\n return null;\n }\n}\n\nexport default parseTreeOutput;\n"],"mappings":"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","names":["prefixUnits","name"]}
|
|
@@ -11,43 +11,48 @@ function isSummaryLine(line) {
|
|
|
11
11
|
function normalizeTabs(line) {
|
|
12
12
|
return line.replace(/│\t/g, "\u2502 ").replace(/\|\t/g, "| ").replace(/\t/g, " ");
|
|
13
13
|
}
|
|
14
|
-
const PREFIX_UNIT_PATTERN = /^(│ {3}|\| {3}| {4})/;
|
|
15
14
|
const CONNECTOR_PATTERNS = [
|
|
16
|
-
|
|
17
|
-
// Unicode branch
|
|
18
|
-
|
|
19
|
-
// Unicode last branch
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
/^\+-- ?/,
|
|
25
|
-
// ASCII branch
|
|
26
|
-
/^\\-- ?/
|
|
27
|
-
// ASCII last branch
|
|
15
|
+
/├──? ?/,
|
|
16
|
+
// Unicode branch (├── or ├─)
|
|
17
|
+
/└──? ?/,
|
|
18
|
+
// Unicode last branch (└── or └─)
|
|
19
|
+
/\+--? ?/,
|
|
20
|
+
// ASCII branch (+-- or +-)
|
|
21
|
+
/\\--? ?/
|
|
22
|
+
// ASCII last branch (\-- or \-)
|
|
28
23
|
];
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
function findConnector(line) {
|
|
25
|
+
for (const pattern of CONNECTOR_PATTERNS) {
|
|
26
|
+
const match = line.match(pattern);
|
|
27
|
+
if (match && match.index !== void 0) {
|
|
28
|
+
return { index: match.index, length: match[0].length };
|
|
29
|
+
}
|
|
33
30
|
}
|
|
34
|
-
return
|
|
31
|
+
return null;
|
|
35
32
|
}
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
function countVerticalLines(prefix) {
|
|
34
|
+
const matches = prefix.match(/[│|]/g);
|
|
35
|
+
return matches ? matches.length : 0;
|
|
36
|
+
}
|
|
37
|
+
function countPrefixUnits(prefix) {
|
|
38
|
+
const verticalLineCount = countVerticalLines(prefix);
|
|
39
|
+
const lengthBasedCount = Math.floor(prefix.length / 4);
|
|
40
|
+
return Math.max(verticalLineCount, lengthBasedCount);
|
|
43
41
|
}
|
|
44
42
|
function parseLine(line) {
|
|
45
43
|
const normalized = normalizeTabs(line);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
const connectorInfo = findConnector(normalized);
|
|
45
|
+
if (connectorInfo) {
|
|
46
|
+
const prefix = normalized.slice(0, connectorInfo.index);
|
|
47
|
+
const afterConnector = normalized.slice(connectorInfo.index + connectorInfo.length);
|
|
48
|
+
const prefixUnits2 = countPrefixUnits(prefix);
|
|
49
|
+
const level = prefixUnits2 + 1;
|
|
50
|
+
const name2 = afterConnector.trim();
|
|
51
|
+
return { level, name: name2 };
|
|
52
|
+
}
|
|
53
|
+
const prefixUnits = countPrefixUnits(normalized);
|
|
54
|
+
const name = normalized.replace(/^[│├└─|+\\\-\s]+/, "").trim() || normalized.trim();
|
|
55
|
+
return { level: prefixUnits, name };
|
|
51
56
|
}
|
|
52
57
|
function computeNextLevels(parsedLines) {
|
|
53
58
|
return parsedLines.map((_, index) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/code-block/parse-tree-output.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\nexport type TreeNode = {\n id: string;\n name: string;\n type: 'file' | 'directory';\n extension?: string;\n level: number;\n children?: TreeNode[];\n};\n\n/**\n * Computes the SHA-256 hash of a given string and returns it as a hex string.\n *\n * @param str - The input string to hash\n * @returns The SHA-256 digest in hexadecimal format\n */\nfunction hashSha256(str: string): string {\n const hash = createHash('sha256');\n hash.update(str);\n return hash.digest('hex');\n}\n\n/**\n * Checks if a line is a tree summary line (e.g., \"3 directories, 5 files\").\n */\nfunction isSummaryLine(line: string): boolean {\n const trimmed = line.trim();\n return /^(?:\\d+\\s+directories?,\\s*\\d+\\s+files?|\\d+\\s+directories?|\\d+\\s+files?)$/i.test(trimmed);\n}\n\n/**\n * Normalizes tab characters in a line.\n * - Replaces │\\t or |\\t with vertical line + 3 spaces (4 chars total)\n * - Replaces remaining tabs with 4 spaces\n */\nfunction normalizeTabs(line: string): string {\n return line.replace(/│\\t/g, '│ ').replace(/\\|\\t/g, '| ').replace(/\\t/g, ' ');\n}\n\n/**\n * Pattern for 4-character prefix units before the connector.\n * Unicode: \"│ \" or \" \"\n * ASCII: \"| \" or \" \"\n */\nconst PREFIX_UNIT_PATTERN = /^(│ {3}|\\| {3}| {4})/;\n\n/**\n * Patterns for tree connectors.\n */\nconst CONNECTOR_PATTERNS = [\n /^├── ?/, // Unicode branch\n /^└── ?/, // Unicode last branch\n /^├─ ?/, // Shorter Unicode variant\n /^└─ ?/, // Shorter Unicode variant\n /^\\+-- ?/, // ASCII branch\n /^\\\\-- ?/, // ASCII last branch\n];\n\n/**\n * Recursively counts and removes prefix units from the beginning of a line.\n */\nfunction countAndRemovePrefixes(line: string, count: number = 0): { remaining: string; prefixCount: number } {\n const match = line.match(PREFIX_UNIT_PATTERN);\n if (match) {\n return countAndRemovePrefixes(line.slice(match[0].length), count + 1);\n }\n return { remaining: line, prefixCount: count };\n}\n\n/**\n * Checks if the line starts with a connector and removes it.\n */\nfunction checkAndRemoveConnector(line: string): { remaining: string; hasConnector: boolean } {\n const matchingPattern = CONNECTOR_PATTERNS.find((pattern) => pattern.test(line));\n if (matchingPattern) {\n const match = line.match(matchingPattern);\n return { remaining: line.slice(match![0].length), hasConnector: true };\n }\n return { remaining: line, hasConnector: false };\n}\n\n/**\n * Parses a single line from tree command output.\n * Returns the indentation level and the node name.\n *\n * Standard tree format:\n * - Root: no prefix\n * - Level 1: ├── or └── (preceded by nothing)\n * - Level 2: │ ├── or │ └── or ├── or └── (one 4-char prefix)\n * - Level 3: │ │ ├── etc. (two 4-char prefixes)\n */\nfunction parseLine(line: string): { level: number; name: string } {\n const normalized = normalizeTabs(line);\n const { remaining: afterPrefixes, prefixCount } = countAndRemovePrefixes(normalized);\n const { remaining: afterConnector, hasConnector } = checkAndRemoveConnector(afterPrefixes);\n\n // Calculate level: prefix count + 1 if there's a connector\n const level = hasConnector ? prefixCount + 1 : prefixCount;\n\n // Clean any remaining box-drawing characters and trim\n const name = afterConnector.replace(/^[│├└─|+\\\\\\-\\s]+/, '').trim() || afterConnector.trim();\n\n return { level, name };\n}\n\ntype ParsedLine = { level: number; name: string };\n\n/**\n * Computes the next non-empty line's level for each line.\n * Used to determine if a node is a directory (has children).\n */\nfunction computeNextLevels(parsedLines: ParsedLine[]): (number | null)[] {\n return parsedLines.map((_, index) => {\n const nextLineWithName = parsedLines.slice(index + 1).find((line) => line.name);\n return nextLineWithName?.level ?? null;\n });\n}\n\n/**\n * Gets file extension from a filename.\n */\nfunction getExtension(filename: string): string | undefined {\n return filename.match(/\\.(\\w+)$/)?.[1];\n}\n\n/**\n * Creates a TreeNode from parsed line data.\n */\nfunction createNode(parsedLine: ParsedLine, index: number, idPrefix: string, isDirectory: boolean): TreeNode {\n return {\n id: index === 0 ? idPrefix + 'root' : idPrefix + 'node-' + index,\n name: parsedLine.name,\n type: isDirectory ? 'directory' : 'file',\n extension: isDirectory ? undefined : getExtension(parsedLine.name),\n level: parsedLine.level,\n children: isDirectory ? [] : undefined,\n };\n}\n\n/**\n * Finds the parent node for a given level by traversing the stack.\n * Returns a new stack with nodes popped until we find the correct parent.\n */\nfunction findParentStack(\n stack: Array<{ node: TreeNode; level: number }>,\n level: number\n): Array<{ node: TreeNode; level: number }> {\n if (stack.length <= 1 || stack[stack.length - 1].level < level) {\n return stack;\n }\n return findParentStack(stack.slice(0, -1), level);\n}\n\n/**\n * Builds the tree structure from parsed lines using reduce.\n */\nfunction buildTree(parsedLines: ParsedLine[], nextLevels: (number | null)[], idPrefix: string): TreeNode {\n const rootLine = parsedLines[0];\n const rootNode = createNode({ ...rootLine, level: 0 }, 0, idPrefix, true);\n\n const result = parsedLines.slice(1).reduce(\n (acc, parsedLine, idx) => {\n const index = idx + 1; // Adjust for slice(1)\n if (!parsedLine.name) return acc;\n\n const nextLevel = nextLevels[index];\n const isDirectory = nextLevel !== null && nextLevel > parsedLine.level;\n const newNode = createNode(parsedLine, index, idPrefix, isDirectory);\n\n // Find the correct parent\n const newStack = findParentStack(acc.stack, parsedLine.level);\n const parent = newStack[newStack.length - 1].node;\n\n // Add to parent's children (mutating children array is acceptable here\n // since we're building the tree and the node is not yet exposed)\n parent.children = parent.children ?? [];\n parent.children.push(newNode);\n\n // Update stack if this is a directory\n const updatedStack = isDirectory ? [...newStack, { node: newNode, level: parsedLine.level }] : newStack;\n\n return { ...acc, stack: updatedStack };\n },\n { root: rootNode, stack: [{ node: rootNode, level: 0 }] as Array<{ node: TreeNode; level: number }> }\n );\n\n return result.root;\n}\n\n/**\n * Parses the output of a `tree` command (including box-drawing characters)\n * and returns a TreeNode tree structure.\n *\n * @remarks\n * - Supports both Unicode box-drawing (│, ├, └, ─) and ASCII (|, +, \\, -) formats.\n * - Handles mixed tabs and spaces in indentation.\n * - Automatically excludes summary lines at the end (e.g., `3 directories, 5 files`).\n * - Automatically excludes empty lines.\n * - Uses the rule that only lines with children are considered directories\n * (lines without children are treated as files).\n * - Returns `null` if an exception occurs during parsing\n * (callers should handle fallback rendering).\n *\n * @param treeOutput - The complete text output from the `tree` command\n * @returns The root `TreeNode` on success, or `null` on failure or empty input\n *\n * @complexity O(n) - Processes in a single pass over n input lines\n * (with an additional reverse pass, so memory usage is O(n)).\n */\nexport function parseTreeOutput(treeOutput: string): TreeNode | null {\n // Normalize input: remove carriage returns and trim\n const raw = treeOutput.replace(/\\r/g, '').trim();\n if (!raw) return null;\n\n const lines = raw.split('\\n');\n\n // Filter out empty lines and summary lines\n const filteredLines = lines.filter((line) => {\n const trimmed = line.trim();\n return trimmed !== '' && !isSummaryLine(line);\n });\n\n if (filteredLines.length === 0) return null;\n\n const idPrefix = 'tree-' + hashSha256(raw) + '-';\n\n // Parse all lines to extract level and name\n const parsedLines = filteredLines.map(parseLine);\n\n // Pre-compute next line's level for directory detection\n const nextLevels = computeNextLevels(parsedLines);\n\n try {\n return buildTree(parsedLines, nextLevels, idPrefix);\n } catch {\n return null;\n }\n}\n\nexport default parseTreeOutput;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/code-block/parse-tree-output.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\nexport type TreeNode = {\n id: string;\n name: string;\n type: 'file' | 'directory';\n extension?: string;\n level: number;\n children?: TreeNode[];\n};\n\n/**\n * Computes the SHA-256 hash of a given string and returns it as a hex string.\n *\n * @param str - The input string to hash\n * @returns The SHA-256 digest in hexadecimal format\n */\nfunction hashSha256(str: string): string {\n const hash = createHash('sha256');\n hash.update(str);\n return hash.digest('hex');\n}\n\n/**\n * Checks if a line is a tree summary line (e.g., \"3 directories, 5 files\").\n */\nfunction isSummaryLine(line: string): boolean {\n const trimmed = line.trim();\n return /^(?:\\d+\\s+directories?,\\s*\\d+\\s+files?|\\d+\\s+directories?|\\d+\\s+files?)$/i.test(trimmed);\n}\n\n/**\n * Normalizes tab characters in a line.\n * - Replaces │\\t or |\\t with vertical line + 3 spaces (4 chars total)\n * - Replaces remaining tabs with 4 spaces\n */\nfunction normalizeTabs(line: string): string {\n return line.replace(/│\\t/g, '│ ').replace(/\\|\\t/g, '| ').replace(/\\t/g, ' ');\n}\n\n/**\n * Patterns for tree connectors (branch and last-branch indicators).\n * Supports both Unicode box-drawing characters and ASCII equivalents.\n */\nconst CONNECTOR_PATTERNS = [\n /├──? ?/, // Unicode branch (├── or ├─)\n /└──? ?/, // Unicode last branch (└── or └─)\n /\\+--? ?/, // ASCII branch (+-- or +-)\n /\\\\--? ?/, // ASCII last branch (\\-- or \\-)\n];\n\n/**\n * Finds the first connector in the line and returns its position and length.\n * Returns null if no connector is found.\n */\nfunction findConnector(line: string): { index: number; length: number } | null {\n for (const pattern of CONNECTOR_PATTERNS) {\n const match = line.match(pattern);\n if (match && match.index !== undefined) {\n return { index: match.index, length: match[0].length };\n }\n }\n return null;\n}\n\n/**\n * Counts the number of vertical line characters in the prefix portion.\n * Supports both Unicode (│) and ASCII (|) vertical lines.\n */\nfunction countVerticalLines(prefix: string): number {\n const matches = prefix.match(/[│|]/g);\n return matches ? matches.length : 0;\n}\n\n/**\n * Counts prefix units in a string.\n * A prefix unit is typically 4 characters wide and can be:\n * - A vertical line (│ or |) followed by spaces (e.g., \"│ \")\n * - 4 consecutive spaces (e.g., \" \")\n *\n * The algorithm uses the maximum of:\n * - Number of vertical lines (for standard tree output)\n * - Total prefix length / 4 (for space-based indentation)\n *\n * This handles various cases:\n * - \"│ │ \" → 2 units (2 vertical lines)\n * - \" \" → 2 units (8 chars / 4)\n * - \"│ \" → 2 units (8 chars / 4, even with only 1 vertical line)\n */\nfunction countPrefixUnits(prefix: string): number {\n const verticalLineCount = countVerticalLines(prefix);\n const lengthBasedCount = Math.floor(prefix.length / 4);\n\n // Use the maximum to handle cases where vertical lines are sparse\n // (e.g., \"│ \" should be 2 units, not 1)\n return Math.max(verticalLineCount, lengthBasedCount);\n}\n\n/**\n * Parses a single line from tree command output.\n * Returns the indentation level and the node name.\n *\n * The level is determined by analyzing the prefix portion before the connector:\n * - Count vertical line characters (│ or |) when present\n * - Fall back to counting 4-space units for indentation-only formats\n *\n * @example\n * - \"root\" → level 0\n * - \"├── file\" → level 1 (0 prefix units + connector)\n * - \"│ ├── file\" → level 2 (1 vertical line + connector)\n * - \" └── file\" → level 2 (4 spaces = 1 unit + connector)\n * - \"│ │ └── file\" → level 3 (2 vertical lines + connector)\n */\nfunction parseLine(line: string): { level: number; name: string } {\n const normalized = normalizeTabs(line);\n const connectorInfo = findConnector(normalized);\n\n if (connectorInfo) {\n const prefix = normalized.slice(0, connectorInfo.index);\n const afterConnector = normalized.slice(connectorInfo.index + connectorInfo.length);\n const prefixUnits = countPrefixUnits(prefix);\n\n // Level = number of prefix units + 1 (for the connector itself)\n const level = prefixUnits + 1;\n const name = afterConnector.trim();\n\n return { level, name };\n }\n\n // No connector found - this is likely the root or a line with only indentation\n const prefixUnits = countPrefixUnits(normalized);\n const name = normalized.replace(/^[│├└─|+\\\\\\-\\s]+/, '').trim() || normalized.trim();\n\n return { level: prefixUnits, name };\n}\n\ntype ParsedLine = { level: number; name: string };\n\n/**\n * Computes the next non-empty line's level for each line.\n * Used to determine if a node is a directory (has children).\n */\nfunction computeNextLevels(parsedLines: ParsedLine[]): (number | null)[] {\n return parsedLines.map((_, index) => {\n const nextLineWithName = parsedLines.slice(index + 1).find((line) => line.name);\n return nextLineWithName?.level ?? null;\n });\n}\n\n/**\n * Gets file extension from a filename.\n */\nfunction getExtension(filename: string): string | undefined {\n return filename.match(/\\.(\\w+)$/)?.[1];\n}\n\n/**\n * Creates a TreeNode from parsed line data.\n */\nfunction createNode(parsedLine: ParsedLine, index: number, idPrefix: string, isDirectory: boolean): TreeNode {\n return {\n id: index === 0 ? idPrefix + 'root' : idPrefix + 'node-' + index,\n name: parsedLine.name,\n type: isDirectory ? 'directory' : 'file',\n extension: isDirectory ? undefined : getExtension(parsedLine.name),\n level: parsedLine.level,\n children: isDirectory ? [] : undefined,\n };\n}\n\n/**\n * Finds the parent node for a given level by traversing the stack.\n * Returns a new stack with nodes popped until we find the correct parent.\n */\nfunction findParentStack(\n stack: Array<{ node: TreeNode; level: number }>,\n level: number\n): Array<{ node: TreeNode; level: number }> {\n if (stack.length <= 1 || stack[stack.length - 1].level < level) {\n return stack;\n }\n return findParentStack(stack.slice(0, -1), level);\n}\n\n/**\n * Builds the tree structure from parsed lines using reduce.\n */\nfunction buildTree(parsedLines: ParsedLine[], nextLevels: (number | null)[], idPrefix: string): TreeNode {\n const rootLine = parsedLines[0];\n const rootNode = createNode({ ...rootLine, level: 0 }, 0, idPrefix, true);\n\n const result = parsedLines.slice(1).reduce(\n (acc, parsedLine, idx) => {\n const index = idx + 1; // Adjust for slice(1)\n if (!parsedLine.name) return acc;\n\n const nextLevel = nextLevels[index];\n const isDirectory = nextLevel !== null && nextLevel > parsedLine.level;\n const newNode = createNode(parsedLine, index, idPrefix, isDirectory);\n\n // Find the correct parent\n const newStack = findParentStack(acc.stack, parsedLine.level);\n const parent = newStack[newStack.length - 1].node;\n\n // Add to parent's children (mutating children array is acceptable here\n // since we're building the tree and the node is not yet exposed)\n parent.children = parent.children ?? [];\n parent.children.push(newNode);\n\n // Update stack if this is a directory\n const updatedStack = isDirectory ? [...newStack, { node: newNode, level: parsedLine.level }] : newStack;\n\n return { ...acc, stack: updatedStack };\n },\n { root: rootNode, stack: [{ node: rootNode, level: 0 }] as Array<{ node: TreeNode; level: number }> }\n );\n\n return result.root;\n}\n\n/**\n * Parses the output of a `tree` command (including box-drawing characters)\n * and returns a TreeNode tree structure.\n *\n * @remarks\n * - Supports both Unicode box-drawing (│, ├, └, ─) and ASCII (|, +, \\, -) formats.\n * - Handles mixed tabs and spaces in indentation.\n * - Automatically excludes summary lines at the end (e.g., `3 directories, 5 files`).\n * - Automatically excludes empty lines.\n * - Uses the rule that only lines with children are considered directories\n * (lines without children are treated as files).\n * - Returns `null` if an exception occurs during parsing\n * (callers should handle fallback rendering).\n *\n * @param treeOutput - The complete text output from the `tree` command\n * @returns The root `TreeNode` on success, or `null` on failure or empty input\n *\n * @complexity O(n) - Processes in a single pass over n input lines\n * (with an additional reverse pass, so memory usage is O(n)).\n */\nexport function parseTreeOutput(treeOutput: string): TreeNode | null {\n // Normalize input: remove carriage returns and trim\n const raw = treeOutput.replace(/\\r/g, '').trim();\n if (!raw) return null;\n\n const lines = raw.split('\\n');\n\n // Filter out empty lines and summary lines\n const filteredLines = lines.filter((line) => {\n const trimmed = line.trim();\n return trimmed !== '' && !isSummaryLine(line);\n });\n\n if (filteredLines.length === 0) return null;\n\n const idPrefix = 'tree-' + hashSha256(raw) + '-';\n\n // Parse all lines to extract level and name\n const parsedLines = filteredLines.map(parseLine);\n\n // Pre-compute next line's level for directory detection\n const nextLevels = computeNextLevels(parsedLines);\n\n try {\n return buildTree(parsedLines, nextLevels, idPrefix);\n } catch {\n return null;\n }\n}\n\nexport default parseTreeOutput;\n"],"mappings":"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","names":["prefixUnits","name"]}
|