From 398dcd0f188de0fb3bb911ad028da01ed21dbda9 Mon Sep 17 00:00:00 2001 From: tk Date: Thu, 14 Dec 2023 11:23:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=202.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .commitlintrc.js | 72 +++ .github/workflows/ci.yml | 50 ++ .template.config/template.json | 30 + NSExt.sln | 2 +- README.md | 133 ++-- README.zh-CN.md | 131 ++-- assets/logo.png | Bin 0 -> 1482 bytes assets/res/Statements.ln | 0 build/code.quality.props | 47 ++ build/copy.pkg.xml.comment.files.targets | 10 + build/minver.targets | 11 + build/prebuild.targets | 24 + build/stylecop.analyzers.ruleset | 243 +++++++ docs/CONTRIBUTING.md | 182 ++++++ docs/SEMVER.md | 226 +++++++ key.snk | Bin 0 -> 596 bytes package.json | 14 + scripts/clean.ln.csx | 26 + scripts/code.clean.csx | 44 ++ scripts/code.clean.ps1 | 4 + scripts/gen.cs.tt | 77 +++ scripts/gen.ln.cmd | 2 + scripts/gen.resx.tt | 39 ++ scripts/git.pr.ps1 | 11 + scripts/git.rc.ps1 | 6 + scripts/image.optimize.csx | 65 ++ scripts/install.as.tpl.ps1 | 2 + scripts/rename.csx | 27 + scripts/resharper.full.ps1 | 1 + scripts/switcher.nsext.json | 17 + scripts/switcher.ps1 | 27 + scripts/sync.sln.files.csx | 63 ++ src/backend/GlobalUsings.cs | 6 + src/backend/NSExt.Tests/NSExt.Tests.csproj | 14 + .../NSExt/Attributes/LocalizationAttribute.cs | 26 + .../ResourceDescriptionAttribute.cs | 28 + src/backend/NSExt/Constant/Regexes.cs | 21 + .../NSExt/Extensions/ByteExtensions.cs | 61 ++ .../NSExt/Extensions/CharExtensions.cs | 23 + .../NSExt/Extensions/DateTimeExtensions.cs | 110 ++++ .../NSExt/Extensions/DbCommandExtensions.cs | 39 ++ .../NSExt/Extensions/DecimalExtensions.cs | 26 + .../NSExt/Extensions/EnumExtensions.cs | 41 ++ .../NSExt/Extensions/EnumerableExtensions.cs | 26 + .../NSExt/Extensions/GenericExtensions.cs | 41 ++ src/backend/NSExt/Extensions/IntExtensions.cs | 41 ++ .../NSExt/Extensions/LoggerExtensions.cs | 86 +++ .../NSExt/Extensions/LongExtensions.cs | 42 ++ .../NSExt/Extensions/ObjectExtensions.cs | 30 + .../NSExt/Extensions/StreamExtensions.cs | 32 + .../NSExt/Extensions/StringExtensions.cs | 594 ++++++++++++++++++ .../NSExt/Extensions/TypeExtensions.cs | 19 + src/backend/NSExt/Extensions/UriExtensions.cs | 15 + src/backend/NSExt/NSExt.csproj | 31 + 54 files changed, 2775 insertions(+), 163 deletions(-) create mode 100644 .commitlintrc.js create mode 100644 .github/workflows/ci.yml create mode 100644 .template.config/template.json create mode 100644 assets/logo.png create mode 100644 assets/res/Statements.ln create mode 100644 build/code.quality.props create mode 100644 build/copy.pkg.xml.comment.files.targets create mode 100644 build/minver.targets create mode 100644 build/prebuild.targets create mode 100644 build/stylecop.analyzers.ruleset create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/SEMVER.md create mode 100644 key.snk create mode 100644 package.json create mode 100644 scripts/clean.ln.csx create mode 100644 scripts/code.clean.csx create mode 100644 scripts/code.clean.ps1 create mode 100644 scripts/gen.cs.tt create mode 100644 scripts/gen.ln.cmd create mode 100644 scripts/gen.resx.tt create mode 100644 scripts/git.pr.ps1 create mode 100644 scripts/git.rc.ps1 create mode 100644 scripts/image.optimize.csx create mode 100644 scripts/install.as.tpl.ps1 create mode 100644 scripts/rename.csx create mode 100644 scripts/resharper.full.ps1 create mode 100644 scripts/switcher.nsext.json create mode 100644 scripts/switcher.ps1 create mode 100644 scripts/sync.sln.files.csx create mode 100644 src/backend/GlobalUsings.cs create mode 100644 src/backend/NSExt.Tests/NSExt.Tests.csproj create mode 100644 src/backend/NSExt/Attributes/LocalizationAttribute.cs create mode 100644 src/backend/NSExt/Attributes/ResourceDescriptionAttribute.cs create mode 100644 src/backend/NSExt/Constant/Regexes.cs create mode 100644 src/backend/NSExt/Extensions/ByteExtensions.cs create mode 100644 src/backend/NSExt/Extensions/CharExtensions.cs create mode 100644 src/backend/NSExt/Extensions/DateTimeExtensions.cs create mode 100644 src/backend/NSExt/Extensions/DbCommandExtensions.cs create mode 100644 src/backend/NSExt/Extensions/DecimalExtensions.cs create mode 100644 src/backend/NSExt/Extensions/EnumExtensions.cs create mode 100644 src/backend/NSExt/Extensions/EnumerableExtensions.cs create mode 100644 src/backend/NSExt/Extensions/GenericExtensions.cs create mode 100644 src/backend/NSExt/Extensions/IntExtensions.cs create mode 100644 src/backend/NSExt/Extensions/LoggerExtensions.cs create mode 100644 src/backend/NSExt/Extensions/LongExtensions.cs create mode 100644 src/backend/NSExt/Extensions/ObjectExtensions.cs create mode 100644 src/backend/NSExt/Extensions/StreamExtensions.cs create mode 100644 src/backend/NSExt/Extensions/StringExtensions.cs create mode 100644 src/backend/NSExt/Extensions/TypeExtensions.cs create mode 100644 src/backend/NSExt/Extensions/UriExtensions.cs create mode 100644 src/backend/NSExt/NSExt.csproj diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..fb715c8 --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1,72 @@ +// .commitlintrc.js +/** @type {import('cz-git').UserConfig} */ +module.exports = { + rules: { + // @see: https://commitlint.js.org/#/reference-rules + }, + prompt: { + alias: { fd: 'docs: fix typos' }, + messages: { + type: '选择你要提交的类型 :', + scope: '选择一个提交范围(可选):', + customScope: '请输入自定义的提交范围 :', + subject: '填写简短精炼的变更描述 :\n', + body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n', + breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n', + footerPrefixesSelect: '选择关联issue前缀(可选):', + customFooterPrefix: '输入自定义issue前缀 :', + footer: '列举关联issue (可选) 例如: #31, #I3244 :\n', + confirmCommit: '是否提交或修改commit ?' + }, + types: [ + { value: 'wip', name: 'wip: 开发之中 | In development ', emoji:'🧠' }, + { value: 'feat', name: 'feat: 新增功能 | A new feature', emoji:'✨' }, + { value: 'fix', name: 'fix: 修复缺陷 | A bug fix', emoji:'🐛' }, + { value: 'docs', name: 'docs: 文档更新 | Documentation only changes', emoji:'📝' }, + { value: 'style', name: 'style: 代码格式 | Changes that do not affect the meaning of the code', emoji:'💄' }, + { value: 'refactor', name: 'refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature', emoji:'♻️' }, + { value: 'perf', name: 'perf: 性能提升 | A code change that improves performance', emoji:'⚡' }, + { value: 'test', name: 'test: 测试相关 | Adding missing tests or correcting existing tests', emoji:'✅' }, + { value: 'build', name: 'build: 构建相关 | Changes that affect the build system or external dependencies', emoji:'📦' }, + { value: 'ci', name: 'ci: 持续集成 | Changes to our CI configuration files and scripts', emoji:'🎡' }, + { value: 'revert', name: 'revert: 回退代码 | Revert to a commit', emoji:'⏪' }, + { value: 'chore', name: 'chore: 其他修改 | Other changes that do not modify src or test files', emoji:'🔨' }, + ], + useEmoji: true, + emojiAlign: 'center', + useAI: false, + aiNumber: 1, + themeColorCode: '', + scopes: [], + allowCustomScopes: true, + allowEmptyScopes: true, + customScopesAlign: 'bottom', + customScopesAlias: 'custom', + emptyScopesAlias: 'empty', + upperCaseSubject: false, + markBreakingChangeMode: false, + allowBreakingChanges: ['feat', 'fix'], + breaklineNumber: 100, + breaklineChar: '|', + skipQuestions: [], + issuePrefixes: [ + // 如果使用 gitee 作为开发管理 + { value: 'link', name: 'link: 链接 ISSUES 进行中' }, + { value: 'closed', name: 'closed: 标记 ISSUES 已完成' } + ], + customIssuePrefixAlign: 'top', + emptyIssuePrefixAlias: 'skip', + customIssuePrefixAlias: 'custom', + allowCustomIssuePrefix: true, + allowEmptyIssuePrefix: true, + confirmColorize: true, + maxHeaderLength: Infinity, + maxSubjectLength: Infinity, + minSubjectLength: 0, + scopeOverrides: undefined, + defaultBody: '', + defaultIssues: '', + defaultScope: '', + defaultSubject: '' + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2ce89e1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI +on: + push: + tags: + - v* +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + filter: tree:0 + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x + - uses: actions/cache@v3 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: ${{ runner.os }}-nuget + - working-directory: ./src/backend/NSExt + run: dotnet publish NSExt.csproj -f net8.0-windows -r win-x64 -c Release + - working-directory: ./src/backend/NSExt + run: dotnet publish NSExt.csproj -f net8.0 -r linux-x64 -c Release + - uses: actions/create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + - uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./dist/backend/NSExt/bin/Release/net8.0-windows/win-x64/publish/dot.exe + asset_name: dot-win-x64.exe + asset_content_type: application/octet-stream + - uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./dist/backend/NSExt/bin/Release/net8.0/linux-x64/publish/dot + asset_name: dot-linux-x64 + asset_content_type: application/octet-stream \ No newline at end of file diff --git a/.template.config/template.json b/.template.config/template.json new file mode 100644 index 0000000..f709711 --- /dev/null +++ b/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "author": "nsnail", //必须 + "classifications": [ + "Web/WebAPI" + ], //必须,这个对应模板的Tags + "name": "NSExt", //必须,这个对应模板的Templates + "identity": "NSExt", //可选,模板的唯一名称 + "shortName": "lop", //必须,这个对应模板的Short Name + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "NSExt", // 可选,要替换的名字 + "preferNameDirectory": true, // 可选,添加目录 + "sources": [ + { + "modifiers": [ + { + "exclude": [ + "**/.vs/**", + "**/.idea/**", + "**/.git/**", + "**/dist/**", + "**/node_modules/**" + ] + } + ] + } + ], +} \ No newline at end of file diff --git a/NSExt.sln b/NSExt.sln index bd5a9d0..171567a 100644 --- a/NSExt.sln +++ b/NSExt.sln @@ -13,10 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{5198A03D-0 .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore - CHANGELOG.md = CHANGELOG.md Directory.Build.props = Directory.Build.props dotnet-tools.json = dotnet-tools.json global.json = global.json + key.snk = key.snk LICENSE = LICENSE NSExt.sln.DotSettings = NSExt.sln.DotSettings NuGet.Config = NuGet.Config diff --git a/README.md b/README.md index 610911e..0e53b79 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,65 @@ -# dot +# ns-ext [中](README.zh-CN.md) | **En** +The **ns-ext** is a .NET extension function library, containing the following types of extension modules: -Cross-platform, all-around utility set with a beautiful character interface-the Swiss Army knife +| Features | File name | +|------------------------------------------|------------------------------------| +| Byte type extension | ByteExtensions.cs | +| Character Type Extensions | CharExtensions.cs | +| Date Type Extensions | DateTimeExtensions.cs | +| Database command type extension | DbCommandExtensions.cs | +| Decimal Number Type extension | DecimalExtensions.cs | +| Enumable type extension | EnumerableExtensions.cs | +| Enumeration type extension | EnumExtensions.cs | +| General type extension | GenericExtensions.cs | +| Integer type extension | IntExtensions.cs | +| Json Serialization option type extension | JsonSerializerOptionsExtensions.cs | +| Log type extension | LoggerExtensions.cs | +| Long integer extension | LongExtensions.cs | +| Object type extension | ObjectExtensions.cs | +| Stream type extension | StreamExtensions.cs | +| String type extension | StringExtensions.cs | +| Prototype type extension | TypeExtensions.cs | +| Resource locator type extension | UriExtensions.cs | -### Brief introduction +## Quick start -The dot is the one based on the one. NET 7, a cross-platform command-line tool, integrates more than 10 utilities that -program developers often use in their daily work, and is constantly increasing. - -``` -USAGE: - dot [OPTIONS] - -OPTIONS: - -h, --help Prints help information - -v, --version Prints version information - -COMMANDS: - git Git batch operation tool - color Screen coordinate color selection tool - tran Translation tools - guid GUID tool - ip IP tools - json Json tool - pwd Random password generator - rbom Remove the uf8 bom of the file - trim Remove line breaks and spaces at the end of the file - text Text encoding tool - time Time synchronization tool - tolf Convert newline characters to LF - get Multithreaded download tool +### Install +```shell +dotnet add package NSExt --prerelease ``` -### Some functional examples +### Example -- ##### Git batch management +```c# +using NSExt.Extensions; -When you have a clone and a lot of git repositories, use this command to pull their latest code all at once: +internal static class Program +{ + private class Person + { + public string Name { get; set; } + public int Age { get; set; } + } + public static void Main(string[] args) + { + var person = + """ +{ + "Name": "Jason", + "Age": "30" +} +""".Object(); -``` -dot git -a "pull" d:\repos + Console.WriteLine(person.Json()); + } +} ``` -![20221212212417](./assets/snapshots/20221212212417.png) +### Output -Similarly, you can execute any git commands on a bunch of git repositories in bulk: - -``` -dot git -a "config --get http.proxy" d:\repos -``` - -![20221212213957](./assets/snapshots/20221212213957.png) - -- ##### High-precision time-clock synchronization - -Supports parallel requests from multiple NTP clock servers, while removing the network communication duration to set the -precise synchronization of the native clocks with the NTP standard time: - -``` -dot time -k -``` - -![20221212214514](./assets/snapshots/20221212214514.png) - -- ##### Text codec - -Copy you need to view various codec text in the clipboard, and then enter the following command to view - -``` -dot text -``` - -![20221212214904](./assets/snapshots/20221212214904.png) - -- ##### Multi-threading download tool - -Support setting the block size, number of threads to replace the single thread wget tool: - -``` -dot get https://github.com/nsnail/dot/releases/download/v1.1.1/dot-v1.1.1-win-x64.7z -``` - -![20221212215259](./assets/snapshots/20221212215259.png) - -- ##### Remove the blank at the end of the file - -Remove excess spaces and line breaks in the tail of all files in the specified directory: - -``` -dot trim d:\repos -``` - -![20221212215853](./assets/snapshots/20221212215853.png) \ No newline at end of file +```json +{"name":"Jason","age":30} +``` \ No newline at end of file diff --git a/README.zh-CN.md b/README.zh-CN.md index 0cd4786..127d239 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,94 +1,65 @@ -# dot - +# ns-ext [En](README.md) | **中** +**ns-ext** 是一个.NET扩展函数库,包含以下类型扩展模块: -跨平台的、具有美观字符界面的全能实用工具集 - 程序员的瑞士军刀 -### 简介 +| 功能 | 文件名 | +| -------- | ---------------------------------- | +| 字节类型扩展 | ByteExtensions.cs | +| 字符类型扩展 | CharExtensions.cs | +| 日期类型扩展 | DateTimeExtensions.cs | +| 数据库命令类型扩展 | DbCommandExtensions.cs | +| 十进制数类型扩展 | DecimalExtensions.cs | +| 可枚举类型扩展 | EnumerableExtensions.cs | +| 枚举类型扩展 | EnumExtensions.cs | +| 泛型类型扩展 | GenericExtensions.cs | +| 整数型扩展 | IntExtensions.cs | +| Json序列化选项类型扩展 | JsonSerializerOptionsExtensions.cs | +| 日志类型扩展 | LoggerExtensions.cs | +| 长整型扩展 | LongExtensions.cs | +| 对象类型扩展 | ObjectExtensions.cs | +| 流类型扩展 | StreamExtensions.cs | +| 字符串类型扩展 | StringExtensions.cs | +| 原型类型扩展 | TypeExtensions.cs | +| 资源定位符类型扩展 | UriExtensions.cs | -dot 是一款基于.NET7,跨平台的命令行工具,集成10多种程序开发人员在日常工作常常用到的实用功能,并且还在不断增加。 +## 快速开始 -``` -USAGE: - dot [OPTIONS] - -OPTIONS: - -h, --help Prints help information - -v, --version Prints version information - -COMMANDS: - git Git批量操作工具 - color 屏幕坐标颜色选取工具 - tran 翻译工具 - guid GUID工具 - ip IP工具 - json Json工具 - pwd 随机密码生成器 - rbom 移除文件的uf8 bom - trim 移除文件尾部换行和空格 - text 文本编码工具 - time 时间同步工具 - tolf 转换换行符为LF - get 多线程下载工具 +### 安装 +```shell +dotnet add package NSExt --prerelease ``` -### 部分功能示例 +### 示例 -- ##### Git批量管理 +```c# +using NSExt.Extensions; -当你clone了大量的git仓库, 使用这条命令可以一次性拉取它们的最新代码: +internal static class Program +{ + private class Person + { + public string Name { get; set; } + public int Age { get; set; } + } + public static void Main(string[] args) + { + var person = + """ +{ + "Name": "Jason", + "Age": "30" +} +""".Object(); -``` -dot git -a "pull" d:\repos + Console.WriteLine(person.Json()); + } +} ``` -![20221212212417](./assets/snapshots/20221212212417.png) +### 输出 -类似地,你可以批量对一堆git仓库执行任何git命令: - -``` -dot git -a "config --get http.proxy" d:\repos -``` - -![20221212213957](./assets/snapshots/20221212213957.png) - -- ##### 高精度时钟同步 - -支持多个NTP时钟服务器并行请求,同时除去网络通信时长以设置本机时钟与NTP标准时间精确同步: - -``` -dot time -k -``` - -![20221212214514](./assets/snapshots/20221212214514.png) - -- ##### 文本编解码 - -复制你需要查看各种编解码的文本在剪贴板中,然后输入如下命令,即可查看 - -``` -dot text -``` - -![20221212214904](./assets/snapshots/20221212214904.png) - -- ##### 多线程下载工具 - -支持设置分块大小,线程数量,用以替代单线程的wget工具: - -``` -dot get https://github.com/nsnail/dot/releases/download/v1.1.1/dot-v1.1.1-win-x64.7z -``` - -![20221212215259](./assets/snapshots/20221212215259.png) - -- ##### 移除文件末尾空白 - -移除指定目录下所有文件尾部多余的空格和换行符: - -``` -dot trim d:\repos -``` - -![20221212215853](./assets/snapshots/20221212215853.png) \ No newline at end of file +```json +{"name":"Jason","age":30} +``` \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..77638c101c0683444bf377c797cf393d20352073 GIT binary patch literal 1482 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k8A#4*i(3PvSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?o382Ka=yx)=E9=;&yJ0g$1qt7~j*3}L9Kr~tWIT3SFZ5a{XY0ogzj$ObA0 zGJvu`0FeTU0NFqiq!bmv#UYB-)zyLKK-h4F5bZ#&rluxDP)$t@$bi_6tQcZNXlN)< z7AOu;0Cx<83pWH}6r6-8gV+Vs3uI_$XaM~S6nAiNfS3$17Gh)VU9)CjsKu28`2{n` zZcB|h!R1iF9&LELLCiqs*L-flD>rPdo1LD2Pnteys^~)z1v>&$UuZOAXr8r#94t;QE|!ld)5ACH}9?f_uqKinRh-$njXJ%YM$== z&U{2BrAPPXdrQ8<20VuyQtRz5+SeK1n?K|J_K1s{!@gbFU-5LSoVBEIardh{vFBV%%t4izjH4D9~mv?SC5?FZK;ZB$A1m~-I z?O_&^R@F`tvoO+@nlP2ePGxP$q+{(*wkTLu9Phkzn$zk@h;UIC-_0v6Ig93Ro|1Rq zd^Bq=$B)ntlgv^$rHxkcN*--nvPDtTi9@kP07B#~)t=_NwbyP*?xv0F6!yKU2`#m| zy63#`^|z}wJSzKr{6+6-1L5~yPfq+0vg4!BK8txPKTQ69Ffg2BWw-26HkKuUQ=A@g zDHp!13}5I{Ay$^CuDeS^;)(+_BQ{|-D`jR{pt`g;kIO~R>ytLSu(Q^T3lQ8(@t@z zNz13*E0?x@HsUVkiV)oQ{Wt6LoHEtb?=ChgSAO(TspS45u3TO7*>-}8+N3=P8=u&D zDXcb?IT?O&3J;TP%qA%dWs3=^fw7D17HG~C_B~lFbK(&@=M&irQ~peFuw!&w=W_g- zx6FyJ>6|Hy8D7s+CnPF#yfB??BCsT$$&<f z`IHMJ#l&7qNxt;sXNY6z&4>x{vp$w^-^(#xcfcms;K!Di4<_ZQEVFg@`KE5%mK?5n z{{IZK4g;w>1v!C>J46Ir!V(U&HlEP>Is01}hk(-C0%7(>kKM5rd*xV;O#c4T)4}1- zi%XN)8-83|e7x{g?*zr(U61{{J_aa$T=Y1)#JZc6;p67bH{NfVwNpT0XWHqH_ELAh opIKM$^m5;EV8#R^;gmWiZ?20kKZ@6_0~RX`p00i_>zopr0JFMD6951J literal 0 HcmV?d00001 diff --git a/assets/res/Statements.ln b/assets/res/Statements.ln new file mode 100644 index 0000000..e69de29 diff --git a/build/code.quality.props b/build/code.quality.props new file mode 100644 index 0000000..facdab9 --- /dev/null +++ b/build/code.quality.props @@ -0,0 +1,47 @@ + + + $(SolutionDir)/build/stylecop.analyzers.ruleset + + + true + true + true + true + true + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + \ No newline at end of file diff --git a/build/copy.pkg.xml.comment.files.targets b/build/copy.pkg.xml.comment.files.targets new file mode 100644 index 0000000..93db426 --- /dev/null +++ b/build/copy.pkg.xml.comment.files.targets @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/build/minver.targets b/build/minver.targets new file mode 100644 index 0000000..2a53ff9 --- /dev/null +++ b/build/minver.targets @@ -0,0 +1,11 @@ + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + $(MinVerVersion) + $(MinVerVersion) + $(MinVerVersion) + + + \ No newline at end of file diff --git a/build/prebuild.targets b/build/prebuild.targets new file mode 100644 index 0000000..4270160 --- /dev/null +++ b/build/prebuild.targets @@ -0,0 +1,24 @@ + + + + + + + + + Languages/Statements.ln + + + Languages/Ln.resx + PublicResXFileCodeGenerator + + + Languages/Ln.Designer.cs + + + \ No newline at end of file diff --git a/build/stylecop.analyzers.ruleset b/build/stylecop.analyzers.ruleset new file mode 100644 index 0000000..b8faaff --- /dev/null +++ b/build/stylecop.analyzers.ruleset @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..fccf379 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,182 @@ +# 约定式提交 1.0.0 + +## [](#概述)概述 + +约定式提交规范是一种基于提交信息的轻量级约定。 它提供了一组简单规则来创建清晰的提交历史; 这更有利于编写自动化工具。 通过在提交信息中描述功能、修复和破坏性变更, 使这种惯例与 [SemVer](http://semver.org/lang/zh-CN) 相互对应。 + +提交说明的结构如下所示: + +* * * + +原文: + + [optional scope]: + + [optional body] + + [optional footer(s)] + + +译文: + + <类型>[可选 范围]: <描述> + + [可选 正文] + + [可选 脚注] + + +* * * + +提交说明包含了下面的结构化元素,以向类库使用者表明其意图: + +1. **fix:** _类型_ 为 `fix` 的提交表示在代码库中修复了一个 bug(这和语义化版本中的 [`PATCH`](https://semver.org/lang/zh-CN/#%E6%91%98%E8%A6%81) 相对应)。 +2. **feat:** _类型_ 为 `feat` 的提交表示在代码库中新增了一个功能(这和语义化版本中的 [`MINOR`](https://semver.org/lang/zh-CN/#%E6%91%98%E8%A6%81) 相对应)。 +3. **BREAKING CHANGE:** 在脚注中包含 `BREAKING CHANGE:` 或 <类型>(范围) 后面有一个 `!` 的提交,表示引入了破坏性 API 变更(这和语义化版本中的 [`MAJOR`](https://semver.org/lang/zh-CN/#%E6%91%98%E8%A6%81) 相对应)。 破坏性变更可以是任意 _类型_ 提交的一部分。 +4. 除 `fix:` 和 `feat:` 之外,也可以使用其它提交 _类型_ ,例如 [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)(基于 [Angular 约定](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines))中推荐的 `build:`、`chore:`、 `ci:`、`docs:`、`style:`、`refactor:`、`perf:`、`test:`,等等。 + * build: 用于修改项目构建系统,例如修改依赖库、外部接口或者升级 Node 版本等; + * chore: 用于对非业务性代码进行修改,例如修改构建流程或者工具配置等; + * ci: 用于修改持续集成流程,例如修改 Travis、Jenkins 等工作流配置; + * docs: 用于修改文档,例如修改 README 文件、API 文档等; + * style: 用于修改代码的样式,例如调整缩进、空格、空行等; + * refactor: 用于重构代码,例如修改代码结构、变量名、函数名等但不修改功能逻辑; + * perf: 用于优化性能,例如提升代码的性能、减少内存占用等; + * test: 用于修改测试用例,例如添加、删除、修改代码的测试用例等。 +5. 脚注中除了 `BREAKING CHANGE: ` ,其它条目应该采用类似 [git trailer format](https://git-scm.com/docs/git-interpret-trailers) 这样的惯例。 + +其它提交类型在约定式提交规范中并没有强制限制,并且在语义化版本中没有隐式影响(除非它们包含 BREAKING CHANGE)。 可以为提交类型添加一个围在圆括号内的范围,以为其提供额外的上下文信息。例如 `feat(parser): adds ability to parse arrays.`。 + +## [](#示例)示例 + +### [](#包含了描述并且脚注中有破坏性变更的提交说明)包含了描述并且脚注中有破坏性变更的提交说明 + + feat: allow provided config object to extend other configs + + BREAKING CHANGE: `extends` key in config file is now used for extending other config files + + +### [](#包含了--字符以提醒注意破坏性变更的提交说明)包含了 `!` 字符以提醒注意破坏性变更的提交说明 + + feat!: send an email to the customer when a product is shipped + + +### [](#包含了范围和破坏性变更--的提交說明)包含了范围和破坏性变更 `!` 的提交說明 + + feat(api)!: send an email to the customer when a product is shipped + + +### [](#包含了--和-breaking-change-脚注的提交说明)包含了 `!` 和 BREAKING CHANGE 脚注的提交说明 + + chore!: drop support for Node 6 + + BREAKING CHANGE: use JavaScript features not available in Node 6. + + +### [](#不包含正文的提交说明)不包含正文的提交说明 + + docs: correct spelling of CHANGELOG + + +### [](#包含范围的提交说明)包含范围的提交说明 + + feat(lang): add polish language + + +### [](#包含多行正文和多行脚注的提交说明)包含多行正文和多行脚注的提交说明 + + fix: prevent racing of requests + + Introduce a request id and a reference to latest request. Dismiss + incoming responses other than from latest request. + + Remove timeouts which were used to mitigate the racing issue but are + obsolete now. + + Reviewed-by: Z + Refs: #123 + + +## [](#约定式提交规范)约定式提交规范 + +本文中的关键词 “必须(MUST)”、“禁止(MUST NOT)”、“必要(REQUIRED)”、“应当(SHALL)”、“不应当(SHALL NOT)”、“应该(SHOULD)”、“不应该(SHOULD NOT)”、“推荐(RECOMMENDED)”、“可以(MAY)” 和 “可选(OPTIONAL)” ,其相关解释参考 [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 。 + +1. 每个提交都**必须**使用类型字段前缀,它由一个名词构成,诸如 `feat` 或 `fix` , 其后接**可选的**范围字段,**可选的** `!`,以及**必要的**冒号(英文半角)和空格。 +2. 当一个提交为应用或类库实现了新功能时,**必须**使用 `feat` 类型。 +3. 当一个提交为应用修复了 bug 时,**必须**使用 `fix` 类型。 +4. 范围字段**可以**跟随在类型字段后面。范围**必须**是一个描述某部分代码的名词,并用圆括号包围,例如: `fix(parser):` +5. 描述字段**必须**直接跟在 <类型>(范围) 前缀的冒号和空格之后。 描述指的是对代码变更的简短总结,例如: _fix: array parsing issue when multiple spaces were contained in string_ 。 +6. 在简短描述之后,**可以**编写较长的提交正文,为代码变更提供额外的上下文信息。正文**必须**起始于描述字段结束的一个空行后。 +7. 提交的正文内容自由编写,并**可以**使用空行分隔不同段落。 +8. 在正文结束的一个空行之后,**可以**编写一行或多行脚注。每行脚注都**必须**包含 一个令牌(token),后面紧跟 `:` 或 `#` 作为分隔符,后面再紧跟令牌的值(受 [git trailer convention](https://git-scm.com/docs/git-interpret-trailers) 启发)。 +9. 脚注的令牌**必须**使用 `-` 作为连字符,比如 `Acked-by` (这样有助于 区分脚注和多行正文)。有一种例外情况就是 `BREAKING CHANGE`,它**可以**被认为是一个令牌。 +10. 脚注的值**可以**包含空格和换行,值的解析过程**必须**直到下一个脚注的令牌/分隔符出现为止。 +11. 破坏性变更**必须**在提交信息中标记出来,要么在 <类型>(范围) 前缀中标记,要么作为脚注的一项。 +12. 包含在脚注中时,破坏性变更**必须**包含大写的文本 `BREAKING CHANGE`,后面紧跟着冒号、空格,然后是描述,例如: _BREAKING CHANGE: environment variables now take precedence over config files_ 。 +13. 包含在 <类型>(范围) 前缀时,破坏性变更**必须**通过把 `!` 直接放在 `:` 前面标记出来。 如果使用了 `!`,那么脚注中**可以**不写 `BREAKING CHANGE:`, 同时提交信息的描述中**应该**用来描述破坏性变更。 +14. 在提交说明中,**可以**使用 `feat` 和 `fix` 之外的类型,比如:_docs: updated ref docs._ 。 +15. 工具的实现必须**不区分**大小写地解析构成约定式提交的信息单元,只有 `BREAKING CHANGE` **必须**是大写的。 +16. BREAKING-CHANGE 作为脚注的令牌时**必须**是 BREAKING CHANGE 的同义词。 + +## [](#为什么使用约定式提交)为什么使用约定式提交 + +* 自动化生成 CHANGELOG。 +* 基于提交的类型,自动决定语义化的版本变更。 +* 向同事、公众与其他利益关系者传达变化的性质。 +* 触发构建和部署流程。 +* 让人们探索一个更加结构化的提交历史,以便降低对你的项目做出贡献的难度。 + +## [](#faq)FAQ + +### [](#在初始开发阶段我该如何处理提交说明)在初始开发阶段我该如何处理提交说明? + +我们建议你按照假设你已发布了产品那样来处理。因为通常总 _有人_ 使用你的软件,即便那是你软件开发的同事们。他们会希望知道诸如修复了什么、哪里不兼容等信息。 + +### [](#提交标题中的类型是大写还是小写)提交标题中的类型是大写还是小写? + +大小写都可以,但最好是一致的。 + +### [](#如果提交符合多种类型我该如何操作)如果提交符合多种类型我该如何操作? + +回退并尽可能创建多次提交。约定式提交的好处之一是能够促使我们做出更有组织的提交和 PR。 + +### [](#这不会阻碍快速开发和迭代吗)这不会阻碍快速开发和迭代吗? + +它阻碍的是以杂乱无章的方式快速前进。它助你能在横跨多个项目以及和多个贡献者协作时长期地快速演进。 + +### [](#约定式提交会让开发者受限于提交的类型吗因为他们会想着已提供的类型)约定式提交会让开发者受限于提交的类型吗(因为他们会想着已提供的类型)? + +约定式提交鼓励我们更多地使用某些类型的提交,比如 `fixes`。除此之外,约定式提交的灵活性也允许你的团队使用自己的类型,并随着时间的推移更改这些类型。 + +### [](#这和-semver-有什么关联呢)这和 SemVer 有什么关联呢? + +`fix` 类型提交应当对应到 `PATCH` 版本。`feat` 类型提交应该对应到 `MINOR` 版本。带有 `BREAKING CHANGE` 的提交不管类型如何,都应该对应到 `MAJOR` 版本。 + +### [](#我对约定式提交做了形如-jameswomackconventional-commit-spec-的扩展该如何版本化管理这些扩展呢)我对约定式提交做了形如 `@jameswomack/conventional-commit-spec` 的扩展,该如何版本化管理这些扩展呢? + +我们推荐使用 SemVer 来发布你对于这个规范的扩展(并鼓励你创建这些扩展!) + +### [](#如果我不小心使用了错误的提交类型该怎么办呢)如果我不小心使用了错误的提交类型,该怎么办呢? + +#### [](#当你使用了在规范中但错误的类型时例如将-feat-写成了-fix)当你使用了在规范中但错误的类型时,例如将 `feat` 写成了 `fix` + +在合并或发布这个错误之前,我们建议使用 `git rebase -i` 来编辑提交历史。而在发布之后,根据你使用的工具和流程不同,会有不同的清理方案。 + +#### [](#当使用了-不在-规范中的类型时例如将-feat-写成了-feet)当使用了 _不在_ 规范中的类型时,例如将 `feat` 写成了 `feet` + +在最坏的场景下,即便提交没有满足约定式提交的规范,也不会是世界末日。这只意味着这个提交会被基于规范的工具错过而已。 + +### [](#所有的贡献者都需要使用约定式提交规范吗)所有的贡献者都需要使用约定式提交规范吗? + +并不!如果你使用基于 squash 的 Git 工作流,主管维护者可以在合并时清理提交信息——这不会对普通提交者产生额外的负担。 有种常见的工作流是让 git 系统自动从 pull request 中 squash 出提交,并向主管维护者提供一份表单,用以在合并时输入合适的 git 提交信息。 + +### [](#约定式提交规范中如何处理还原revert提交)约定式提交规范中如何处理还原(revert)提交? + +还原提交(Reverting)会比较复杂:你还原的是多个提交吗?如果你还原了一个功能模块,下次发布的应该是补丁吗? + +约定式提交不能明确的定义还原行为。所以我们把这个问题留给工具开发者, 基于 _类型_ 和 _脚注_ 的灵活性来开发他们自己的还原处理逻辑。 + +一种建议是使用 `revert` 类型,和一个指向被还原提交摘要的脚注: + + revert: let us never again speak of the noodle incident + + Refs: 676104e, a215868 \ No newline at end of file diff --git a/docs/SEMVER.md b/docs/SEMVER.md new file mode 100644 index 0000000..53b6009 --- /dev/null +++ b/docs/SEMVER.md @@ -0,0 +1,226 @@ +语义化版本 2.0.0 +=== + +摘要 +--- + +版本格式:主版本号.次版本号.修订号,版本号递增规则如下: + +1. 主版本号:当你做了不兼容的 API 修改, +2. 次版本号:当你做了向下兼容的功能性新增, +3. 修订号:当你做了向下兼容的问题修正。 + +先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。 + +简介 +--- + +在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。 + +在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。 + +作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。为了让这套理论运作,你必须先有定义好的公共 API。这可能包括文档或代码的强制要求。无论如何,这套 API 的清楚明了是十分重要的。一旦你定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:X.Y.Z(主版本号.次版本号.修订号)修复问题但不影响 API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。 + +我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。 + +语义化版本控制规范(SemVer) +--- + +以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。 + +1. 使用语义化版本控制的软件必须(MUST)定义公共 API。该 API 可以在代码中被定义或出现于严谨的文档内。无论何种形式都应该力求精确且完整。 + +2. 标准的版本号必须(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止(MUST NOT)在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须(MUST)以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。 + +3. 标记版本号的软件发行后,禁止(MUST NOT)改变该版本软件的内容。任何修改都必须(MUST)以新版本发行。 + +4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。 + +5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。 + +6. 修订号 Z(x.y.Z `|` x > 0)必须(MUST)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。 + +7. 次版本号 Y(x.Y.z `|` x > 0)必须(MUST)在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也必须(MUST)递增。也可以(MAY)在内部程序有大量新功能或改进被加入时递增,其中可以(MAY)包括修订级别的改变。每当次版本号递增时,修订号必须(MUST)归零。 + +8. 主版本号 X(X.y.z `|` X > 0)必须(MUST)在有任何不兼容的修改被加入公共 API 时递增。其中可以(MAY)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(MUST)归零。 + +9. 先行版本号可以(MAY)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。数字型的标识符禁止(MUST NOT)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。 + +10. 版本编译信息可以(MAY)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。当判断版本的优先层级时,版本编译信息可(SHOULD)被忽略。因此当两个版本只有在版本编译信息有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。 + +11. 版本的优先层级指的是不同版本在排序时如何比较。 + + 1. 判断优先层级时,必须(MUST)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译信息不在这份比较的列表中)。 + + 2. 由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较。 + + 例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。 + + 3. 当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。 + + 例如:1.0.0-alpha < 1.0.0。 + + 4. 有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(MUST)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定: + + 1. 只有数字的标识符以数值高低比较。 + + 2. 有字母或连接号时则逐字以 ASCII 的排序来比较。 + + 3. 数字的标识符比非数字的标识符优先层级低。 + + 4. 若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。 + + 例如:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。 + +合法语义化版本的巴科斯范式语法 +-------------------------------------------------- +``` + ::= + | "-" + | "+" + | "-" "+" + + ::= "." "." + + ::= + + ::= + + ::= + + ::= + + ::= + | "." + + ::= + + ::= + | "." + + ::= + | + + ::= + | + + ::= + | + | + | + + ::= "0" + | + | + + ::= + | + + ::= + | + + ::= + | "-" + + ::= + | + + ::= "0" + | + + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + + ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" + | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" + | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" | "c" | "d" + | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" + | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" + | "y" | "z" +``` + +为什么要使用语义化的版本控制? +--- + +这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。 + +举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函数库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能,你可以放心地指定依赖于梯子的版本号大于等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。 + +作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。 + +如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函数库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页链接,让别人也知道这些规则并从中受益。 + +FAQ +--- + +### 在 0.y.z 初始开发阶段,我该如何进行版本控制? + +最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。 + +### 如何判断发布 1.0.0 版本的时机? + +当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。 + +### 这不会阻碍快速开发和迭代吗? + +主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。 + +### 对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版? + +这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。 + +### 为整个公共 API 写文档太费事了! + +为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。保持项目高效的一个非常重要的部分是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。 + +### 万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办? + +一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文档中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。 + +### 如果我更新了自己的依赖但没有改变公共 API 该怎么办? + +由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。 + +### 如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中) + +自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。 + +### 我该如何处理即将弃用的功能? + +弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部分公共 API 时,你应该做两件事:(1)更新你的文档让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。 + +### 语义化版本对于版本的字符串长度是否有限制呢? + +没有,请自行做适当的判断。举例来说,长到 255 个字符的版本已过度夸张。再者,特定的系统对于字符串长度可能会有他们自己的限制。 + +### “v1.2.3” 是一个语义化版本号吗? + +“v1.2.3” 并不是一个语义化的版本号。但是,在语义化版本号之前增加前缀 “v” 是用来表示版本号的常用做法。在版本控制系统中,将 “version” 缩写为 “v” 是很常见的。比如:`git tag v1.2.3 -m "Release version 1.2.3"` 中,“v1.2.3” 表示标签名称,而 “1.2.3” 是语义化版本号。 + +### 是否有推荐的正则表达式用以检查语义化版本号的正确性? + +有两个推荐的正则表达式。第一个用于支持按组名称提取的语言(PCRE[Perl 兼容正则表达式,比如 Perl、PHP 和 R]、Python 和 Go)。 + +参见: + +``` +^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ +``` + +第二个用于支持按编号提取的语言(与第一个对应的提取项按顺序分别为:major、minor、patch、prerelease、buildmetadata)。主要包括 ECMA Script(JavaScript)、PCRE(Perl 兼容正则表达式,比如 Perl、PHP 和 R)、Python 和 Go。 +参见: + +``` +^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ +``` + +关于 +--- + +语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 [Tom Preston-Werner](http://tom.preston-werner.com) 所建立。 + +如果您有任何建议,请到 [GitHub 上提出您的问题](https://github.com/semver/semver/issues)。 + +许可证 +--- + +[知识共享 署名 3.0 (CC BY 3.0)](http://creativecommons.org/licenses/by/3.0/) \ No newline at end of file diff --git a/key.snk b/key.snk new file mode 100644 index 0000000000000000000000000000000000000000..aac3fbeb5ae1341b1b9b469def4208dac924e14f GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097vN^yTHA;18iOOTsZY+D>e zVME~o&&5GpZ1!S$@?9mG(Yqb4CIuW@m@rI$xWg2~sb~k8oU=a98^PzhJ)b1Nx`sh3 zWV|yEoORANt8kHFV(-0;t3Uu_imcxXt4f-Ydw* zG@5~*R`J)2R?Blbp4|)MFmVe6N z$X0Hm>DOkxP)J%F{V#u@dj8B3QbR2tV=pL4V`cPk)2{RkEdkO?VJnL|3IOXxvX6Jm z`$VP3Rp67I$!OKe=$yxdgfOg5}i=CQpRmzckBY7((h!cF-$3%_JEB literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..b907439 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.1", + "devDependencies": { + "cz-git": "^1.7.1", + "commitizen": "^4.3.0", + "prettier": "^3.1.0", + "standard-version": "^9.5.0" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + } +} \ No newline at end of file diff --git a/scripts/clean.ln.csx b/scripts/clean.ln.csx new file mode 100644 index 0000000..f77913d --- /dev/null +++ b/scripts/clean.ln.csx @@ -0,0 +1,26 @@ +using System.Text.RegularExpressions; + +var csFiles = Directory.EnumerateFiles(@"../src/backend", $"*.cs", new EnumerationOptions { RecurseSubdirectories = true }); +foreach (var lnFile in Directory.EnumerateFiles("../assets/res", "*.ln")) +{ + var newLines = new List(); + foreach (var line in File.ReadAllLines(lnFile)) + { + var found = false; + foreach (var csFile in csFiles) + { + if (File.ReadAllText(csFile).Contains($"Ln.{(Regex.IsMatch(line, @"^\d") ? "_" : "") + line}")) + { + found = true; + newLines.Add(line); + break; + } + } + + if (!found) + { + Console.WriteLine(line); + } + } + File.WriteAllLines(lnFile, newLines); +} \ No newline at end of file diff --git a/scripts/code.clean.csx b/scripts/code.clean.csx new file mode 100644 index 0000000..8cd6214 --- /dev/null +++ b/scripts/code.clean.csx @@ -0,0 +1,44 @@ +using System.Text.RegularExpressions; +using System.Net.Http; +using System.Net.Http.Json; + + +{ + var files = string.Join( + ';', + Args[0] + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Replace('\\', '/').Trim()) + ); + + Console.WriteLine(files); + + using var p = Process.Start( + new ProcessStartInfo + { + CreateNoWindow = true, + FileName = "dotnet", + Arguments = $"jb cleanupcode --include=\"{files}\" --no-build ../NSExt.sln", + UseShellExecute = false, + RedirectStandardOutput = true + } + ); + p.WaitForExit(); + Console.WriteLine(p.StandardOutput.ReadToEnd()); + + using var p2 = Process.Start( + new ProcessStartInfo + { + CreateNoWindow = true, + FileName = "git", + Arguments = $"status", + UseShellExecute = false, + RedirectStandardOutput = true + } + ); + p2.WaitForExit(); + var content = p2.StandardOutput.ReadToEnd(); + Console.WriteLine(content); + + return content.Contains("working tree clean") ? 0 : 1; +} \ No newline at end of file diff --git a/scripts/code.clean.ps1 b/scripts/code.clean.ps1 new file mode 100644 index 0000000..b1fbeab --- /dev/null +++ b/scripts/code.clean.ps1 @@ -0,0 +1,4 @@ +dotnet jb cleanupcode --no-build --include = $( $( git status --porcelain | Where-Object { $_ -match "^\s*[MA]" } | ForEach-Object { $_.TrimStart(" M").TrimStart(" A") } ) -join ";" ) ../NSExt.sln +dot rbom -w -e refs -e .git -e node_modules ../ +dot trim -w -e refs -e .git -e node_modules ../ +dot tolf -w -e refs -e .git -e node_modules ../ \ No newline at end of file diff --git a/scripts/gen.cs.tt b/scripts/gen.cs.tt new file mode 100644 index 0000000..5c029ee --- /dev/null +++ b/scripts/gen.cs.tt @@ -0,0 +1,77 @@ +<#@ template language="C#" #> +<#@ assembly name="System.Xml" #> +<#@ output encoding="utf-8" extension="Designer.cs" #> + +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System.CodeDom.Compiler; +using System.Diagnostics.CodeAnalysis; +using System.Resources; +using System.Runtime.CompilerServices; + +namespace NSExt.Languages; + +/// +/// A strongly-typed resource class, for looking up localized strings, etc. +/// + +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] +[System.Diagnostics.DebuggerNonUserCode] +[System.Runtime.CompilerServices.CompilerGenerated] +public sealed class Ln +{ + private static ResourceManager _resourceMan; + + /// + /// Initializes a new instance of the class. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Ln() { } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static ResourceManager ResourceManager { + get { + if (ReferenceEquals(_resourceMan, null)) { + var temp = new ResourceManager("NSExt.Languages.Ln", typeof(Ln).Assembly); + _resourceMan = temp; + } + + return _resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static CultureInfo Culture { get; set; } +<# + var xml = new System.Xml.XmlDocument(); + xml.Load("../assets/res/Ln.resx"); + foreach (System.Xml.XmlNode data in xml.SelectNodes("//root/data")!) + { +#> + /// + /// <#= data.SelectSingleNode("value")?.InnerText #> + /// + public static string <#= + data.Attributes!["name"].Value.Replace(" ", "_") #> => ResourceManager.GetString("<#= data.Attributes!["name"].Value #>", Culture); +<# + } +#> +} \ No newline at end of file diff --git a/scripts/gen.ln.cmd b/scripts/gen.ln.cmd new file mode 100644 index 0000000..1a86b86 --- /dev/null +++ b/scripts/gen.ln.cmd @@ -0,0 +1,2 @@ +dotnet t4 ./gen.resx.tt -o ../assets/res/Ln.resx +dotnet t4 ./gen.cs.tt -o ../dist/backend/NSExt/Ln.cs \ No newline at end of file diff --git a/scripts/gen.resx.tt b/scripts/gen.resx.tt new file mode 100644 index 0000000..6e0cad2 --- /dev/null +++ b/scripts/gen.resx.tt @@ -0,0 +1,39 @@ +<#@ template language="C#" #> +<#@ output encoding="utf-8" extension="resx" #> + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + <# + var regex = new System.Text.RegularExpressions.Regex(@"^\d", System.Text.RegularExpressions.RegexOptions.Compiled); + foreach (var file in System.IO.Directory.GetFiles("../assets/res/", "*.ln")) + { + foreach (var line in System.IO.File.ReadLines(file)) + { +#> + <#= line #>" xml:space="preserve"><#= line #> +<# + } + } +#> + \ No newline at end of file diff --git a/scripts/git.pr.ps1 b/scripts/git.pr.ps1 new file mode 100644 index 0000000..9bbf472 --- /dev/null +++ b/scripts/git.pr.ps1 @@ -0,0 +1,11 @@ +$branch = $( git branch --show-current ) +git add ../ +./code.clean.ps1 +git add ../ +../node_modules/.bin/git-cz.ps1 +git pull +git push --set-upstream origin $branch +Start-Process -FilePath "https://github.com/nsnail/dot/compare/main...$branch" +Write-Host "按『Enter』重建分支,『Ctrl+C』退出" +Pause +./git.rc.ps1 \ No newline at end of file diff --git a/scripts/git.rc.ps1 b/scripts/git.rc.ps1 new file mode 100644 index 0000000..2a66810 --- /dev/null +++ b/scripts/git.rc.ps1 @@ -0,0 +1,6 @@ +$branch = $( git branch --show-current ) +git checkout main +git pull +git branch -D $branch +git branch $branch +git checkout $branch \ No newline at end of file diff --git a/scripts/image.optimize.csx b/scripts/image.optimize.csx new file mode 100644 index 0000000..41f558c --- /dev/null +++ b/scripts/image.optimize.csx @@ -0,0 +1,65 @@ +/* + for %%i in (*.png) do pngquant %%i --force --output %%i --skip-if-larger + for %%i in (*.jpg) do jpegtran -copy none -optimize -perfect %%i %%i + * + */ + + +var files = Directory + .EnumerateFiles( + "../", + "*.png", + new EnumerationOptions + { + RecurseSubdirectories = true, + AttributesToSkip = FileAttributes.ReparsePoint, + IgnoreInaccessible = true + } + ) + .ToArray(); + +Parallel.ForEach( + files, + file => + { + var startInfo = new ProcessStartInfo + { + FileName = "pngquant", + Arguments = $"\"{file}\" --force --output \"{file}\" --skip-if-larger" + }; + using var p = Process.Start(startInfo); + p.WaitForExit(); + Console.WriteLine($"{file}: {p.ExitCode}"); + } +); + +files = new[] { "*.jpg", "*.jpeg" } + .SelectMany( + x => + Directory.EnumerateFiles( + "../", + x, + new EnumerationOptions + { + RecurseSubdirectories = true, + AttributesToSkip = FileAttributes.ReparsePoint, + IgnoreInaccessible = true + } + ) + ) + .ToArray(); + +Parallel.ForEach( + files, + file => + { + var startInfo = new ProcessStartInfo + { + FileName = "jpegtran", + Arguments = $"-copy none -optimize -perfect \"{file}\" \"{file}\"" + }; + using var p = Process.Start(startInfo); + p.WaitForExit(); + Console.WriteLine($"{file}: {p.ExitCode}"); + } +); \ No newline at end of file diff --git a/scripts/install.as.tpl.ps1 b/scripts/install.as.tpl.ps1 new file mode 100644 index 0000000..ad933dd --- /dev/null +++ b/scripts/install.as.tpl.ps1 @@ -0,0 +1,2 @@ +dotnet new uninstall ../ +dotnet new --install ../ \ No newline at end of file diff --git a/scripts/rename.csx b/scripts/rename.csx new file mode 100644 index 0000000..cb7672a --- /dev/null +++ b/scripts/rename.csx @@ -0,0 +1,27 @@ +#r "nuget: NSExt, 1.1.0" +using NSExt.Extensions; + +Console.WriteLine("请输入原始名称(NSExt):"); +var oldName = Console.ReadLine().NullOrEmpty("NSExt"); +Console.WriteLine("请输入替换名称:"); +var newName = Console.ReadLine(); +foreach (var path in Directory.EnumerateDirectories("../", $"*{oldName}*", + SearchOption.AllDirectories)) +{ + Console.Write($"{path} --> "); + var newPath = path.Replace(oldName, newName); + Directory.Move(path, newPath); + Console.WriteLine(newPath); +} + + +Console.WriteLine(); +foreach (var path in Directory.EnumerateFiles("../", $"*.*", SearchOption.AllDirectories)) +{ + File.WriteAllText(path, File.ReadAllText(path).Replace(oldName, newName)); + var newPath = path.Replace(oldName, newName); + if (newPath == path) continue; + Console.Write($"{path} --> "); + Directory.Move(path, newPath); + Console.WriteLine(newPath); +} \ No newline at end of file diff --git a/scripts/resharper.full.ps1 b/scripts/resharper.full.ps1 new file mode 100644 index 0000000..da69383 --- /dev/null +++ b/scripts/resharper.full.ps1 @@ -0,0 +1 @@ +dotnet jb cleanupcode --no-build ../NSExt.sln \ No newline at end of file diff --git a/scripts/switcher.nsext.json b/scripts/switcher.nsext.json new file mode 100644 index 0000000..118955a --- /dev/null +++ b/scripts/switcher.nsext.json @@ -0,0 +1,17 @@ +{ + "solution": "NSExt.sln", + "mappings": { + "NSExt": "../refs/ns-ext/src/NSExt/NSExt.csproj" + }, + "restore": [ + { + "name": "NSExt", + "packages": [ + { + "packageName": "NSExt", + "version": "1.1.0" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/switcher.ps1 b/scripts/switcher.ps1 new file mode 100644 index 0000000..41dafe7 --- /dev/null +++ b/scripts/switcher.ps1 @@ -0,0 +1,27 @@ +# https://github.com/RicoSuter/DNT#switch-to-projects +$targets = @{ + '1' = 'switch-to-projects' + '2' = 'switch-to-packages' +} +$key = '' +while ($null -eq $targets[$key]) +{ + $key = 读取-Host '请选择:1(切换到项目引用) 2(切换到Nuget包引用)' +} +$files = Get-ChildItem Switcher.*.json +$file = 9999 +while ($null -eq $files[[int]$file - 1]) +{ + $i = 0 + Write-Host '请选择要切换的配置文件文件' + foreach ($file in $files) + { + $i++ + Write-Host $i $file.Name + } + $file = 读取-Host +} +$file = [int]$file - 1 +Copy-Item $files[$file] 'switcher.json' -Force +dotnet dnt $targets[$key] ../NSExt.sln +Remove-Item switcher.json \ No newline at end of file diff --git a/scripts/sync.sln.files.csx b/scripts/sync.sln.files.csx new file mode 100644 index 0000000..201e1d6 --- /dev/null +++ b/scripts/sync.sln.files.csx @@ -0,0 +1,63 @@ +using System.Text.RegularExpressions; + +var slnFile = Directory.GetFiles(@"../", "*.sln").First(); +var content = File.ReadAllText(slnFile); + +content = Regex.Replace( + content, + "Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"meta\", \"meta\", \"{5198A03D-0CAC-4828-A807-34A693F73859}\"(?:.|\n)*?EndProject", + $$""" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{5198A03D-0CAC-4828-A807-34A693F73859}" +{{'\t'}}ProjectSection(SolutionItems) = preProject +{{string.Join('\n', + Directory.GetFiles(@"../", "*").Where(x => !x.EndsWith(".sln") && !x.EndsWith(".user")) + .Select(x=>$"\t\t{Path.GetFileName(x)} = {Path.GetFileName(x)}") + )}} +{{'\t'}}EndProject +""" +); + +content = Regex.Replace( + content, + "Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"workflows\", \"workflows\", \"{3C6F049E-3EE8-4D66-9AFF-E8A369032487}\"(?:.|\n)*?EndProject", + $$""" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{3C6F049E-3EE8-4D66-9AFF-E8A369032487}" +{{'\t'}}ProjectSection(SolutionItems) = preProject +{{string.Join('\n', + Directory.GetFiles(@"../.github/workflows", "*") + .Select(x=>$"\t\t{Path.GetFileName(x)} = .github/workflows/{Path.GetFileName(x)}") + )}} +{{'\t'}}EndProject +""" +); + +content = Regex.Replace( + content, + "Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"scripts\", \"scripts\", \"{BB0B25C9-0901-4923-913F-00F9A6B352A5}\"(?:.|\n)*?EndProject", + $$""" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{BB0B25C9-0901-4923-913F-00F9A6B352A5}" +{{'\t'}}ProjectSection(SolutionItems) = preProject +{{string.Join('\n', + Directory.GetFiles(@"../scripts", "*") + .Select(x=>$"\t\t{Path.GetFileName(x)} = scripts/{Path.GetFileName(x)}") + )}} +{{'\t'}}EndProject +""" +); + +content = Regex.Replace( + content, + "Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"build\", \"build\", \"{8E4C93BA-9493-4892-80C4-5E174C504829}\"(?:.|\n)*?EndProject", + $$""" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{8E4C93BA-9493-4892-80C4-5E174C504829}" +{{'\t'}}ProjectSection(SolutionItems) = preProject +{{string.Join('\n', + Directory.GetFiles(@"../build", "*") + .Select(x=>$"\t\t{Path.GetFileName(x)} = build/{Path.GetFileName(x)}") + )}} +{{'\t'}}EndProject +""" +); + +Console.WriteLine(content); +File.WriteAllText(slnFile, content); \ No newline at end of file diff --git a/src/backend/GlobalUsings.cs b/src/backend/GlobalUsings.cs new file mode 100644 index 0000000..bcf7404 --- /dev/null +++ b/src/backend/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using System; +global using System.ComponentModel; +global using System.Globalization; +global using System.Text; +global using System.Text.RegularExpressions; +global using NSExt.Languages; \ No newline at end of file diff --git a/src/backend/NSExt.Tests/NSExt.Tests.csproj b/src/backend/NSExt.Tests/NSExt.Tests.csproj new file mode 100644 index 0000000..a81c311 --- /dev/null +++ b/src/backend/NSExt.Tests/NSExt.Tests.csproj @@ -0,0 +1,14 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + \ No newline at end of file diff --git a/src/backend/NSExt/Attributes/LocalizationAttribute.cs b/src/backend/NSExt/Attributes/LocalizationAttribute.cs new file mode 100644 index 0000000..4c157a8 --- /dev/null +++ b/src/backend/NSExt/Attributes/LocalizationAttribute.cs @@ -0,0 +1,26 @@ +namespace NSExt.Attributes; + +/// +/// 指定本地化资源类型 +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Field)] +public sealed class LocalizationAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + #pragma warning disable IDE0290 + public LocalizationAttribute(Type resourceClass) + #pragma warning restore IDE0290 + { + ResourceClass = resourceClass; + } + + /// + /// Gets or sets 资源类型 + /// + /// + /// 资源类型 + /// + public Type ResourceClass { get; set; } +} \ No newline at end of file diff --git a/src/backend/NSExt/Attributes/ResourceDescriptionAttribute.cs b/src/backend/NSExt/Attributes/ResourceDescriptionAttribute.cs new file mode 100644 index 0000000..c2e4386 --- /dev/null +++ b/src/backend/NSExt/Attributes/ResourceDescriptionAttribute.cs @@ -0,0 +1,28 @@ +namespace NSExt.Attributes; + +/// +/// 本地化资源描述特性 +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Field)] +public sealed class ResourceDescriptionAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + #pragma warning disable IDE0290 + public ResourceDescriptionAttribute(string resourceName) + #pragma warning restore IDE0290 + { + ResourceName = resourceName; + } + + /// + /// 资源名称 + /// + public string ResourceName { get; set; } + + /// + /// 资源对象 + /// + public T ResourceObject { get; set; } +} \ No newline at end of file diff --git a/src/backend/NSExt/Constant/Regexes.cs b/src/backend/NSExt/Constant/Regexes.cs new file mode 100644 index 0000000..873927a --- /dev/null +++ b/src/backend/NSExt/Constant/Regexes.cs @@ -0,0 +1,21 @@ +namespace NSExt.Constant; +#pragma warning disable SYSLIB1045 + +/// +/// 使用 RegexGenerator 新特性会生成重复key值的xmlComment导致出错 +/// +internal static class Regexes +{ + public static readonly Regex RegexBacksLantUnicode + = new(@"\\u([a-fA-F0-9]{4})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex RegexHtmlTag = new("<[^>]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex RegexMobile + = new(@"^(\d{3})\d{4}(\d{4})$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex RegexPercentUnicode + = new(@"\\u([a-fA-F0-9]{4})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex RegexUpLetter = new("([A-Z])", RegexOptions.Compiled | RegexOptions.IgnoreCase); +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/ByteExtensions.cs b/src/backend/NSExt/Extensions/ByteExtensions.cs new file mode 100644 index 0000000..5c810e0 --- /dev/null +++ b/src/backend/NSExt/Extensions/ByteExtensions.cs @@ -0,0 +1,61 @@ +namespace NSExt.Extensions; + +/// +/// ByteExtensions +/// +public static class ByteExtensions +{ + /// + /// base64编码 + /// + /// me + /// 编码后的base64字符串 + public static string Base64(this byte[] me) + { + return Convert.ToBase64String(me); + } + + /// + /// 将字节数组解码成字符串 + /// + /// me + /// 字符串使用的编码方式 + /// 解码后的原始字符串 + public static string HexDe(this byte[] me, Encoding e) + { + return e.GetString(me); + } + + /// + /// 将字节数组解码成字符串 + /// + /// me + /// 解码后的原始字符串 + public static string HexDe(this byte[] me) + { + return me.HexDe(Encoding.UTF8); + } + + /// + /// 将字节数组转换成16进制字符串 + /// + /// me + /// 是否大写 + /// 字节间分隔符 + /// 分隔符跳跃字节数 + public static string Str(this IEnumerable me, bool upperCase = true, string splitShar = "" + , int splitInterval = 1) + { + var sb = new StringBuilder(); + var i = 0; + foreach (var c in me.Select(x => x.ToString(upperCase ? "X2" : "x2", CultureInfo.InvariantCulture))) { + if (i++ % splitInterval == 0) { + _ = sb.Append(splitShar); + } + + _ = sb.Append(c); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/CharExtensions.cs b/src/backend/NSExt/Extensions/CharExtensions.cs new file mode 100644 index 0000000..3d5438a --- /dev/null +++ b/src/backend/NSExt/Extensions/CharExtensions.cs @@ -0,0 +1,23 @@ +namespace NSExt.Extensions; + +/// +/// CharExtensions +/// +public static class CharExtensions +{ + /// + /// 是否数字或大小写字母 + /// + public static bool IsAsciiLetterOrDigit(this char me) + { + return (((uint)me - 'A') & ~0x20) < 26 || (uint)me - '0' < 10; + } + + /// + /// 是否base64字符 + /// + public static bool IsBase64Character(this char me) + { + return IsAsciiLetterOrDigit(me) || me is '+' or '/' or '='; + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/DateTimeExtensions.cs b/src/backend/NSExt/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..1d486e8 --- /dev/null +++ b/src/backend/NSExt/Extensions/DateTimeExtensions.cs @@ -0,0 +1,110 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable UnusedMember.Global + +#pragma warning disable SA1300, IDE1006 +namespace NSExt.Extensions; + +/// +/// DateTimeExtensions +/// +public static class DateTimeExtensions +{ + /// + /// 指定时间的世界协调时的unix时间戳形式 + /// + /// me + /// unix时间戳 + public static long TimeUnixUtc(this DateTime me) + { + return (me.ToUniversalTime().Ticks - 621355968000000000) / 10000000; + } + + /// + /// 指定时间的世界协调时的unix时间戳形式(毫秒) + /// + public static long TimeUnixUtcMs(this DateTime me) + { + return (me.ToUniversalTime().Ticks - 621355968000000000) / 10000; + } + + /// + /// ToString 的 Invariant 版本 + /// + public static string ToInvString(this DateTime me) + { + return me.ToString(CultureInfo.InvariantCulture); + } + + /// + /// 将一个过去时间对象与当前时间相减转换成“xx以前”的字符串, 如2秒以前, 3天以前 + /// + /// me + /// 字符串 + public static string UtcTimeAgo(this DateTime me) + { + var ts = DateTime.UtcNow - me; + return ts.Days switch { + > 0 => ts.Days + "天前" + , _ => ts.Hours switch { + > 0 => ts.Hours + "小时前" + , _ => ts.Minutes switch { > 0 => ts.Minutes + "分钟前", _ => ts.Seconds + "秒前" } + } + }; + } + + /// + /// yyyy_MM + /// + public static string yyyy_MM(this DateTime me) + { + return me.ToString("yyyy-MM", CultureInfo.InvariantCulture); + } + + /// + /// yyyy_MM_dd + /// + public static string yyyy_MM_dd(this DateTime me) + { + return me.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } + + /// + /// yyyy_MM_dd_HH_mm + /// + public static string yyyy_MM_dd_HH_mm(this DateTime me) + { + return me.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); + } + + /// + /// yyyy_MM_dd_HH_mm_ss + /// + public static string yyyy_MM_dd_HH_mm_ss(this DateTime me) + { + return me.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + } + + /// + /// yyyy_MM_dd_HH_mm_ss_fff + /// + public static string yyyy_MM_dd_HH_mm_ss_fff(this DateTime me) + { + return me.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); + } + + /// + /// yyyyMM + /// + public static string yyyyMM(this DateTime me) + { + return me.ToString("yyyyMM", CultureInfo.InvariantCulture); + } + + /// + /// yyyyMMdd + /// + public static string yyyyMMdd(this DateTime me) + { + return me.ToString("yyyyMMdd", CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/DbCommandExtensions.cs b/src/backend/NSExt/Extensions/DbCommandExtensions.cs new file mode 100644 index 0000000..e95758d --- /dev/null +++ b/src/backend/NSExt/Extensions/DbCommandExtensions.cs @@ -0,0 +1,39 @@ +using System.Data; +using System.Data.Common; + +namespace NSExt.Extensions; + +/// +/// DbCommandExtensions +/// +public static class DbCommandExtensions +{ + /// + /// 格式化参数拼接成完整的SQL语句 + /// + public static string ParameterFormat(this DbCommand me) + { + var sql = me.CommandText; + + // 应逆向替换,否则由于 多个表的过滤器问题导致替换不完整 如 @TenantId1 @TenantId10 + for (var i = me.Parameters.Count - 1; i >= 0; i--) { + #pragma warning disable IDE0072 + sql = me.Parameters[i].DbType switch { + #pragma warning restore IDE0072 + DbType.String or DbType.DateTime or DbType.Date or DbType.Time or DbType.DateTime2 + or DbType.DateTimeOffset or DbType.Guid or DbType.VarNumeric or DbType.AnsiStringFixedLength + or DbType.AnsiString or DbType.StringFixedLength => sql.Replace( // + me.Parameters[i].ParameterName, "'" + me.Parameters[i].Value + "'") + , DbType.Boolean => sql.Replace( // + me.Parameters[i].ParameterName + , me.Parameters[i].Value != DBNull.Value && + Convert.ToBoolean(me.Parameters[i].Value, CultureInfo.InvariantCulture) + ? "1" + : "0") + , _ => sql.Replace(me.Parameters[i].ParameterName, me.Parameters[i].Value?.ToString()) + }; + } + + return sql; + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/DecimalExtensions.cs b/src/backend/NSExt/Extensions/DecimalExtensions.cs new file mode 100644 index 0000000..7f6f46e --- /dev/null +++ b/src/backend/NSExt/Extensions/DecimalExtensions.cs @@ -0,0 +1,26 @@ +namespace NSExt.Extensions; + +/// +/// DecimalExtensions +/// +public static class DecimalExtensions +{ + /// + /// 四舍五入后的近似值 + /// + /// me + /// 小数点位数 + /// 处理后的值 + public static decimal Round(this decimal me, int place) + { + return Math.Round(me, place); + } + + /// + /// ToString 的 Invariant 版本 + /// + public static string ToInvString(this decimal me) + { + return me.ToString(CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/EnumExtensions.cs b/src/backend/NSExt/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..b40f675 --- /dev/null +++ b/src/backend/NSExt/Extensions/EnumExtensions.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using NSExt.Attributes; + +namespace NSExt.Extensions; + +/// +/// EnumExtensions +/// +public static class EnumExtensions +{ + /// + /// 获取显示特性 + /// + public static DisplayAttribute GetDisplay(this Enum me) + { + return me.GetAttributeOfType(); + } + + /// + /// 获取枚举的本地化资源描述 + /// + public static string ResDesc(this Enum e) + { + var typeOfEnum = e.GetType(); + var typeOfField = typeOfEnum.GetField(Enum.GetName(typeOfEnum, e)!); + var resDescAttr = typeOfField!.GetCustomAttribute>(true); + return resDescAttr is null + ? Enum.GetName(typeOfEnum, e) + : typeof(T).GetProperty(resDescAttr.ResourceName)?.GetValue(default) as string; + } + + /// + /// 通过类泛型类型获取特性 + /// + private static T GetAttributeOfType(this Enum me) + where T : Attribute + { + return me.GetType().GetMember(me.ToString())[0].GetCustomAttributes(false).FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/EnumerableExtensions.cs b/src/backend/NSExt/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..08eef9f --- /dev/null +++ b/src/backend/NSExt/Extensions/EnumerableExtensions.cs @@ -0,0 +1,26 @@ +namespace NSExt.Extensions; + +/// +/// EnumerableExtensions +/// +public static class EnumerableExtensions +{ + /// + /// 将列表转成分隔符分隔的字符串 + /// + public static string Join(this IEnumerable me, string separator) + { + return string.Join(separator, me); + } + + /// + /// 判断对象是否为null或不存在子元素(如果为集合对象) + /// + /// 对象类型 + /// me + /// 空则返回true + public static bool NullOrEmpty(this IEnumerable me) + { + return me?.Any() != true; + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/GenericExtensions.cs b/src/backend/NSExt/Extensions/GenericExtensions.cs new file mode 100644 index 0000000..8015c1c --- /dev/null +++ b/src/backend/NSExt/Extensions/GenericExtensions.cs @@ -0,0 +1,41 @@ +namespace NSExt.Extensions; + +/// +/// GenericExtensions +/// +public static class GenericExtensions +{ + /// + /// 从指定的对象拷贝属性 + /// + /// 对象类型 + /// me + /// 拷贝来源 + /// 需要处理的属性名 + /// True包含,false排除 + public static void CopyFrom(this T me, T copyObj, IList propNameList = null + , bool isIncludeOrExclude = false) + { + foreach (var p in me.GetType().GetProperties()) { + if (!p.CanWrite) { + continue; + } + + var isSet = isIncludeOrExclude + ? propNameList?.Contains(p.Name) ?? false + : !propNameList?.Contains(p.Name) ?? true; + if (isSet) { + p.SetValue(me, copyObj.GetType().GetProperty(p.Name)?.GetValue(copyObj, null), null); + } + } + } + + /// + /// 判断是否与某对象相等 + /// + public static T Is(this T me, T compare, T ret) + where T : struct + { + return me.Equals(compare) ? ret : me; + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/IntExtensions.cs b/src/backend/NSExt/Extensions/IntExtensions.cs new file mode 100644 index 0000000..ecf2270 --- /dev/null +++ b/src/backend/NSExt/Extensions/IntExtensions.cs @@ -0,0 +1,41 @@ +namespace NSExt.Extensions; + +/// +/// IntExtensions +/// +public static class IntExtensions +{ + /// + /// 判断枚举是否包含某个位 + /// + public static bool HasFlag(this int me, T flag) + where T : Enum + { + return ((long)me).HasFlag(flag); + } + + /// + /// 生成随机数 + /// + /// me + public static int Rand(this int[] me) + { + return new Random(Guid.NewGuid().GetHashCode()).Next(me[0], me[1]); + } + + /// + /// ToString 的 Invariant 版本 + /// + public static string ToInvString(this int me) + { + return me.ToString(CultureInfo.InvariantCulture); + } + + /// + /// 转换成ipv4 + /// + public static string ToIpV4(this int me) + { + return string.Join(".", BitConverter.GetBytes(me).Reverse()); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/LoggerExtensions.cs b/src/backend/NSExt/Extensions/LoggerExtensions.cs new file mode 100644 index 0000000..ca694c2 --- /dev/null +++ b/src/backend/NSExt/Extensions/LoggerExtensions.cs @@ -0,0 +1,86 @@ +// ReSharper disable TemplateIsNotCompileTimeConstantProblem + +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; + +namespace NSExt.Extensions; + +/// +/// LoggerExtensions +/// +public static class LoggerExtensions +{ + private const string _MESSAGE_S_THREAD_ID_CALLER_NAME_CALLER_FILE_PATH_CALLER_LINE_NUMBER + = "{Message} "; + + private static readonly Action _logDebug + = LoggerMessage.Define(LogLevel.Debug, default + , _MESSAGE_S_THREAD_ID_CALLER_NAME_CALLER_FILE_PATH_CALLER_LINE_NUMBER); + + private static readonly Action _logError + = LoggerMessage.Define(LogLevel.Error, default + , _MESSAGE_S_THREAD_ID_CALLER_NAME_CALLER_FILE_PATH_CALLER_LINE_NUMBER); + + private static readonly Action _logFatal + = LoggerMessage.Define(LogLevel.Critical, default + , _MESSAGE_S_THREAD_ID_CALLER_NAME_CALLER_FILE_PATH_CALLER_LINE_NUMBER); + + private static readonly Action _logInfo + = LoggerMessage.Define(LogLevel.Information, default + , _MESSAGE_S_THREAD_ID_CALLER_NAME_CALLER_FILE_PATH_CALLER_LINE_NUMBER); + + private static readonly Action _logWarn + = LoggerMessage.Define(LogLevel.Warning, default + , _MESSAGE_S_THREAD_ID_CALLER_NAME_CALLER_FILE_PATH_CALLER_LINE_NUMBER); + + /// + /// Debug + /// + public static void Debug(this ILogger me, object message, [CallerMemberName] string callerName = null + , [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + { + _logDebug(me, message.ToString(), callerName, Path.GetFileName(callerFilePath) + , callerLineNumber.ToString(CultureInfo.InvariantCulture), null); + } + + /// + /// Error + /// + public static void Error(this ILogger me, object message, [CallerMemberName] string callerName = null + , [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + { + _logError(me, message.ToString(), callerName, Path.GetFileName(callerFilePath) + , callerLineNumber.ToString(CultureInfo.InvariantCulture), null); + } + + /// + /// Fatal + /// + public static void Fatal(this ILogger me, object message, Exception ex = null + , [CallerMemberName] string callerName = null, [CallerFilePath] string callerFilePath = null + , [CallerLineNumber] int callerLineNumber = 0) + { + _logFatal(me, message.ToString(), callerName, Path.GetFileName(callerFilePath) + , callerLineNumber.ToString(CultureInfo.InvariantCulture), ex); + } + + /// + /// Info + /// + public static void Info(this ILogger me, object message, [CallerMemberName] string callerName = null + , [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + { + _logInfo(me, message.ToString(), callerName, Path.GetFileName(callerFilePath) + , callerLineNumber.ToString(CultureInfo.InvariantCulture), null); + } + + /// + /// Warn + /// + public static void Warn(this ILogger me, object message, [CallerMemberName] string callerName = null + , [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0) + { + _logWarn(me, message.ToString(), callerName, Path.GetFileName(callerFilePath) + , callerLineNumber.ToString(CultureInfo.InvariantCulture), null); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/LongExtensions.cs b/src/backend/NSExt/Extensions/LongExtensions.cs new file mode 100644 index 0000000..6ea66b9 --- /dev/null +++ b/src/backend/NSExt/Extensions/LongExtensions.cs @@ -0,0 +1,42 @@ +namespace NSExt.Extensions; + +/// +/// LongExtensions +/// +public static class LongExtensions +{ + /// + /// 判断枚举是否包含某个位 + /// + public static bool HasFlag(this long me, T flag) + where T : Enum + { + var val = (long)(object)flag; + return (me & val) == val; + } + + /// + /// 生成随机数 + /// + /// me + public static long Rand(this long[] me) + { + return new Random(Guid.NewGuid().GetHashCode()).NextInt64(me[0], me[1]); + } + + /// + /// 1970毫秒数转换成日期对象 + /// + public static DateTime Time(this long msFrom1970) + { + return DateTime.UnixEpoch.AddMilliseconds(msFrom1970).ToLocalTime(); + } + + /// + /// ToString 的 Invariant 版本 + /// + public static string ToInvString(this long me) + { + return me.ToString(CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/ObjectExtensions.cs b/src/backend/NSExt/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..99c5dcc --- /dev/null +++ b/src/backend/NSExt/Extensions/ObjectExtensions.cs @@ -0,0 +1,30 @@ +using System.Text.Json; + +namespace NSExt.Extensions; + +/// +/// ObjectExtensions +/// +public static class ObjectExtensions +{ + /// + /// 将一个对象序列化成json文本 + /// + /// me + /// json文本 + public static string Json(this object me) + { + return JsonSerializer.Serialize(me); + } + + /// + /// 将一个对象序列化成json文本 + /// + /// me + /// 序列化选项 + /// json文本 + public static string Json(this object me, JsonSerializerOptions options) + { + return JsonSerializer.Serialize(me, options); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/StreamExtensions.cs b/src/backend/NSExt/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..0c8e61e --- /dev/null +++ b/src/backend/NSExt/Extensions/StreamExtensions.cs @@ -0,0 +1,32 @@ +namespace NSExt.Extensions; + +/// +/// StreamExtensions +/// +public static class StreamExtensions +{ + /// + /// FirstByteIndex + /// + public static long FirstByteIndex(this Stream me, byte[] findBytes) + { + int data; + while ((data = me.ReadByte()) != -1) { + if (findBytes.Contains((byte)data)) { + return me.Position; + } + } + + return -1; + } + + /// + /// IsTextStream + /// + public static bool IsTextStream(this Stream me) + { + #pragma warning disable IDE0300 + return me.FirstByteIndex(new byte[] { 0x00, 0xff }) < 0; + #pragma warning restore IDE0300 + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/StringExtensions.cs b/src/backend/NSExt/Extensions/StringExtensions.cs new file mode 100644 index 0000000..80c49fe --- /dev/null +++ b/src/backend/NSExt/Extensions/StringExtensions.cs @@ -0,0 +1,594 @@ +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global + +#pragma warning disable CA1720 +using System.Security.Cryptography; +using System.Text.Json; +using System.Web; +using NSExt.Constant; + +namespace NSExt.Extensions; + +/// +/// StringExtensions +/// +#pragma warning disable CodeLinesAnalyzer +public static class StringExtensions +{ + /// + /// aes加密 + /// + /// me + /// 密钥 + public static string Aes(this string me, string key) + { + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Padding = PaddingMode.PKCS7; + aes.Mode = CipherMode.ECB; + aes.Key = key.Hex(); + using var encryptor = aes.CreateEncryptor(); + var bytes = me.Hex(); + var decrypted = encryptor.TransformFinalBlock(bytes, 0, bytes.Length); + return decrypted.Base64(); + } + + /// + /// aes解密 + /// + /// me + /// 密钥 + public static string AesDe(this string me, string key) + { + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Padding = PaddingMode.PKCS7; + aes.Mode = CipherMode.ECB; + aes.Key = key.Hex(); + using var encryptor = aes.CreateDecryptor(); + var bytes = me.Base64De(); + var decrypted = encryptor.TransformFinalBlock(bytes, 0, bytes.Length); + return decrypted.HexDe(); + } + + /// + /// base64编码 + /// + /// me + /// 字符串的编码方式 + /// 编码后的base64字符串 + public static string Base64(this string me, Encoding e) + { + return e.GetBytes(me).Base64(); + } + + /// + /// base64解码 + /// + /// me + /// 解码后的原始字节数组 + public static byte[] Base64De(this string me) + { + return Convert.FromBase64String(me); + } + + /// + /// base64解码 + /// + /// me + /// 字符串的编码方式 + /// 解码后的原始字符串 + public static string Base64De(this string me, Encoding e) + { + return e.GetString(me.Base64De()); + } + + /// + /// 将易于web传输的base64web字符串转换为原生base64 + /// + /// 原生base64 + public static string Base64Sys(this string me) + { + return me.Replace("-", "+").Replace("_", "/").Replace(".", "="); + } + + /// + /// 将原生base64字符串转换成易于web传输的字符串 + /// + /// 易于web传输的字符串 + public static string Base64Web(this string me) + { + return me.Replace("+", "-").Replace("/", "_").Replace("=", "."); + } + + /// + /// 将字符串转换成日期对象 + /// + /// me + /// 转换后的日期对象 + public static DateTime DateTime(this string me) + { + return System.DateTime.Parse(me, CultureInfo.CurrentCulture); + } + + /// + /// 将字符串转换成日期对象 + /// + /// me + /// 日期格式 + /// 转换后的日期对象 + public static DateTime DateTimeExact(this string me, string format) + { + return System.DateTime.ParseExact(me, format, CultureInfo.CurrentCulture); + } + + /// + /// 将字符串转换成日期对象 + /// + /// me + /// 日期格式 + /// 转换失败时返回的日期对象 + /// 转换后的日期对象 + public static DateTime DateTimeExactTry(this string me, string format, DateTime def) + { + return !System.DateTime.TryParseExact(me, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var ret) + ? def + : ret; + } + + /// + /// 将字符串转换成日期对象 + /// + /// me + /// 转换失败时返回的日期对象 + /// 转换后的日期对象 + public static DateTime DateTimeTry(this string me, DateTime def) + { + return !System.DateTime.TryParse(me, CultureInfo.InvariantCulture, DateTimeStyles.None, out var ret) + ? def + : ret; + } + + /// + /// string to decimal + /// + /// me + /// decimal + public static decimal Dec(this string me) + { + return decimal.Parse(me, CultureInfo.CurrentCulture); + } + + /// + /// 尝试将字符串转为decimal + /// + /// me + /// 转换失败后返回的默认值 + /// 转换后的decimal + public static decimal DecTry(this string me, decimal def) + { + return !decimal.TryParse(me, out var ret) ? def : ret; + } + + /// + /// string to double + /// + /// me + /// Int32 + public static double Double(this string me) + { + return double.Parse(me, CultureInfo.CurrentCulture); + } + + /// + /// 将字符串转换成枚举对象 + /// + public static T Enum(this string name) + where T : Enum + { + return (T)System.Enum.Parse(typeof(T), name, true); + } + + /// + /// 将字符串转换成枚举对象 + /// + public static T EnumTry(this string name, T def) + where T : Enum + { + return !System.Enum.TryParse(typeof(T), name, out var ret) ? def : (T)ret; + } + + /// + /// string to float + /// + /// me + /// Int32 + public static float Float(this string me) + { + return float.Parse(me, CultureInfo.CurrentCulture); + } + + /// + /// 将字符串转为guid + /// + /// me + public static Guid Guid(this string me) + { + return System.Guid.Parse(me); + } + + /// + /// 将字符串转换成guid + /// + /// me + /// 转换失败的返回值 + public static Guid Guid(this string me, Guid def) + { + return System.Guid.TryParse(me, out var ret) ? ret : def; + } + + /// + /// 将字符串转换成字节数组形式 + /// + /// me + /// 字符串使用的编码 + /// 字节数组 + public static byte[] Hex(this string me, Encoding e) + { + return e.GetBytes(me); + } + + /// + /// 将字符串转换成字节数组形式 + /// + /// me + /// 字节数组 + public static byte[] Hex(this string me) + { + return me.Hex(Encoding.UTF8); + } + + /// + /// 对一个字符串进行sha1 hash运算 + /// + /// me + /// 密钥 + /// 使用的编码 + /// hash摘要的16进制文本形式(无连字符小写) + public static string HmacSha1(this string me, string secret, Encoding e) + { + #pragma warning disable CA5350 + using var hmacSha1 = new HMACSHA1(e.GetBytes(secret)); + #pragma warning restore CA5350 + + return BitConverter.ToString(hmacSha1.ComputeHash(e.GetBytes(me))) + .Replace("-", string.Empty) + .ToLower(CultureInfo.CurrentCulture); + } + + /// + /// html编码 + /// + public static string Html(this string me) + { + return HttpUtility.HtmlEncode(me); + } + + /// + /// 解码html编码 + /// + /// me + /// 解码后的原始字符串 + public static string HtmlDe(this string me) + { + return HttpUtility.HtmlDecode(me); + } + + /// + /// string to Int32 + /// + /// me + /// Int32 + public static int Int32(this string me) + { + return int.Parse(me, CultureInfo.CurrentCulture); + } + + /// + /// 尝试将字符串转为int32 + /// + /// me + /// 转换失败后返回的默认值 + /// 转换后的int32 + public static int Int32Try(this string me, int def) + { + return !int.TryParse(me, out var ret) ? def : ret; + } + + /// + /// string to Int64 + /// + /// me + /// Int64 + public static long Int64(this string me) + { + return long.Parse(me, CultureInfo.CurrentCulture); + } + + /// + /// 尝试将字符串转为int64 + /// + /// me + /// 转换失败后返回的默认值 + /// 转换后的int64 + public static long Int64Try(this string me, long def) + { + return !long.TryParse(me, out var ret) ? def : ret; + } + + /// + /// ipv4格式转int32格式 + /// + public static int IpV4ToInt32(this string me) + { + return BitConverter.ToInt32(me.Split('.').Select(byte.Parse).Reverse().ToArray(), 0); + } + + /// + /// 是否base64字符串 + /// + /// me + public static bool IsBase64String(this string me) + { + // 一个合法的Base64,有着以下特征: + // 字符串的长度为4的整数倍。 + // 字符串的符号取值只能在A -Z, a -z, 0 -9, +, /, =共计65个字符中,且 = 如果出现就必须在结尾出现。 + if (!me.All(x => x.IsBase64Character())) { + return false; + } + + if (me.Length % 4 != 0) { + return false; + } + + var firstEqualSignPos = me.IndexOf('='); + if (firstEqualSignPos < 0) { + return true; + } + + var lastEqualSignPos = me.LastIndexOf('='); + return lastEqualSignPos == me.Length - 1 && me[firstEqualSignPos..lastEqualSignPos].All(x => x == '='); + } + + /// + /// 中文姓名打马赛克 + /// + public static string MaskChineseName(this string me) + { + return me.Length == 2 ? "*" + me[1..] : me[..1] + "*" + me[^1..]; + } + + /// + /// 对一个手机号进行掩码处理 + /// + /// me + /// 掩码后的手机号 + public static string MaskMobile(this string me) + { + return Regexes.RegexMobile.Replace(me, "$1****$2"); + } + + /// + /// 对一个字符串进行md5hash运算 + /// + /// me + /// 字符串使用的编码 + /// hash摘要的16进制文本形式(无连字符小写) + public static string Md5(this string me, Encoding e) + { + #pragma warning disable CA5351 + return BitConverter.ToString(MD5.HashData(e.GetBytes(me))) + #pragma warning restore CA5351 + .Replace("-", string.Empty) + .ToLower(CultureInfo.CurrentCulture); + } + + /// + /// 判断字符串是否为null或不存在子元素(如果为集合对象);如果为空,返回指定的默认值,否则返回字符串本身 + /// + /// me + /// 指定的默认值 + /// 如果为空,返回指定的默认值,否则返回字符串本身 + public static string NullOrEmpty(this string me, string defVal) + { + return me.AsEnumerable().NullOrEmpty() ? defVal : me; + } + + /// + /// null或空白字符 + /// + public static bool NullOrWhiteSpace(this string me) + { + return string.IsNullOrWhiteSpace(me); + } + + /// + /// 反序列化一个文件获得指定类型的数据对象 + /// + /// me + /// 序列化选项 + /// 反序列化后生成的对象 + public static T Object(this string me, JsonSerializerOptions options = null) + { + return JsonSerializer.Deserialize(me, options); + } + + /// + /// 反序列化一个文件获得指定类型的数据对象 + /// + /// me + /// 实际类型 + /// 序列化选项 + /// 反序列化后生成的对象 + public static object Object(this string me, Type type, JsonSerializerOptions options = null) + { + return JsonSerializer.Deserialize(me, type, options); + } + + /// + /// 生成密码 + /// + /// me + /// 密文 + public static string Pwd(this string me) + { + return me.Md5Hmac(me.Md5(Encoding.UTF8), Encoding.UTF8); + } + + /// + /// 移除字符串中的html标签 + /// + /// me + /// 处理之后的字符串 + public static string RemoveHtmlTag(this string me) + { + return Regexes.RegexHtmlTag.Replace(me, string.Empty); + } + + /// + /// 删除换行符 + /// + public static string RemoveWrapped(this string me) + { + return me.Replace("\r", string.Empty).Replace("\n", string.Empty); + } + + /// + /// 对一个字符串进行sha1 hash运算 + /// + /// me + /// 字符串使用的编码 + /// hash摘要的16进制文本形式(无连字符小写) + public static string Sha1(this string me, Encoding e) + { + #pragma warning disable CA5350 + return BitConverter.ToString(SHA1.HashData(e.GetBytes(me))) + #pragma warning restore CA5350 + .Replace("-", string.Empty) + .ToLower(CultureInfo.CurrentCulture); + } + + /// + /// 蛇形命名 + /// + public static string SnakeCase(this string me) + { + return Regexes.RegexUpLetter.Replace(me, "-$1").ToLower(CultureInfo.InvariantCulture).TrimStart('-'); + } + + /// + /// 截取指定长度的字符串,代替substring + /// + public static string Sub(this string me, int startIndex, int length) + { + if (startIndex + length > me.Length) { + length = me.Length - startIndex; + } + + return me.Substring(startIndex, length); + } + + /// + /// 纯文本字符串转html + /// + public static string Text2Html(this string me) + { + return $"
{me}
"; + } + + /// + /// 首字母小写 + /// + public static string ToLowerCamelCase(this string me) + { + return string.IsNullOrWhiteSpace(me) + ? me + : string.Concat( // + me[0].ToString(CultureInfo.InvariantCulture).ToLowerInvariant(), me.AsSpan(1)); + } + + /// + /// 首字母大写 + /// + public static string ToUpperCamelCase(this string me) + { + return string.IsNullOrWhiteSpace(me) ? me : string.Concat(me[0].ToString().ToUpperInvariant(), me.AsSpan(1)); + } + + /// + /// 将连续多个空格替换成一个空格 + /// + public static string TrimSpaces(this string me) + { + var ret = me.Replace(" ", " "); + + // ReSharper disable once TailRecursiveCall + return ret == me ? ret : ret.TrimSpaces(); + } + + /// + /// 将\ux0000 、 %u0000 、 &#x0000; 编码转换成可读字符串 + /// + public static string UnicodeDe(this string me) + { + const string replacement = "&#x$1;"; + if (me.Contains(@"\u")) { + return Regexes.RegexBacksLantUnicode.Replace(me, replacement).HtmlDe(); + } + + // ReSharper disable once ConvertIfStatementToReturnStatement + #pragma warning disable IDE0046 + if (me.Contains("%u")) { + #pragma warning restore IDE0046 + return Regexes.RegexPercentUnicode.Replace(me, replacement).HtmlDe(); + } + + return me.HtmlDe(); + } + + /// + /// url编码 + /// + /// me + /// url编码后的字符串 + public static string Url(this string me) + { + return Uri.EscapeDataString(me); + } + + /// + /// 解码url编码 + /// + /// me + /// 解码后的原始字符串 + public static string UrlDe(this string me) + { + return Uri.UnescapeDataString(me); + } + + /// + /// MD5 hmac编码 + /// + /// me + /// 密钥 + /// 字符串使用的编码 + /// hash摘要的16进制文本形式(无连字符小写) + private static string Md5Hmac(this string me, string key, Encoding e) + { + #pragma warning disable CA5351 + using var md5Hmac = new HMACMD5(e.GetBytes(key)); + #pragma warning restore CA5351 + return BitConverter.ToString(md5Hmac.ComputeHash(e.GetBytes(me))) + .Replace("-", string.Empty) + .ToLower(CultureInfo.CurrentCulture); + } +} +#pragma warning restore CodeLinesAnalyzer \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/TypeExtensions.cs b/src/backend/NSExt/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..ea897bc --- /dev/null +++ b/src/backend/NSExt/Extensions/TypeExtensions.cs @@ -0,0 +1,19 @@ +namespace NSExt.Extensions; + +/// +/// TypeExtensions +/// +public static class TypeExtensions +{ + /// + /// 搜索此成员的继承链以查找自定义属性,接口也会被搜索。 + /// + public static IEnumerable GetCustomAttributesIncludingBaseInterfaces(this Type me) + { + var attributeType = typeof(T); + return me.GetCustomAttributes(attributeType, true) + .Union(me.GetInterfaces() + .SelectMany(interfaceType => interfaceType.GetCustomAttributes(attributeType, true))) + .Cast(); + } +} \ No newline at end of file diff --git a/src/backend/NSExt/Extensions/UriExtensions.cs b/src/backend/NSExt/Extensions/UriExtensions.cs new file mode 100644 index 0000000..6f9377a --- /dev/null +++ b/src/backend/NSExt/Extensions/UriExtensions.cs @@ -0,0 +1,15 @@ +namespace NSExt.Extensions; + +/// +/// UriExtensions +/// +public static class UriExtensions +{ + /// + /// 移除url的Scheme + /// + public static string RemoveScheme(this Uri me) + { + return "//" + me.Authority + me.PathAndQuery; + } +} \ No newline at end of file diff --git a/src/backend/NSExt/NSExt.csproj b/src/backend/NSExt/NSExt.csproj new file mode 100644 index 0000000..5d6fabe --- /dev/null +++ b/src/backend/NSExt/NSExt.csproj @@ -0,0 +1,31 @@ + + + ../../../key.snk + false + true + true + true + true + true + logo.png + MIT + https://github.com/nsnail/NSExt.git + extensions + true + NSExt + true + snupkg + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file