@longzai-intelligence-git/gitignore-formatter 0.0.1

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,169 @@
1
+ import { GitignoreFormatOptions, GitignoreGroup, LziGitConfig } from "@longzai-intelligence-git/config";
2
+ //#region src/classify.d.ts
3
+ /**
4
+ * .gitignore 行分类工具。
5
+ *
6
+ * 把一行 .gitignore 内容判定为:空行、注释行、模式行;并把模式行匹配到配置分组。
7
+ * 判定遵循 git 官方 gitignore 规范,严格区分转义字符(`\#`/`\!`)与控制字符(`#`/`!`)。
8
+ */
9
+ /**
10
+ * 判断一行是否为空行(仅含空白或完全为空)。
11
+ *
12
+ * @param line - 单行内容(不含行结束符)
13
+ * @returns 是否为空行
14
+ */
15
+ declare function isBlankLine(line: string): boolean;
16
+ /**
17
+ * 判断一行是否为注释行。
18
+ *
19
+ * 按 git 规范:行首(可选前置空白后)第一个字符为未转义的 `#` 即注释。
20
+ * `\#foo` 是转义字面量 `#`,不算注释。
21
+ *
22
+ * @param line - 单行内容
23
+ * @returns 是否为注释行
24
+ */
25
+ declare function isCommentLine(line: string): boolean;
26
+ /**
27
+ * 判断模式行是否为 negation(取反,以未转义的 `!` 开头)。
28
+ *
29
+ * `\!important!.txt` 是转义字面量 `!`,不是 negation。
30
+ *
31
+ * @param pattern - 已确认为模式行的内容(非空、非注释)
32
+ * @returns 是否为 negation 模式
33
+ */
34
+ declare function isNegationPattern(pattern: string): boolean;
35
+ /**
36
+ * 取 negation 模式的实际模式体(去掉前导 `!`)。
37
+ *
38
+ * 仅在 isNegationPattern 为 true 时调用。
39
+ *
40
+ * @param pattern - negation 模式
41
+ * @returns 去掉 `!` 后的模式体
42
+ */
43
+ declare function getNegationBody(pattern: string): string;
44
+ /**
45
+ * 判断两个模式是否应归入同一分组(分类等价)。
46
+ *
47
+ * 分类等价比"语义相等"更宽容:对目录型模式,`foo` 与 `foo/` 在分类时视为同类
48
+ * (gitignore 语义上 `foo` ⊇ `foo/`,归入同组不改变它们各自的匹配行为,文件输出仍保持原写法)。
49
+ * 这让预设里写 `node_modules/` 也能匹配到仓库里写成 `node_modules` 的项,反之亦然。
50
+ *
51
+ * 不影响:negation 前缀(`!foo` 与 `foo` 不等价)、转义字面量。
52
+ *
53
+ * @param a - 模式 A
54
+ * @param b - 模式 B
55
+ * @returns 是否分类等价
56
+ */
57
+ declare function classifyEquivalent(a: string, b: string): boolean;
58
+ /**
59
+ * 把一个模式匹配到配置分组。
60
+ *
61
+ * 匹配规则:模式与某 group.patterns 中的某一项"分类等价"(精确相等或仅尾斜杠差异),
62
+ * 即归入该组。遍历顺序按 groups 数组顺序,首个命中即返回(避免一个模式归入多个组)。
63
+ *
64
+ * 注意:group.patterns 本身可能含 negation(如 `!.env.example`),匹配时按字面比较其分类等价性。
65
+ *
66
+ * @param pattern - 待分类的模式(应为已归一化的模式行)
67
+ * @param groups - 有序分组定义
68
+ * @returns 命中分组的索引;未命中返回 null
69
+ */
70
+ declare function classifyPattern(pattern: string, groups: readonly GitignoreGroup[]): number | null;
71
+ //#endregion
72
+ //#region src/normalize.d.ts
73
+ /**
74
+ * .gitignore 语义安全归一化引擎。
75
+ *
76
+ * 归一化只做"不改变匹配语义"的变换,严格遵循 git 官方 gitignore 规范。
77
+ *
78
+ * 强制处理顺序(见 normalizeGitignore):
79
+ * 1. 行结束符统一为 \n
80
+ * 2. 去未转义尾空白(保留 `\ ` 转义尾空格)
81
+ * 3. 去重(归一化后比较)
82
+ * 4. 合并连续空行→1,去首尾空行
83
+ * 5. 结尾保证单个 \n
84
+ *
85
+ * 绝不改动(否则改变匹配语义):
86
+ * - 前导 `/`(锚定到 .gitignore 所在目录)
87
+ * - 尾随 `/`(仅匹配目录)
88
+ * - `!`(negation)与 `\!`(转义字面量 !)
89
+ * - `#`(注释)与 `\#`(转义字面量 #)
90
+ * - `**`(通配语义)
91
+ */
92
+ /**
93
+ * 去掉模式行尾部的未转义空白(空格/制表符)。
94
+ *
95
+ * 按 git 规范,尾部空白被忽略,**除非**被反斜杠转义(如 `foo\ ` 保留一个字面尾空格)。
96
+ * 实现逻辑:从右向左扫描,删除连续的空格/制表符,但遇到 `\ `(反斜杠+空格)时停止——
97
+ * 该转义序列表示一个字面尾空格,必须保留。
98
+ *
99
+ * 注释行原样返回(注释尾空白无语义意义,但保留以免改动用户文档注释排版)。
100
+ *
101
+ * @param line - 单行内容(不含行结束符)
102
+ * @returns 去尾空白后的行
103
+ */
104
+ declare function stripTrailingWhitespace(line: string): string;
105
+ /**
106
+ * .gitignore 文件级归一化(不做分组编排)。
107
+ *
108
+ * 应用强制处理顺序的 5 步归一化,返回以单个 `\n` 结尾的规范字符串。
109
+ * 输入为空或全空白时返回空字符串。
110
+ *
111
+ * @param content - 原始 .gitignore 文件内容
112
+ * @param dedupe - 是否去重模式行(默认 true)
113
+ * @returns 归一化后的内容
114
+ */
115
+ declare function normalizeGitignore(content: string, dedupe?: boolean): string;
116
+ //#endregion
117
+ //#region src/format.d.ts
118
+ /**
119
+ * .gitignore 格式化引擎(presets + inject + 分组编排)。
120
+ *
121
+ * 流程:
122
+ * 1. 归一化现有内容 → 现有模式集合 P
123
+ * 2. 解析启用的 preset → 期望模式集合 S + preset 分组定义
124
+ * 3. inject:S 中不在 P 的模式 → 补充进对应 preset 分组(只增不删)
125
+ * 4. 合并 P ∪ S,按"preset 分组 → 自定义 groups → misc"顺序编排输出
126
+ *
127
+ * 输出约定:
128
+ * - 分组头自动补 `# ` 前缀(comment 为纯文本,不含 #)
129
+ * - 组间按 blankLinesBetweenGroups 插入空行
130
+ * - 未匹配任何分组的模式归入 misc 组(尾部)
131
+ */
132
+ /**
133
+ * 解析用户 Partial 选项,未提供字段回退到 DEFAULT_GITIGNORE_OPTIONS。
134
+ *
135
+ * @param partial - 用户在 config 中提供的 Partial 选项
136
+ * @returns 完整的格式化选项
137
+ */
138
+ declare function resolveOptions(partial?: Partial<GitignoreFormatOptions>): GitignoreFormatOptions;
139
+ /**
140
+ * 把分组标题格式化为注释行(自动补 `# ` 前缀)。
141
+ *
142
+ * 若 comment 已含 `#` 前缀则不重复补(兼容用户误写)。
143
+ *
144
+ * @param comment - 纯文本标题
145
+ * @returns `# <comment>` 形式的注释行
146
+ */
147
+ declare function formatGroupHeader(comment: string): string;
148
+ /**
149
+ * 从 config 解析启用的 preset,按 GITIGNORE_PRESETS 表顺序返回有序分组定义。
150
+ *
151
+ * 每个 preset 启用(用户覆盖为 true 或未列出且 default=true)时,生成一个内置分组。
152
+ *
153
+ * @param presets - 合并后的 presets 开关字典
154
+ * @returns 有序的 preset 分组定义数组
155
+ */
156
+ declare function resolveEnabledPresets(presets: Record<string, boolean> | undefined): GitignoreGroup[];
157
+ /**
158
+ * .gitignore 格式化入口。
159
+ *
160
+ * 根据配置决定是按 preset+groups 重组(含 inject)还是保留原分组,统一应用归一化规则。
161
+ * 缺失 config.gitignore 时退化为纯归一化(无分组、无 inject)。
162
+ *
163
+ * @param content - 原始 .gitignore 内容
164
+ * @param config - lzi-git.config.ts 解析得到的配置(可为 undefined,表示无配置文件)
165
+ * @returns 格式化后的内容(以单个 \n 结尾)
166
+ */
167
+ declare function formatGitignore(content: string, config?: LziGitConfig): string;
168
+ //#endregion
169
+ export { classifyEquivalent, classifyPattern, formatGitignore, formatGroupHeader, getNegationBody, isBlankLine, isCommentLine, isNegationPattern, normalizeGitignore, resolveEnabledPresets, resolveOptions, stripTrailingWhitespace };
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import{DEFAULT_GITIGNORE_OPTIONS as e,GITIGNORE_PRESETS as t}from"@longzai-intelligence-git/config";function n(e){return e.trim().length===0}function r(e){let t=e.trimStart();return t.length!==0&&t[0]===`#`}function i(e){let t=e.trimStart();return t.length!==0&&t[0]===`!`}function a(e){return e.trimStart().slice(1)}function o(e,t){return e===t||(e.endsWith(`/`)?e.slice(0,-1):e)===(t.endsWith(`/`)?t.slice(0,-1):t)}function s(e,t){let n=e;for(let e=0;e<t.length;e+=1){let r=t[e];if(r!==void 0&&r.patterns.some(e=>o(n,e)))return e}return null}function c(e){if(r(e))return e;let t=e.length;for(;t>0;){let n=e[t-1];if(n===void 0)break;if(n===` `||n===` `){if(e[t-2]===`\\`)break;--t}else break}return e.slice(0,t)}function l(e){return e.replace(/\r\n/g,`
2
+ `).replace(/\r/g,`
3
+ `).split(`
4
+ `).map(e=>c(e))}function u(e,t){if(!t)return e;let i=new Set,a=[];for(let t of e){if(n(t)||r(t)){a.push(t);continue}i.has(t)||(i.add(t),a.push(t))}return a}function d(e){let t=[];for(let r of e){if(n(r)){t.length>0&&!n(t[t.length-1]??``)&&t.push(``);continue}t.push(r)}for(;t.length>0&&n(t[t.length-1]??``);)t.pop();return t}function f(e,t=!0){let n=d(u(l(e),t));return n.length===0?``:`${n.join(`
5
+ `)}\n`}function p(t){return{...e,...t}}function m(e,t){return e<t?-1:+(e>t)}function h(e){let t=f(e,!0);return t===``?[]:t.split(`
6
+ `).filter(e=>!n(e)&&!r(e))}function g(e){let t=e.trim();return t.startsWith(`#`)?t:`# ${t}`}function _(e){if(e===void 0)return[];let n=Object.keys(t),r=[];for(let i of n){let n=t[i];n!==void 0&&(e[i]??n.default)&&r.push({comment:n.comment,patterns:[...n.patterns]})}return r}function v(e,t){return t===void 0||t.length===0?e:[...e,...t]}function y(e,t,n){let r=`
7
+ `.repeat(Math.max(0,n.blankLinesBetweenGroups)+1),i=[],a=new Set;for(let r of t){if(r===void 0)continue;let t=[];for(let n of e)a.has(n)||r.patterns.some(e=>o(n,e))&&(t.push(n),a.add(n));for(let n of r.patterns)e.some(e=>o(e,n))||t.push(n);n.sortWithinGroup&&t.sort(m);let s=g(r.comment);i.push(t.length>0?[s,...t].join(`
8
+ `):s)}let s=e.filter(e=>!a.has(e));return s.length>0&&(n.sortWithinGroup&&s.sort(m),i.push([g(n.miscGroupComment),...s].join(`
9
+ `))),i.length===0?``:`${i.join(r)}\n`}function b(e,t){let i=f(e,t.dedupe);if(i===``)return``;let a=i.split(`
10
+ `),o=[],s={header:null,patterns:[]},c=!1;for(let e of a)n(e)||(r(e)?(c&&o.push(s),s={header:e,patterns:[]},c=!0):(s.patterns.push(e),c=!0));if(c&&o.push(s),t.sortWithinGroup)for(let e of o)e.patterns.sort(m);let l=`
11
+ `.repeat(Math.max(0,t.blankLinesBetweenGroups)+1),u=o.map(e=>{let t=[];return e.header!==null&&t.push(e.header),t.push(...e.patterns),t.join(`
12
+ `)}).filter(e=>e.length>0);return u.length===0?``:`${u.join(l)}\n`}function x(e,t){let n=t?.gitignore;if(n===void 0)return f(e,!0);let r=p(n.options);return r.groupByConfig?y(h(e),v(_(n.presets),n.groups),r):b(e,r)}export{o as classifyEquivalent,s as classifyPattern,x as formatGitignore,g as formatGroupHeader,a as getNegationBody,n as isBlankLine,r as isCommentLine,i as isNegationPattern,f as normalizeGitignore,_ as resolveEnabledPresets,p as resolveOptions,c as stripTrailingWhitespace};
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@longzai-intelligence-git/gitignore-formatter",
3
+ "version": "0.0.1",
4
+ "description": "Longzai Intelligence Git - .gitignore 格式化业务逻辑",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "NODE_ENV=production lzi-builder",
19
+ "build:prod": "NODE_ENV=production lzi-builder",
20
+ "prepublishOnly": "bun run build:prod",
21
+ "typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
22
+ "typecheck:app": "lzi-tsgo typecheck tsconfig/app.json",
23
+ "typecheck:node": "lzi-tsgo typecheck tsconfig/node.json",
24
+ "typecheck:test": "lzi-tsgo typecheck tsconfig/test.json",
25
+ "lint": "oxlint && oxfmt --check",
26
+ "lint:fix": "oxlint --fix && oxfmt",
27
+ "test": "bun test",
28
+ "test:watch": "bun test --watch",
29
+ "test:coverage": "bun test --coverage",
30
+ "clean": "lzi-dev-cli clean"
31
+ },
32
+ "dependencies": {
33
+ "@longzai-intelligence-git/config": "0.0.1"
34
+ },
35
+ "devDependencies": {},
36
+ "peerDependencies": {}
37
+ }