@hackersheet/next-document-content-components 0.1.0-alpha.15 → 0.1.0-alpha.16

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.
@@ -1,8 +1,19 @@
1
1
  import React from 'react';
2
2
 
3
- interface CodeBlockCopyButtonProps {
3
+ type CodeBlockCopyButtonProps = {
4
4
  code: string;
5
- }
5
+ };
6
+ /**
7
+ * コードブロック用のコピー ボタンコンポーネント。
8
+ *
9
+ * 与えられた code をクリップボードに書き込み、コピー完了時にアイコンをチェックに切り替えます。
10
+ *
11
+ * @param props.code コピー対象のコード文字列(Shiki 注釈が含まれている可能性あり)。
12
+ * @returns コピー用ボタンの React 要素。
13
+ *
14
+ * @remarks
15
+ * ブラウザの navigator.clipboard を使用します。ユーザーの環境によっては権限や HTTPS が必要です。
16
+ */
6
17
  declare function CodeBlockCopyButton({ code }: CodeBlockCopyButtonProps): React.JSX.Element;
7
18
 
8
19
  export { type CodeBlockCopyButtonProps, CodeBlockCopyButton as default };
@@ -34,9 +34,9 @@ __export(code_block_copy_button_exports, {
34
34
  module.exports = __toCommonJS(code_block_copy_button_exports);
35
35
  var import_react = __toESM(require("react"));
36
36
  var import_hi2 = require("react-icons/hi2");
37
+ const removeShikiCode = (code) => code.replace(/ *\/\/.*\[!code[^\]]+\]/gm, "").trim();
37
38
  function CodeBlockCopyButton({ code }) {
38
39
  const [copied, setCopied] = (0, import_react.useState)(false);
39
- const removeShikiCode = (code2) => code2.replace(/ *\/\/.*\[!code[^\]]+\]/gm, "").trim();
40
40
  const handleClick = () => {
41
41
  navigator.clipboard.writeText(removeShikiCode(code)).then(() => {
42
42
  setCopied(true);
@@ -1,8 +1,42 @@
1
- import React from 'react';
1
+ import { JSX } from 'react';
2
2
 
3
- interface CodeBlockIconProps {
3
+ /**
4
+ * CodeBlockIcon コンポーネントの props 型定義。
5
+ *
6
+ * `language` プロパティに基づいて、コードブロックに表示する代表的なアイコンを返します。
7
+ * 小文字の言語識別子(例: `"typescript"`, `"bash"`, `"json"`, `"markdown"`)を想定しています。
8
+ * 未知の値やサポート外の値は汎用のコードアイコンにフォールバックします。
9
+ */
10
+ type CodeBlockIconProps = {
11
+ /**
12
+ * アイコン選択に用いる言語識別子(小文字推奨)。例: 'typescript', 'bash', 'json'
13
+ */
4
14
  language: string;
5
- }
6
- declare function CodeBlockIcon({ language }: CodeBlockIconProps): React.JSX.Element;
15
+ };
16
+ /**
17
+ * 指定された言語に対応するアイコンを返すコンポーネント。
18
+ *
19
+ * 共通の言語識別子を `react-icons` のアイコンにマッピングします。
20
+ * 認識できない言語は汎用の括弧(コード)アイコンに置き換わります。
21
+ *
22
+ * 使用例:
23
+ *
24
+ * ```tsx
25
+ * <CodeBlockIcon language="typescript" />
26
+ * ```
27
+ *
28
+ * サポート例(非網羅):
29
+ * - 'bash' | 'sh' -> ターミナルアイコン
30
+ * - 'typescript' -> TypeScript アイコン
31
+ * - 'tsx' -> React アイコン
32
+ * - 'json' -> JSON アイコン
33
+ * - 'markdown' -> Markdown アイコン
34
+ * - 'hcl' -> Terraform アイコン
35
+ * - 'php', 'ruby', 'yaml', 'text' など
36
+ *
37
+ * @param props.language アイコン選択に用いる小文字の言語名
38
+ * @returns 選択されたアイコンを含む JSX 要素
39
+ */
40
+ declare function CodeBlockIcon({ language }: CodeBlockIconProps): JSX.Element;
7
41
 
8
42
  export { type CodeBlockIconProps, CodeBlockIcon as default };
@@ -42,6 +42,18 @@ function CodeBlockIcon({ language }) {
42
42
  case "sh":
43
43
  case "bash":
44
44
  return /* @__PURE__ */ import_react.default.createElement(import_fa6.FaTerminal, null);
45
+ case "js":
46
+ case "javascript":
47
+ return /* @__PURE__ */ import_react.default.createElement(import_si.SiJavascript, null);
48
+ case "py":
49
+ case "python":
50
+ return /* @__PURE__ */ import_react.default.createElement(import_si.SiPython, null);
51
+ case "kotlin":
52
+ return /* @__PURE__ */ import_react.default.createElement(import_si.SiKotlin, null);
53
+ case "go":
54
+ return /* @__PURE__ */ import_react.default.createElement(import_si.SiGo, null);
55
+ case "rust":
56
+ return /* @__PURE__ */ import_react.default.createElement(import_si.SiRust, null);
45
57
  case "hcl":
46
58
  return /* @__PURE__ */ import_react.default.createElement(import_si.SiTerraform, null);
47
59
  case "typescript":
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { TreeNode } from './parse-tree-output.js';
3
+
4
+ type DirectoryTreeItemProps = {
5
+ node: TreeNode;
6
+ };
7
+ /**
8
+ * 単一のツリーノード(ディレクトリまたはファイル)をレンダリングするコンポーネント。
9
+ * 再帰的に自身を呼び出して子ノードを描画します。
10
+ *
11
+ * @param props.node - 表示対象の TreeNode
12
+ */
13
+ declare function DirectoryTreeItem({ node }: DirectoryTreeItemProps): React.JSX.Element;
14
+
15
+ export { DirectoryTreeItem as default };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var directory_tree_item_exports = {};
30
+ __export(directory_tree_item_exports, {
31
+ default: () => DirectoryTreeItem
32
+ });
33
+ module.exports = __toCommonJS(directory_tree_item_exports);
34
+ var import_react = __toESM(require("react"));
35
+ var import_lu = require("react-icons/lu");
36
+ function DirectoryTreeItem({ node }) {
37
+ return /* @__PURE__ */ import_react.default.createElement("li", { key: node.id, className: `${node.type === "directory" ? "directory-tree-directory" : "directory-tree-file"}` }, /* @__PURE__ */ import_react.default.createElement("div", { className: "directory-tree-node-content" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "directory-tree-icon" }, node.type === "directory" && /* @__PURE__ */ import_react.default.createElement(import_lu.LuFolder, null), node.type === "file" && /* @__PURE__ */ import_react.default.createElement(import_lu.LuFile, null)), /* @__PURE__ */ import_react.default.createElement("div", null, node.name)), node.children && /* @__PURE__ */ import_react.default.createElement("ul", null, node.children.map((child) => /* @__PURE__ */ import_react.default.createElement(DirectoryTreeItem, { key: child.id, node: child }))));
38
+ }
@@ -1,6 +1,13 @@
1
1
  import React from 'react';
2
2
  import { DirectoryTreeComponentProps } from '@hackersheet/react-document-content';
3
3
 
4
+ /**
5
+ * code ブロック内の `tree` コマンド出力をパースしてツリーをレンダリングするコンポーネント。
6
+ * ヘッダにはツリーアイコン、ファイル名(language のコロン以降)、コピー用ボタンを表示します。
7
+ *
8
+ * @param props.code - `tree` コマンドの出力テキスト
9
+ * @param props.language - 言語/ファイル名情報(形式: "xxx:filename")
10
+ */
4
11
  declare function DirectoryTree({ code, ...props }: DirectoryTreeComponentProps): Promise<React.JSX.Element>;
5
12
 
6
13
  export { DirectoryTree as default };
@@ -31,72 +31,13 @@ __export(directory_tree_exports, {
31
31
  default: () => DirectoryTree
32
32
  });
33
33
  module.exports = __toCommonJS(directory_tree_exports);
34
- var import_crypto = require("crypto");
35
34
  var import_react = __toESM(require("react"));
36
35
  var import_lu = require("react-icons/lu");
37
36
  var import_code_block_copy_button = __toESM(require("./code-block-copy-button"));
37
+ var import_directory_tree_item = __toESM(require("./directory-tree-item"));
38
+ var import_parse_tree_output = __toESM(require("./parse-tree-output"));
38
39
  async function DirectoryTree({ code, ...props }) {
39
40
  const [, filename] = props.language.split(":");
40
- const treeNode = parseTreeOutput(code);
41
- const renderTree = (node) => {
42
- return /* @__PURE__ */ import_react.default.createElement("li", { key: node.id, className: `${node.type === "directory" ? "directory-tree-directory" : "directory-tree-file"}` }, /* @__PURE__ */ import_react.default.createElement("div", { className: "directory-tree-node-content" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "directory-tree-icon" }, node.type === "directory" && /* @__PURE__ */ import_react.default.createElement(import_lu.LuFolder, null), node.type === "file" && /* @__PURE__ */ import_react.default.createElement(import_lu.LuFile, null)), /* @__PURE__ */ import_react.default.createElement("div", null, node.name)), node.children && /* @__PURE__ */ import_react.default.createElement("ul", null, node.children.map((child) => renderTree(child))));
43
- };
44
- return /* @__PURE__ */ import_react.default.createElement("div", { className: "code-block" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "code-block-header" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(import_lu.LuFolderTree, null)), /* @__PURE__ */ import_react.default.createElement("div", { className: "code-block-filename" }, filename), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(import_code_block_copy_button.default, { code }))), /* @__PURE__ */ import_react.default.createElement("div", null, treeNode ? /* @__PURE__ */ import_react.default.createElement("ul", { className: "directory-tree" }, renderTree(treeNode)) : /* @__PURE__ */ import_react.default.createElement("pre", null, code)));
45
- }
46
- function encryptSha256(str) {
47
- const hash = (0, import_crypto.createHash)("sha256");
48
- hash.update(str);
49
- return hash.digest("hex");
50
- }
51
- function parseTreeOutput(treeOutput) {
52
- const lines = treeOutput.trim().split("\n");
53
- const idPrefix = "tree-" + encryptSha256(treeOutput.trim()) + "-";
54
- const rootNode = {
55
- id: idPrefix + "root",
56
- name: "",
57
- type: "directory",
58
- level: 0,
59
- children: []
60
- };
61
- const stack = [{ node: rootNode, level: 0 }];
62
- let rootNodeSet = false;
63
- const getExtension = (filename) => filename.match(/\.(\w+)$/)?.[1];
64
- const cleanNodeName = (name) => name.replace(/^[\s│├└─]+/, "").trim();
65
- try {
66
- lines.forEach((line, index) => {
67
- const trimmedLine = line.trimStart();
68
- const indent = (trimmedLine.match(/^[\s│├└─]+/) || [""])[0];
69
- const level = indent.length / 4;
70
- const cleanedLine = cleanNodeName(trimmedLine);
71
- if (level === 0 && !rootNodeSet) {
72
- rootNode.name = cleanedLine;
73
- rootNodeSet = true;
74
- return;
75
- }
76
- const isDirectory = !cleanedLine.includes(".");
77
- const newNode = {
78
- id: idPrefix + "node-" + index,
79
- name: cleanedLine,
80
- type: isDirectory ? "directory" : "file",
81
- extension: isDirectory ? void 0 : getExtension(cleanedLine),
82
- level,
83
- children: isDirectory ? [] : void 0
84
- };
85
- while (stack.length > 0 && stack[stack.length - 1].level >= level) {
86
- stack.pop();
87
- }
88
- if (stack[stack.length - 1] === void 0) {
89
- throw new Error("Tree parsing failed");
90
- }
91
- const parentNode = stack[stack.length - 1].node;
92
- parentNode.children = parentNode.children || [];
93
- parentNode.children.push(newNode);
94
- if (newNode.type === "directory") {
95
- stack.push({ node: newNode, level });
96
- }
97
- });
98
- return rootNode;
99
- } catch {
100
- return null;
101
- }
41
+ const treeNode = (0, import_parse_tree_output.default)(code);
42
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "code-block" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "code-block-header" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(import_lu.LuFolderTree, null)), /* @__PURE__ */ import_react.default.createElement("div", { className: "code-block-filename" }, filename), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(import_code_block_copy_button.default, { code }))), /* @__PURE__ */ import_react.default.createElement("div", null, treeNode ? /* @__PURE__ */ import_react.default.createElement("ul", { className: "directory-tree" }, /* @__PURE__ */ import_react.default.createElement(import_directory_tree_item.default, { node: treeNode })) : /* @__PURE__ */ import_react.default.createElement("pre", null, code)));
102
43
  }
@@ -0,0 +1,26 @@
1
+ type TreeNode = {
2
+ id: string;
3
+ name: string;
4
+ type: 'file' | 'directory';
5
+ extension?: string;
6
+ level: number;
7
+ children?: TreeNode[];
8
+ };
9
+ /**
10
+ * `tree` コマンドの出力(box-drawing を含むテキスト)をパースして TreeNode のツリー構造を返します。
11
+ *
12
+ * @remarks
13
+ * - 入力は Linux/macOS の `tree` コマンドの標準出力を想定しています。
14
+ * - 行先頭の箱線文字(`│`, `├`, `└`, `─`)や 4 スペースインデントを解析して階層を決定します。
15
+ * - 最後に出力されるサマリ行(例: `3 directories, 5 files`)は自動的に除外します。
16
+ * - 子要素が存在する行のみをディレクトリと見なすルールを採用しています(子要素が無ければファイル)。
17
+ * - 解析中に例外が発生した場合は `null` を返します(呼び出し元でフォールバック処理を行ってください)。
18
+ *
19
+ * @param treeOutput - `tree` コマンドで得られたテキスト全体
20
+ * @returns 解析に成功した場合はルート `TreeNode`、失敗または空入力の場合は `null`
21
+ *
22
+ * @complexity O(n) - 入力行数 n に対して一度の走査で処理します(追加で逆順集計を行うためメモリは O(n))。
23
+ */
24
+ declare function parseTreeOutput(treeOutput: string): TreeNode | null;
25
+
26
+ export { type TreeNode, parseTreeOutput as default, parseTreeOutput };
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var parse_tree_output_exports = {};
20
+ __export(parse_tree_output_exports, {
21
+ default: () => parse_tree_output_default,
22
+ parseTreeOutput: () => parseTreeOutput
23
+ });
24
+ module.exports = __toCommonJS(parse_tree_output_exports);
25
+ var import_crypto = require("crypto");
26
+ function encryptSha256(str) {
27
+ const hash = (0, import_crypto.createHash)("sha256");
28
+ hash.update(str);
29
+ return hash.digest("hex");
30
+ }
31
+ function parseTreeOutput(treeOutput) {
32
+ const raw = treeOutput.replace(/\r/g, "").trim();
33
+ if (!raw) return null;
34
+ const lines = raw.split("\n");
35
+ const filteredLines = lines.filter(
36
+ (l) => !/^\s*(?:\d+\s+directories?,\s*\d+\s+files?|\d+\s+directories?|\d+\s+files?)\s*$/i.test(l)
37
+ );
38
+ const idPrefix = "tree-" + encryptSha256(raw) + "-";
39
+ const rootNode = {
40
+ id: idPrefix + "root",
41
+ name: "",
42
+ type: "directory",
43
+ level: 0,
44
+ children: []
45
+ };
46
+ const stack = [{ node: rootNode, level: 0 }];
47
+ const getExtension = (filename) => filename.match(/\.(\w+)$/)?.[1];
48
+ const cleanNodeName = (name) => name.replace(/^[\s│├└─]+/, "").trim();
49
+ try {
50
+ const treeLineRegex = /^(?<prefix>(?:│\s{3}|\s{4})*)(?<connector>├── |└── )?(?<name>.*)$/;
51
+ const lineInfos = filteredLines.map((ln) => {
52
+ const normalized = ln.replace(/\t/g, " ");
53
+ const m = normalized.match(treeLineRegex);
54
+ if (m && m.groups) {
55
+ const prefix = m.groups["prefix"] || "";
56
+ const connector = m.groups["connector"] || "";
57
+ const name = (m.groups["name"] || "").trim();
58
+ const groupMatches = prefix.match(/(?:│\s{3}|\s{4})/g);
59
+ const level = (groupMatches ? groupMatches.length : 0) + (connector ? 1 : 0);
60
+ return { rawLine: normalized, level, cleanedName: cleanNodeName(name) };
61
+ }
62
+ return { rawLine: normalized, level: 0, cleanedName: cleanNodeName(normalized.trim()) };
63
+ });
64
+ const reduceResult = lineInfos.reduceRight(
65
+ (acc, info) => ({
66
+ lastLevel: info.rawLine.trim() ? info.level : acc.lastLevel,
67
+ nextLevels: [acc.lastLevel, ...acc.nextLevels]
68
+ }),
69
+ { lastLevel: null, nextLevels: [] }
70
+ );
71
+ const nextNonEmptyLevel = reduceResult.nextLevels;
72
+ lineInfos.forEach((info, index) => {
73
+ const level = info.level;
74
+ const cleanedLine = info.cleanedName;
75
+ if (level === 0 && rootNode.name === "") {
76
+ rootNode.name = cleanedLine;
77
+ return;
78
+ }
79
+ const nextLevelForLine = nextNonEmptyLevel[index];
80
+ const isDirectory = nextLevelForLine != null && nextLevelForLine > level;
81
+ const newNode = {
82
+ id: idPrefix + "node-" + index,
83
+ name: cleanedLine,
84
+ type: isDirectory ? "directory" : "file",
85
+ extension: isDirectory ? void 0 : getExtension(cleanedLine),
86
+ level,
87
+ children: isDirectory ? [] : void 0
88
+ };
89
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
90
+ stack.pop();
91
+ }
92
+ if (stack[stack.length - 1] === void 0) {
93
+ throw new Error("Tree parsing failed");
94
+ }
95
+ const parentNode = stack[stack.length - 1].node;
96
+ parentNode.children = parentNode.children || [];
97
+ parentNode.children.push(newNode);
98
+ if (newNode.type === "directory") {
99
+ stack.push({ node: newNode, level });
100
+ }
101
+ });
102
+ return rootNode;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ var parse_tree_output_default = parseTreeOutput;
108
+ // Annotate the CommonJS export names for ESM import in node:
109
+ 0 && (module.exports = {
110
+ parseTreeOutput
111
+ });
@@ -1,8 +1,19 @@
1
1
  import React from 'react';
2
2
 
3
- interface CodeBlockCopyButtonProps {
3
+ type CodeBlockCopyButtonProps = {
4
4
  code: string;
5
- }
5
+ };
6
+ /**
7
+ * コードブロック用のコピー ボタンコンポーネント。
8
+ *
9
+ * 与えられた code をクリップボードに書き込み、コピー完了時にアイコンをチェックに切り替えます。
10
+ *
11
+ * @param props.code コピー対象のコード文字列(Shiki 注釈が含まれている可能性あり)。
12
+ * @returns コピー用ボタンの React 要素。
13
+ *
14
+ * @remarks
15
+ * ブラウザの navigator.clipboard を使用します。ユーザーの環境によっては権限や HTTPS が必要です。
16
+ */
6
17
  declare function CodeBlockCopyButton({ code }: CodeBlockCopyButtonProps): React.JSX.Element;
7
18
 
8
19
  export { type CodeBlockCopyButtonProps, CodeBlockCopyButton as default };
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import React, { useState } from "react";
3
3
  import { HiOutlineClipboardDocumentList, HiCheck } from "react-icons/hi2";
4
+ const removeShikiCode = (code) => code.replace(/ *\/\/.*\[!code[^\]]+\]/gm, "").trim();
4
5
  function CodeBlockCopyButton({ code }) {
5
6
  const [copied, setCopied] = useState(false);
6
- const removeShikiCode = (code2) => code2.replace(/ *\/\/.*\[!code[^\]]+\]/gm, "").trim();
7
7
  const handleClick = () => {
8
8
  navigator.clipboard.writeText(removeShikiCode(code)).then(() => {
9
9
  setCopied(true);
@@ -1,8 +1,42 @@
1
- import React from 'react';
1
+ import { JSX } from 'react';
2
2
 
3
- interface CodeBlockIconProps {
3
+ /**
4
+ * CodeBlockIcon コンポーネントの props 型定義。
5
+ *
6
+ * `language` プロパティに基づいて、コードブロックに表示する代表的なアイコンを返します。
7
+ * 小文字の言語識別子(例: `"typescript"`, `"bash"`, `"json"`, `"markdown"`)を想定しています。
8
+ * 未知の値やサポート外の値は汎用のコードアイコンにフォールバックします。
9
+ */
10
+ type CodeBlockIconProps = {
11
+ /**
12
+ * アイコン選択に用いる言語識別子(小文字推奨)。例: 'typescript', 'bash', 'json'
13
+ */
4
14
  language: string;
5
- }
6
- declare function CodeBlockIcon({ language }: CodeBlockIconProps): React.JSX.Element;
15
+ };
16
+ /**
17
+ * 指定された言語に対応するアイコンを返すコンポーネント。
18
+ *
19
+ * 共通の言語識別子を `react-icons` のアイコンにマッピングします。
20
+ * 認識できない言語は汎用の括弧(コード)アイコンに置き換わります。
21
+ *
22
+ * 使用例:
23
+ *
24
+ * ```tsx
25
+ * <CodeBlockIcon language="typescript" />
26
+ * ```
27
+ *
28
+ * サポート例(非網羅):
29
+ * - 'bash' | 'sh' -> ターミナルアイコン
30
+ * - 'typescript' -> TypeScript アイコン
31
+ * - 'tsx' -> React アイコン
32
+ * - 'json' -> JSON アイコン
33
+ * - 'markdown' -> Markdown アイコン
34
+ * - 'hcl' -> Terraform アイコン
35
+ * - 'php', 'ruby', 'yaml', 'text' など
36
+ *
37
+ * @param props.language アイコン選択に用いる小文字の言語名
38
+ * @returns 選択されたアイコンを含む JSX 要素
39
+ */
40
+ declare function CodeBlockIcon({ language }: CodeBlockIconProps): JSX.Element;
7
41
 
8
42
  export { type CodeBlockIconProps, CodeBlockIcon as default };
@@ -1,7 +1,19 @@
1
1
  import React from "react";
2
2
  import { FaTerminal, FaReact } from "react-icons/fa6";
3
3
  import { HiCodeBracket } from "react-icons/hi2";
4
- import { SiTypescript, SiTerraform, SiMarkdown, SiPhp, SiRuby, SiYaml } from "react-icons/si";
4
+ import {
5
+ SiTypescript,
6
+ SiTerraform,
7
+ SiMarkdown,
8
+ SiPhp,
9
+ SiRuby,
10
+ SiYaml,
11
+ SiJavascript,
12
+ SiPython,
13
+ SiKotlin,
14
+ SiGo,
15
+ SiRust
16
+ } from "react-icons/si";
5
17
  import { TbTxt } from "react-icons/tb";
6
18
  import { VscJson } from "react-icons/vsc";
7
19
  function CodeBlockIcon({ language }) {
@@ -9,6 +21,18 @@ function CodeBlockIcon({ language }) {
9
21
  case "sh":
10
22
  case "bash":
11
23
  return /* @__PURE__ */ React.createElement(FaTerminal, null);
24
+ case "js":
25
+ case "javascript":
26
+ return /* @__PURE__ */ React.createElement(SiJavascript, null);
27
+ case "py":
28
+ case "python":
29
+ return /* @__PURE__ */ React.createElement(SiPython, null);
30
+ case "kotlin":
31
+ return /* @__PURE__ */ React.createElement(SiKotlin, null);
32
+ case "go":
33
+ return /* @__PURE__ */ React.createElement(SiGo, null);
34
+ case "rust":
35
+ return /* @__PURE__ */ React.createElement(SiRust, null);
12
36
  case "hcl":
13
37
  return /* @__PURE__ */ React.createElement(SiTerraform, null);
14
38
  case "typescript":
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { TreeNode } from './parse-tree-output.mjs';
3
+
4
+ type DirectoryTreeItemProps = {
5
+ node: TreeNode;
6
+ };
7
+ /**
8
+ * 単一のツリーノード(ディレクトリまたはファイル)をレンダリングするコンポーネント。
9
+ * 再帰的に自身を呼び出して子ノードを描画します。
10
+ *
11
+ * @param props.node - 表示対象の TreeNode
12
+ */
13
+ declare function DirectoryTreeItem({ node }: DirectoryTreeItemProps): React.JSX.Element;
14
+
15
+ export { DirectoryTreeItem as default };
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import { LuFolder, LuFile } from "react-icons/lu";
3
+ function DirectoryTreeItem({ node }) {
4
+ return /* @__PURE__ */ React.createElement("li", { key: node.id, className: `${node.type === "directory" ? "directory-tree-directory" : "directory-tree-file"}` }, /* @__PURE__ */ React.createElement("div", { className: "directory-tree-node-content" }, /* @__PURE__ */ React.createElement("div", { className: "directory-tree-icon" }, node.type === "directory" && /* @__PURE__ */ React.createElement(LuFolder, null), node.type === "file" && /* @__PURE__ */ React.createElement(LuFile, null)), /* @__PURE__ */ React.createElement("div", null, node.name)), node.children && /* @__PURE__ */ React.createElement("ul", null, node.children.map((child) => /* @__PURE__ */ React.createElement(DirectoryTreeItem, { key: child.id, node: child }))));
5
+ }
6
+ export {
7
+ DirectoryTreeItem as default
8
+ };
@@ -1,6 +1,13 @@
1
1
  import React from 'react';
2
2
  import { DirectoryTreeComponentProps } from '@hackersheet/react-document-content';
3
3
 
4
+ /**
5
+ * code ブロック内の `tree` コマンド出力をパースしてツリーをレンダリングするコンポーネント。
6
+ * ヘッダにはツリーアイコン、ファイル名(language のコロン以降)、コピー用ボタンを表示します。
7
+ *
8
+ * @param props.code - `tree` コマンドの出力テキスト
9
+ * @param props.language - 言語/ファイル名情報(形式: "xxx:filename")
10
+ */
4
11
  declare function DirectoryTree({ code, ...props }: DirectoryTreeComponentProps): Promise<React.JSX.Element>;
5
12
 
6
13
  export { DirectoryTree as default };
@@ -1,71 +1,12 @@
1
- import { createHash } from "crypto";
2
1
  import React from "react";
3
- import { LuFolder, LuFile, LuFolderTree } from "react-icons/lu";
2
+ import { LuFolderTree } from "react-icons/lu";
4
3
  import CodeBlockCopyButton from "./code-block-copy-button";
4
+ import DirectoryTreeItem from "./directory-tree-item";
5
+ import parseTreeOutput from "./parse-tree-output";
5
6
  async function DirectoryTree({ code, ...props }) {
6
7
  const [, filename] = props.language.split(":");
7
8
  const treeNode = parseTreeOutput(code);
8
- const renderTree = (node) => {
9
- return /* @__PURE__ */ React.createElement("li", { key: node.id, className: `${node.type === "directory" ? "directory-tree-directory" : "directory-tree-file"}` }, /* @__PURE__ */ React.createElement("div", { className: "directory-tree-node-content" }, /* @__PURE__ */ React.createElement("div", { className: "directory-tree-icon" }, node.type === "directory" && /* @__PURE__ */ React.createElement(LuFolder, null), node.type === "file" && /* @__PURE__ */ React.createElement(LuFile, null)), /* @__PURE__ */ React.createElement("div", null, node.name)), node.children && /* @__PURE__ */ React.createElement("ul", null, node.children.map((child) => renderTree(child))));
10
- };
11
- return /* @__PURE__ */ React.createElement("div", { className: "code-block" }, /* @__PURE__ */ React.createElement("div", { className: "code-block-header" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(LuFolderTree, null)), /* @__PURE__ */ React.createElement("div", { className: "code-block-filename" }, filename), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(CodeBlockCopyButton, { code }))), /* @__PURE__ */ React.createElement("div", null, treeNode ? /* @__PURE__ */ React.createElement("ul", { className: "directory-tree" }, renderTree(treeNode)) : /* @__PURE__ */ React.createElement("pre", null, code)));
12
- }
13
- function encryptSha256(str) {
14
- const hash = createHash("sha256");
15
- hash.update(str);
16
- return hash.digest("hex");
17
- }
18
- function parseTreeOutput(treeOutput) {
19
- const lines = treeOutput.trim().split("\n");
20
- const idPrefix = "tree-" + encryptSha256(treeOutput.trim()) + "-";
21
- const rootNode = {
22
- id: idPrefix + "root",
23
- name: "",
24
- type: "directory",
25
- level: 0,
26
- children: []
27
- };
28
- const stack = [{ node: rootNode, level: 0 }];
29
- let rootNodeSet = false;
30
- const getExtension = (filename) => filename.match(/\.(\w+)$/)?.[1];
31
- const cleanNodeName = (name) => name.replace(/^[\s│├└─]+/, "").trim();
32
- try {
33
- lines.forEach((line, index) => {
34
- const trimmedLine = line.trimStart();
35
- const indent = (trimmedLine.match(/^[\s│├└─]+/) || [""])[0];
36
- const level = indent.length / 4;
37
- const cleanedLine = cleanNodeName(trimmedLine);
38
- if (level === 0 && !rootNodeSet) {
39
- rootNode.name = cleanedLine;
40
- rootNodeSet = true;
41
- return;
42
- }
43
- const isDirectory = !cleanedLine.includes(".");
44
- const newNode = {
45
- id: idPrefix + "node-" + index,
46
- name: cleanedLine,
47
- type: isDirectory ? "directory" : "file",
48
- extension: isDirectory ? void 0 : getExtension(cleanedLine),
49
- level,
50
- children: isDirectory ? [] : void 0
51
- };
52
- while (stack.length > 0 && stack[stack.length - 1].level >= level) {
53
- stack.pop();
54
- }
55
- if (stack[stack.length - 1] === void 0) {
56
- throw new Error("Tree parsing failed");
57
- }
58
- const parentNode = stack[stack.length - 1].node;
59
- parentNode.children = parentNode.children || [];
60
- parentNode.children.push(newNode);
61
- if (newNode.type === "directory") {
62
- stack.push({ node: newNode, level });
63
- }
64
- });
65
- return rootNode;
66
- } catch {
67
- return null;
68
- }
9
+ return /* @__PURE__ */ React.createElement("div", { className: "code-block" }, /* @__PURE__ */ React.createElement("div", { className: "code-block-header" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(LuFolderTree, null)), /* @__PURE__ */ React.createElement("div", { className: "code-block-filename" }, filename), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(CodeBlockCopyButton, { code }))), /* @__PURE__ */ React.createElement("div", null, treeNode ? /* @__PURE__ */ React.createElement("ul", { className: "directory-tree" }, /* @__PURE__ */ React.createElement(DirectoryTreeItem, { node: treeNode })) : /* @__PURE__ */ React.createElement("pre", null, code)));
69
10
  }
70
11
  export {
71
12
  DirectoryTree as default
@@ -0,0 +1,26 @@
1
+ type TreeNode = {
2
+ id: string;
3
+ name: string;
4
+ type: 'file' | 'directory';
5
+ extension?: string;
6
+ level: number;
7
+ children?: TreeNode[];
8
+ };
9
+ /**
10
+ * `tree` コマンドの出力(box-drawing を含むテキスト)をパースして TreeNode のツリー構造を返します。
11
+ *
12
+ * @remarks
13
+ * - 入力は Linux/macOS の `tree` コマンドの標準出力を想定しています。
14
+ * - 行先頭の箱線文字(`│`, `├`, `└`, `─`)や 4 スペースインデントを解析して階層を決定します。
15
+ * - 最後に出力されるサマリ行(例: `3 directories, 5 files`)は自動的に除外します。
16
+ * - 子要素が存在する行のみをディレクトリと見なすルールを採用しています(子要素が無ければファイル)。
17
+ * - 解析中に例外が発生した場合は `null` を返します(呼び出し元でフォールバック処理を行ってください)。
18
+ *
19
+ * @param treeOutput - `tree` コマンドで得られたテキスト全体
20
+ * @returns 解析に成功した場合はルート `TreeNode`、失敗または空入力の場合は `null`
21
+ *
22
+ * @complexity O(n) - 入力行数 n に対して一度の走査で処理します(追加で逆順集計を行うためメモリは O(n))。
23
+ */
24
+ declare function parseTreeOutput(treeOutput: string): TreeNode | null;
25
+
26
+ export { type TreeNode, parseTreeOutput as default, parseTreeOutput };
@@ -0,0 +1,87 @@
1
+ import { createHash } from "crypto";
2
+ function encryptSha256(str) {
3
+ const hash = createHash("sha256");
4
+ hash.update(str);
5
+ return hash.digest("hex");
6
+ }
7
+ function parseTreeOutput(treeOutput) {
8
+ const raw = treeOutput.replace(/\r/g, "").trim();
9
+ if (!raw) return null;
10
+ const lines = raw.split("\n");
11
+ const filteredLines = lines.filter(
12
+ (l) => !/^\s*(?:\d+\s+directories?,\s*\d+\s+files?|\d+\s+directories?|\d+\s+files?)\s*$/i.test(l)
13
+ );
14
+ const idPrefix = "tree-" + encryptSha256(raw) + "-";
15
+ const rootNode = {
16
+ id: idPrefix + "root",
17
+ name: "",
18
+ type: "directory",
19
+ level: 0,
20
+ children: []
21
+ };
22
+ const stack = [{ node: rootNode, level: 0 }];
23
+ const getExtension = (filename) => filename.match(/\.(\w+)$/)?.[1];
24
+ const cleanNodeName = (name) => name.replace(/^[\s│├└─]+/, "").trim();
25
+ try {
26
+ const treeLineRegex = /^(?<prefix>(?:│\s{3}|\s{4})*)(?<connector>├── |└── )?(?<name>.*)$/;
27
+ const lineInfos = filteredLines.map((ln) => {
28
+ const normalized = ln.replace(/\t/g, " ");
29
+ const m = normalized.match(treeLineRegex);
30
+ if (m && m.groups) {
31
+ const prefix = m.groups["prefix"] || "";
32
+ const connector = m.groups["connector"] || "";
33
+ const name = (m.groups["name"] || "").trim();
34
+ const groupMatches = prefix.match(/(?:│\s{3}|\s{4})/g);
35
+ const level = (groupMatches ? groupMatches.length : 0) + (connector ? 1 : 0);
36
+ return { rawLine: normalized, level, cleanedName: cleanNodeName(name) };
37
+ }
38
+ return { rawLine: normalized, level: 0, cleanedName: cleanNodeName(normalized.trim()) };
39
+ });
40
+ const reduceResult = lineInfos.reduceRight(
41
+ (acc, info) => ({
42
+ lastLevel: info.rawLine.trim() ? info.level : acc.lastLevel,
43
+ nextLevels: [acc.lastLevel, ...acc.nextLevels]
44
+ }),
45
+ { lastLevel: null, nextLevels: [] }
46
+ );
47
+ const nextNonEmptyLevel = reduceResult.nextLevels;
48
+ lineInfos.forEach((info, index) => {
49
+ const level = info.level;
50
+ const cleanedLine = info.cleanedName;
51
+ if (level === 0 && rootNode.name === "") {
52
+ rootNode.name = cleanedLine;
53
+ return;
54
+ }
55
+ const nextLevelForLine = nextNonEmptyLevel[index];
56
+ const isDirectory = nextLevelForLine != null && nextLevelForLine > level;
57
+ const newNode = {
58
+ id: idPrefix + "node-" + index,
59
+ name: cleanedLine,
60
+ type: isDirectory ? "directory" : "file",
61
+ extension: isDirectory ? void 0 : getExtension(cleanedLine),
62
+ level,
63
+ children: isDirectory ? [] : void 0
64
+ };
65
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
66
+ stack.pop();
67
+ }
68
+ if (stack[stack.length - 1] === void 0) {
69
+ throw new Error("Tree parsing failed");
70
+ }
71
+ const parentNode = stack[stack.length - 1].node;
72
+ parentNode.children = parentNode.children || [];
73
+ parentNode.children.push(newNode);
74
+ if (newNode.type === "directory") {
75
+ stack.push({ node: newNode, level });
76
+ }
77
+ });
78
+ return rootNode;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ var parse_tree_output_default = parseTreeOutput;
84
+ export {
85
+ parse_tree_output_default as default,
86
+ parseTreeOutput
87
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackersheet/next-document-content-components",
3
- "version": "0.1.0-alpha.15",
3
+ "version": "0.1.0-alpha.16",
4
4
  "description": "Hacker Sheet document content components for Next.js",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "react-tweet": "^3.2.2",
35
35
  "shiki": "^3.13.0",
36
36
  "@hackersheet/core": "0.1.0-alpha.11",
37
- "@hackersheet/react-document-content": "0.1.0-alpha.12"
37
+ "@hackersheet/react-document-content": "0.1.0-alpha.13"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/react": "^19.2.0",