From 508718573de5618343f4a597ab5308726e7f8782 Mon Sep 17 00:00:00 2001 From: james Date: Tue, 10 Nov 2026 19:15:35 +0000 Subject: [PATCH] chore(import): snapshot james.dad.town/gengo at 8f775550b0ee --- .all-contributorsrc | 154 + .devcontainer/Dockerfile | 3 + .devcontainer/devcontainer.json | 28 + .devcontainer/postCreate.sh | 5 + .dockerignore | 14 + .editorconfig | 43 + .git-blame-ignore-revs | 24 + .gitattributes | 11 + .github/ISSUE_TEMPLATE/new-language.yml | 87 + .github/PULL_REQUEST_TEMPLATE/new_language.md | 4 + .github/PULL_REQUEST_TEMPLATE/other.md | 1 + .github/dependabot.yml | 22 + .github/release.yml | 30 + .github/workflows/ci.yml | 169 ++ .github/workflows/label-new-language-prs.yml | 60 + .github/workflows/release.yml | 121 + .gitignore | 15 + .taplo.toml | 5 + .vscode/extensions.json | 8 + .vscode/settings.json | 15 + CODEOWNERS | 2 + CREDITS.md | 37 + Cargo.lock | 2548 +++++++++++++++++ Cargo.toml | 23 + Dockerfile | 10 + LICENSE-APACHE | 201 ++ LICENSE-MIT | 21 + README.md | 83 + crates-io.md | 62 + docs/CONTRIBUTING.md | 43 + docs/HACKING.md | 114 + docs/INSTALLATION.md | 72 + docs/RELEASING.md | 18 + docs/analysis-flowchart.mmd | 31 + docs/snippets.md | 12 + gengo-bin/Cargo.toml | 36 + gengo-bin/LICENSE-APACHE | 1 + gengo-bin/LICENSE-MIT | 1 + gengo-bin/README.md | 1 + gengo-bin/crates-io.md | 1 + gengo-bin/src/bin/gengo.rs | 8 + gengo-bin/src/cli.rs | 303 ++ gengo-bin/src/lib.rs | 1 + .../test_cli__breakdown_javascript_repo.snap | 18 + ..._cli__color_breakdown_javascript_repo.snap | 16 + .../test_cli__color_javascript_repo.snap | 7 + .../snapshots/test_cli__javascript_repo.snap | 8 + ...t_cli__json_output_on_javascript_repo.snap | 71 + gengo-bin/tests/test_cli.rs | 98 + gengo-language/Cargo.toml | 25 + gengo-language/LICENSE-APACHE | 1 + gengo-language/LICENSE-MIT | 1 + gengo-language/README.md | 3 + gengo-language/build.rs | 630 ++++ gengo-language/languages.yaml | 1152 ++++++++ gengo-language/src/lib.rs | 312 ++ gengo-language/tests/test.rs | 96 + gengo/Cargo.toml | 56 + gengo/LICENSE-APACHE | 1 + gengo/LICENSE-MIT | 1 + gengo/README.md | 1 + gengo/benches/run_on_self.rs | 20 + gengo/crates-io.md | 1 + gengo/src/analysis/mod.rs | 45 + gengo/src/analysis/summary.rs | 46 + gengo/src/binary.rs | 53 + gengo/src/builder.rs | 50 + gengo/src/documentation.rs | 76 + gengo/src/error.rs | 64 + gengo/src/file_source/directory.rs | 91 + gengo/src/file_source/git.rs | 213 ++ gengo/src/file_source/mod.rs | 114 + gengo/src/generated.rs | 102 + gengo/src/language/mod.rs | 1 + gengo/src/lib.rs | 228 ++ gengo/src/vendored.rs | 58 + gengo/tests/gengo_test.rs | 16 + .../snapshots/gengo_test__git_javascript.snap | 61 + install.ps1 | 31 + install.sh | 37 + languages.yaml | 1 + rust-toolchain.toml | 3 + samples-test/Cargo.toml | 13 + .../samples/Batch File/calculator.bat | 32 + samples-test/samples/Batch File/sysinfo.cmd | 12 + samples-test/samples/C++/progress-bar.cpp | 90 + samples-test/samples/C++/progress-bar.h | 30 + samples-test/samples/C/progress-bar.c | 115 + samples-test/samples/C/progress-bar.h | 24 + samples-test/samples/CSS/style.css | 51 + samples-test/samples/Common Lisp/cp.lisp | 12 + samples-test/samples/Gleam/math.gleam | 82 + samples-test/samples/Go/language_stats.go | 81 + samples-test/samples/HCL/spec.hcl | 320 +++ samples-test/samples/HTML/site.html | 74 + samples-test/samples/INI/.editorconfig | 43 + .../samples/Ignore List/.dockerignore | 13 + samples-test/samples/Ignore List/.gitignore | 3 + samples-test/samples/Ignore List/.npmignore | 5 + .../samples/Ignore List/.vercelignore | 13 + .../JSON with Comments/devcontainer.json | 14 + .../samples/JSON with Comments/tsconfig.json | 15 + samples-test/samples/JSON/data.json | 16 + samples-test/samples/JavaScript/button.jsx | 15 + .../samples/JavaScript/count_expression.js | 24 + samples-test/samples/Jule/generic.jule | 42 + .../samples/Jupyter Notebook/simple.ipynb | 34 + samples-test/samples/Makefile/Makefile | 30 + samples-test/samples/Markdown/sample.md | 34 + samples-test/samples/MoonBit/expr.mbt | 15 + samples-test/samples/MoonBit/pkg.mbti | 10 + samples-test/samples/MoonBit/script.mbtx | 10 + samples-test/samples/OpenSCAD/sFeeder.scad | 272 ++ samples-test/samples/Org/org.org | 32 + .../samples/PowerShell/check_for_updates.ps1 | 23 + samples-test/samples/Python/minecraft.py | 32 + samples-test/samples/Ruby/repofetch.rb | 138 + samples-test/samples/Rust/error.rs | 64 + samples-test/samples/SQL/job_info.sql | 6 + samples-test/samples/SVG/shapes.svg | 16 + samples-test/samples/Scheme/example.scm | 19 + samples-test/samples/Shell/rename.bash | 22 + samples-test/samples/Shell/rename.sh | 22 + samples-test/samples/Shell/rename.zsh | 22 + samples-test/samples/TOML/config.toml | 13 + samples-test/samples/TypeScript/App.tsx | 26 + samples-test/samples/TypeScript/calculate.ts | 17 + samples-test/samples/TypeSpec/main.tsp | 44 + samples-test/samples/XML/types.xml | 25 + samples-test/samples/YAML/dependabot.yml | 22 + samples-test/samples/fish/mkcd.fish | 4 + .../samples/reStructured Text/example.rst | 119 + samples-test/src/lib.rs | 73 + scoop/gengo.json | 13 + scripts/check-languages-file.rb | 96 + scripts/conflicts.rb | 41 + scripts/preview-nerd-font-glyphs.rb | 14 + scripts/should-tag-new-language.rb | 26 + scripts/update-scoop.rb | 23 + 139 files changed, 10752 insertions(+) create mode 100644 .all-contributorsrc create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/postCreate.sh create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .git-blame-ignore-revs create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/new-language.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE/new_language.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/other.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/label-new-language-prs.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .taplo.toml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CODEOWNERS create mode 100644 CREDITS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 crates-io.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/HACKING.md create mode 100644 docs/INSTALLATION.md create mode 100644 docs/RELEASING.md create mode 100644 docs/analysis-flowchart.mmd create mode 100644 docs/snippets.md create mode 100644 gengo-bin/Cargo.toml create mode 120000 gengo-bin/LICENSE-APACHE create mode 120000 gengo-bin/LICENSE-MIT create mode 120000 gengo-bin/README.md create mode 120000 gengo-bin/crates-io.md create mode 100644 gengo-bin/src/bin/gengo.rs create mode 100644 gengo-bin/src/cli.rs create mode 100644 gengo-bin/src/lib.rs create mode 100644 gengo-bin/tests/snapshots/test_cli__breakdown_javascript_repo.snap create mode 100644 gengo-bin/tests/snapshots/test_cli__color_breakdown_javascript_repo.snap create mode 100644 gengo-bin/tests/snapshots/test_cli__color_javascript_repo.snap create mode 100644 gengo-bin/tests/snapshots/test_cli__javascript_repo.snap create mode 100644 gengo-bin/tests/snapshots/test_cli__json_output_on_javascript_repo.snap create mode 100644 gengo-bin/tests/test_cli.rs create mode 100644 gengo-language/Cargo.toml create mode 120000 gengo-language/LICENSE-APACHE create mode 120000 gengo-language/LICENSE-MIT create mode 100644 gengo-language/README.md create mode 100644 gengo-language/build.rs create mode 100644 gengo-language/languages.yaml create mode 100644 gengo-language/src/lib.rs create mode 100644 gengo-language/tests/test.rs create mode 100644 gengo/Cargo.toml create mode 120000 gengo/LICENSE-APACHE create mode 120000 gengo/LICENSE-MIT create mode 120000 gengo/README.md create mode 100644 gengo/benches/run_on_self.rs create mode 120000 gengo/crates-io.md create mode 100644 gengo/src/analysis/mod.rs create mode 100644 gengo/src/analysis/summary.rs create mode 100644 gengo/src/binary.rs create mode 100644 gengo/src/builder.rs create mode 100644 gengo/src/documentation.rs create mode 100644 gengo/src/error.rs create mode 100644 gengo/src/file_source/directory.rs create mode 100644 gengo/src/file_source/git.rs create mode 100644 gengo/src/file_source/mod.rs create mode 100644 gengo/src/generated.rs create mode 100644 gengo/src/language/mod.rs create mode 100644 gengo/src/lib.rs create mode 100644 gengo/src/vendored.rs create mode 100644 gengo/tests/gengo_test.rs create mode 100644 gengo/tests/snapshots/gengo_test__git_javascript.snap create mode 100644 install.ps1 create mode 100755 install.sh create mode 120000 languages.yaml create mode 100644 rust-toolchain.toml create mode 100644 samples-test/Cargo.toml create mode 100644 samples-test/samples/Batch File/calculator.bat create mode 100644 samples-test/samples/Batch File/sysinfo.cmd create mode 100644 samples-test/samples/C++/progress-bar.cpp create mode 100644 samples-test/samples/C++/progress-bar.h create mode 100644 samples-test/samples/C/progress-bar.c create mode 100644 samples-test/samples/C/progress-bar.h create mode 100644 samples-test/samples/CSS/style.css create mode 100644 samples-test/samples/Common Lisp/cp.lisp create mode 100644 samples-test/samples/Gleam/math.gleam create mode 100644 samples-test/samples/Go/language_stats.go create mode 100644 samples-test/samples/HCL/spec.hcl create mode 100644 samples-test/samples/HTML/site.html create mode 100644 samples-test/samples/INI/.editorconfig create mode 100644 samples-test/samples/Ignore List/.dockerignore create mode 100644 samples-test/samples/Ignore List/.gitignore create mode 100644 samples-test/samples/Ignore List/.npmignore create mode 100644 samples-test/samples/Ignore List/.vercelignore create mode 100644 samples-test/samples/JSON with Comments/devcontainer.json create mode 100644 samples-test/samples/JSON with Comments/tsconfig.json create mode 100644 samples-test/samples/JSON/data.json create mode 100644 samples-test/samples/JavaScript/button.jsx create mode 100644 samples-test/samples/JavaScript/count_expression.js create mode 100644 samples-test/samples/Jule/generic.jule create mode 100644 samples-test/samples/Jupyter Notebook/simple.ipynb create mode 100644 samples-test/samples/Makefile/Makefile create mode 100644 samples-test/samples/Markdown/sample.md create mode 100644 samples-test/samples/MoonBit/expr.mbt create mode 100644 samples-test/samples/MoonBit/pkg.mbti create mode 100644 samples-test/samples/MoonBit/script.mbtx create mode 100644 samples-test/samples/OpenSCAD/sFeeder.scad create mode 100644 samples-test/samples/Org/org.org create mode 100644 samples-test/samples/PowerShell/check_for_updates.ps1 create mode 100644 samples-test/samples/Python/minecraft.py create mode 100644 samples-test/samples/Ruby/repofetch.rb create mode 100644 samples-test/samples/Rust/error.rs create mode 100644 samples-test/samples/SQL/job_info.sql create mode 100644 samples-test/samples/SVG/shapes.svg create mode 100644 samples-test/samples/Scheme/example.scm create mode 100644 samples-test/samples/Shell/rename.bash create mode 100644 samples-test/samples/Shell/rename.sh create mode 100644 samples-test/samples/Shell/rename.zsh create mode 100644 samples-test/samples/TOML/config.toml create mode 100644 samples-test/samples/TypeScript/App.tsx create mode 100644 samples-test/samples/TypeScript/calculate.ts create mode 100644 samples-test/samples/TypeSpec/main.tsp create mode 100644 samples-test/samples/XML/types.xml create mode 100644 samples-test/samples/YAML/dependabot.yml create mode 100644 samples-test/samples/fish/mkcd.fish create mode 100644 samples-test/samples/reStructured Text/example.rst create mode 100644 samples-test/src/lib.rs create mode 100644 scoop/gengo.json create mode 100755 scripts/check-languages-file.rb create mode 100755 scripts/conflicts.rb create mode 100755 scripts/preview-nerd-font-glyphs.rb create mode 100755 scripts/should-tag-new-language.rb create mode 100755 scripts/update-scoop.rb diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000000000000000000000000000000000000..bb8f13ab9de79a8cb9b39573d90d2db778093dc6 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,154 @@ +{ + "projectName": "gengo", + "projectOwner": "spenserblack", + "files": [ + "CREDITS.md" + ], + "commitConvention": "atom", + "contributorsSortAlphabetically": true, + "contributors": [ + { + "login": "o2sh", + "name": "Ossama Hjaji", + "avatar_url": "https://avatars.githubusercontent.com/u/13710835?v=4", + "profile": "https://github.com/o2sh", + "contributions": [ + "code", + "maintenance", + "data" + ] + }, + { + "login": "jake-87", + "name": "j", + "avatar_url": "https://avatars.githubusercontent.com/u/68929154?v=4", + "profile": "https://github.com/jake-87", + "contributions": [ + "data" + ] + }, + { + "login": "pykenny", + "name": "Po-Yu Hsieh", + "avatar_url": "https://avatars.githubusercontent.com/u/6328813?v=4", + "profile": "https://pyhsieh.net/", + "contributions": [ + "data" + ] + }, + { + "login": "Byron", + "name": "Sebastian Thiel", + "avatar_url": "https://avatars.githubusercontent.com/u/63622?v=4", + "profile": "https://keybase.io/byronbates", + "contributions": [ + "ideas", + "code", + "userTesting" + ] + }, + { + "login": "spsandwichman", + "name": "sandwich", + "avatar_url": "https://avatars.githubusercontent.com/u/73349893?v=4", + "profile": "http://sandwichman.dev", + "contributions": [ + "data" + ] + }, + { + "login": "Sh4rk-Byte", + "name": "Rostislav Otevrel", + "avatar_url": "https://avatars.githubusercontent.com/u/99561766?v=4", + "profile": "https://github.com/Sh4rk-Byte", + "contributions": [ + "data" + ] + }, + { + "login": "vinayakhegde1", + "name": "vinhegde", + "avatar_url": "https://avatars.githubusercontent.com/u/73926849?v=4", + "profile": "https://linktr.ee/vinhegde", + "contributions": [ + "data" + ] + }, + { + "login": "alexpalade", + "name": "Alex Palade", + "avatar_url": "https://avatars.githubusercontent.com/u/1126946?v=4", + "profile": "https://github.com/alexpalade", + "contributions": [ + "data" + ] + }, + { + "login": "hasecilu", + "name": "Ulices", + "avatar_url": "https://avatars.githubusercontent.com/u/53124818?v=4", + "profile": "https://github.com/hasecilu", + "contributions": [ + "data" + ] + }, + { + "login": "EliasLeguizamon123", + "name": "Elías Leguizamón ", + "avatar_url": "https://avatars.githubusercontent.com/u/25516473?v=4", + "profile": "https://www.linkedin.com/in/eliasleguizamon/", + "contributions": [ + "data" + ] + }, + { + "login": "musicinmybrain", + "name": "Ben Beasley", + "avatar_url": "https://avatars.githubusercontent.com/u/6898909?v=4", + "profile": "https://github.com/musicinmybrain", + "contributions": [ + "platform" + ] + }, + { + "login": "adamperkowski", + "name": "Adam Perkowski", + "avatar_url": "https://avatars.githubusercontent.com/u/75480869?v=4", + "profile": "https://adamperkowski.dev", + "contributions": [ + "platform", + "data" + ] + }, + { + "login": "mertcandav", + "name": "Mertcan D.", + "avatar_url": "https://avatars.githubusercontent.com/u/54983926?v=4", + "profile": "https://github.com/mertcandav", + "contributions": [ + "data" + ] + }, + { + "login": "ankddev", + "name": "ANKDDEV", + "avatar_url": "https://avatars.githubusercontent.com/u/190183925?v=4", + "profile": "https://github.com/ankddev", + "contributions": [ + "platform", + "data" + ] + }, + { + "login": "mst-mkt", + "name": "keito", + "avatar_url": "https://avatars.githubusercontent.com/u/131662659?v=4", + "profile": "http://links.keito.dev", + "contributions": [ + "data" + ] + } + ], + "contributorsPerLine": 7, + "commitType": "docs" +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f84eeb59afe248fdd5e44cd7f4399395b0bf77c6 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,3 @@ +ARG VARIANT="1" +FROM mcr.microsoft.com/devcontainers/rust:${VARIANT} +RUN apt update && apt install -y cmake diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000000000000000000000000000000..ba06bc6088209282ac58182a35ac4a82028f4a5e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "Rust", + "build": { + "dockerfile": "Dockerfile", + "args": { "VARIANT": "1" } + }, + "features": { + "ghcr.io/devcontainers/features/ruby:1": {} + }, + "customizations": { + "codespaces": { + "openFiles": ["docs/CONTRIBUTING.md", "docs/HACKING.md", "gengo-language/languages.yaml"] + }, + "vscode": { + "extensions": [ + "EditorConfig.EditorConfig", + "rust-lang.rust-analyzer", + "tomoyukim.vscode-mermaid-editor" + ], + "settings": { + "editor.formatOnSave": true, + "files.autoSaveDelay": 1000 + } + } + }, + "postCreateCommand": ".devcontainer/postCreate.sh", + "remoteUser": "vscode" +} diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100755 index 0000000000000000000000000000000000000000..91aabac9b9c7162edb7e2239093254339854bf7d --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,5 @@ +#!/bin/sh +git remote add gengo https://github.com/spenserblack/gengo +git fetch gengo test/javascript:test/javascript + +cargo install --locked cargo-insta taplo-cli diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..29831391f2b3d80a7eacbc5f8dd0b7025cd079ce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.devcontainer/ +.git/ +.github/ +.vscode/ +docs/ +scripts/ +target/ + +*.md +.all-contributorsrc +.editorconfig +.git-blame-ignore-revs +.gitattributes +CODEOWNERS diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..64b2b330ceb4347d90d4b34c64dd6fba7ad92268 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,43 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[.all-contributorsrc] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[*.{mmd,mermaid}] +indent_style = space +indent_size = 4 + +[*.rb] +indent_style = space +indent_size = 2 + +[*.rs] +indent_style = space +indent_size = 4 + +[*.sh] +indent_style = tab +indent_size = 8 + +[*.toml] +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 + +[Dockerfile] +indent_style = space +indent_size = 4 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000000000000000000000000000000000..156d93b1d7aa2b9350e1d6640855ee7fa2a30472 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,24 @@ +# Fix trailing spaces +c1f0b082a62c51d9fbcdd21a20ad764f8fc627ed + +# Taplo +d5f154ffedae1a6b331f45a274058b2da0176155 + +# Clippy fix +9ff3e4ef2f45551137f7731f75d4374887c91cad + +# README.md format +f4c9fc1dceb03abc7b944757a8ec73fa361f30e8 + +# Switch heuristics to single quotes +b67e485e1182063787a48a495afdf0e24af9e544 + +# Format scoop manifest +c2b4032cb45697b1957c5620f381ff3e38fce2d1 + +# Clippy and format for Rust 1.86 +2d4910cbb56db25c2c353310db54c93a16beb02b + +# Format gengo/Cargo.toml and gengo-bin/Cargo.toml +4a8c2041ff1e27490c81cddf7adcbed685310270 +c35f79fd8d42a2e22d75ca0d021361ae8b8fa080 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..6ae5437c21aba09bece1d82024ecbc4de00c0155 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +*.snap -whitespace linguist-language=Text gengo-language=PlainText +/gengo-language/languages.yaml gengo-detectable linguist-detectable + +# Fix GitHub's detected language +.vscode/*.json linguist-language=JSON-with-Comments + +# Ignore fixtures +**/fixtures/* -gengo-detectable + +# Handle sample code +/samples-test/samples/** -text linguist-vendored -gengo-detectable diff --git a/.github/ISSUE_TEMPLATE/new-language.yml b/.github/ISSUE_TEMPLATE/new-language.yml new file mode 100644 index 0000000000000000000000000000000000000000..eb0a7abfc8936e1cb27b942ab197164e26299334 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-language.yml @@ -0,0 +1,87 @@ +name: New Language Request +description: Request support for a new language +title: "Language: " +labels: + - new-language +body: + - type: input + id: language-name + attributes: + label: Name + description: The name of the language + validations: + required: true + - type: checkboxes + id: popularity + attributes: + label: Popularity + description: "Basically, the creators aren't the only users of the language" + options: + - label: This language is reasonably popular + required: true + - type: dropdown + id: language-category + attributes: + label: Which category best describes this language? + options: + - data + - markup + - programming + - prose + - query + - type: textarea + id: language-color + attributes: + label: Recommended Color + description: Let us know if you have an idea for what the color should be, and why + placeholder: "`#C0FFEE` spells 'coffee' so it's perfect for CoffeeScript" + - type: textarea + id: interpreters + attributes: + label: "Interpreters / shebangs" + description: | + List any interpreters used in shebangs (`#!/path/to/interpreter`) + placeholder: | + e.g. + + - python + - python3 + - type: textarea + id: filenames + attributes: + label: Filenames + description: List any related filenames + placeholder: | + e.g. + + - Dockerfile + - type: textarea + id: extensions + attributes: + label: Extensions + description: List any related file extensions + placeholder: | + e.g. + + - js + - cjs + - mjs + - type: textarea + id: patterns + attributes: + label: File Patterns + description: "If there are any file patterns that don't fit the above, list them here" + placeholder: | + e.g. + + - .vscode/*.json + - type: textarea + id: conflicts + attributes: + label: Conflicts + description: "To the best of your knowledge, do any of your answers conflict with another language?" + placeholder: | + e.g. + + The `rs` extension that Rust uses conflicts with RenderScript + diff --git a/.github/PULL_REQUEST_TEMPLATE/new_language.md b/.github/PULL_REQUEST_TEMPLATE/new_language.md new file mode 100644 index 0000000000000000000000000000000000000000..11224fe4f0bae5bf5a3ffaa59e095920c8c637ae --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_language.md @@ -0,0 +1,4 @@ +This pull request adds the language... + +- [ ] If this language conflicts with another language (same extension), I've added heuristics +- [ ] If this language conflicts with another language, I've linked to the pull request that added the other language here: \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/other.md b/.github/PULL_REQUEST_TEMPLATE/other.md new file mode 100644 index 0000000000000000000000000000000000000000..892ea015e1e0c25bcc655429e0c360abc61dc4c6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/other.md @@ -0,0 +1 @@ +## Description diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..f0e75dcf4d27d5e9445ff3765f807c044e9ed4e8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + groups: + proc-macro: + patterns: + - "prettyplease" + - "proc-macro2" + - "quote" + - "syn" + serde: + patterns: + - serde + - "serde_*" + - "serde-*" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..a2e14b32f64c4bb0896169b07eddf0dd7f2485e7 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,30 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: + - dependabot + - dependabot[bot] + - allcontributors + - allcontributors[bot] + categories: + - title: ":jack_o_lantern: Hacktoberfest Contributions" + labels: + - hacktoberfest + - hacktoberfest-accepted + - title: ":boom: Breaking changes" + labels: + - semver-major + - title: New Languages + labels: + - new-language + - title: New Features + labels: + - semver-minor + - title: Fixes + labels: + - semver-patch + - bug + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..e15b38d0b3cbf917d7b501870ee7405693acf2a9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,169 @@ +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build ${{ matrix.feature-flags }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: + - check-languages-file + strategy: + fail-fast: true + matrix: + os: + - macOS-latest + - ubuntu-latest + - windows-latest + feature-flags: + - "" # Default features + - --no-default-features + - --no-default-features --features color + + steps: + - uses: actions/checkout@v6 + - name: Fetch Test Branches + run: git fetch origin test/javascript:test/javascript + - name: Cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --verbose ${{ matrix.feature-flags }} + + - name: Test + run: cargo test --verbose ${{ matrix.feature-flags }} + env: + COLORTERM: truecolor + + docker: + name: Build Docker Image + runs-on: ubuntu-latest + needs: + - check-languages-file + steps: + - uses: actions/checkout@v6 + - name: Build Docker Image + run: docker build . + + coverage: + name: Coverage ${{ matrix.feature-flags }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: + - build + - lint + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + feature-flags: + - "" # Default features + - --no-default-features + - --no-default-features --features color + env: + TARPAULIN_VERSION: "0.30.0" + + steps: + - uses: actions/checkout@v6 + - name: Fetch Test Branches + run: git fetch origin test/javascript:test/javascript + - name: Main Cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Bin Cache + id: bin-cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + key: ${{ runner.os }}-cargo-bin-tarpaulin-${{ env.TARPAULIN_VERSION }} + - name: Install Tarpualin + if: steps.bin-cache.outputs.cache-hit != 'true' + run: cargo install "cargo-tarpaulin@$TARPAULIN_VERSION" + shell: bash + - name: Generate Coverage + run: cargo tarpaulin --verbose ${{ matrix.feature-flags }} --workspace --timeout 120 --out xml + - name: Upload Coverage + uses: codecov/codecov-action@v6 + with: + flags: ${{ matrix.os }},${{ matrix.feature-flags }} + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + + lint: + name: Lint + runs-on: ubuntu-latest + needs: + - build + steps: + - uses: actions/checkout@v6 + - name: Cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Check Format + run: cargo fmt -- --check + + - name: Lint + run: cargo clippy -- -D warnings + + check-languages-file: + name: Check Languages File + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + - name: Check Languages File + run: ruby ./scripts/check-languages-file.rb ./gengo-language/languages.yaml + + benchmarks: + name: Benchmarks + runs-on: ubuntu-latest + needs: + - build + - lint + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 3 + - name: Cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Fetch Test Branches + run: git fetch origin test/javascript:test/javascript + - name: Run Benchmarks + run: cargo bench diff --git a/.github/workflows/label-new-language-prs.yml b/.github/workflows/label-new-language-prs.yml new file mode 100644 index 0000000000000000000000000000000000000000..5f3a170f155ed1d5285bb22a5807ad370dfd90c4 --- /dev/null +++ b/.github/workflows/label-new-language-prs.yml @@ -0,0 +1,60 @@ +name: Auto-Label New Language PRs +on: + pull_request: + branches: [main] + + +jobs: + compare: + name: Compare Languages + runs-on: ubuntu-latest + outputs: + should-label: ${{ steps.compare.outputs.should-label }} + + steps: + - name: Fetch current languages.yaml + uses: actions/checkout@v6 + with: + path: old + ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: gengo-language/languages.yaml + sparse-checkout-cone-mode: false + clean: false + - name: Fetch updated languages.yaml + uses: actions/checkout@v6 + with: + path: new + ref: ${{ github.event.pull_request.head.sha }} + sparse-checkout: gengo-language/languages.yaml + sparse-checkout-cone-mode: false + clean: false + - name: Checkout scripts + uses: actions/checkout@v6 + with: + path: scripts + sparse-checkout: scripts/ + sparse-checkout-cone-mode: false + clean: false + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + - name: Compare languages + run: (ruby ./scripts/scripts/should-tag-new-language.rb old/gengo-language/languages.yaml new/gengo-language/languages.yaml && echo "should-label=true" || echo "should-label=false") >> "$GITHUB_OUTPUT" + id: compare + shell: bash + + label: + name: Label PR + runs-on: ubuntu-latest + permissions: + pull-requests: write + needs: + - compare + if: ${{ needs.compare.outputs.should-label == 'true' }} + + steps: + - run: gh pr edit $PR_URL --add-label "$PR_LABEL" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_LABEL: new-language diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..de2017501f68a595ca795a6b68eb9a2463a8acb1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,121 @@ +name: Release + +on: + push: + tags: ["v*.*.*"] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + publish: + name: Publish + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Publish Languages + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p gengo-language + - name: Publish Library + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p gengo + - name: Publish Binary + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p gengo-bin + + github: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: x86_64-apple-darwin + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: windows-latest + target: x86_64-pc-windows-msvc + + steps: + - uses: actions/checkout@v6 + - name: Add target + run: rustup target add ${{ matrix.target }} + - name: Build Assets + run: | + cargo build --release --target ${{ matrix.target }} + tar -C target/${{ matrix.target }}/release/ -czvf gengo-${{ matrix.target }}.tar.gz gengo${{ runner.os == 'Windows' && '.exe' || '' }} + - name: Upload Release Assets + if: ${{ github.event_name != 'workflow_dispatch' }} + uses: softprops/action-gh-release@v2 + with: + files: '*.tar.gz' + - name: Upload Artifacts + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: actions/upload-artifact@v7 + with: + path: '*.tar.gz' + + docker: + name: Publish Docker Image + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v6 + - name: Login to ghcr + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Build and Publish + uses: docker/build-push-action@v7 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + scoop: + name: Update scoop manifest + if: ${{ github.event_name != 'workflow_dispatch' }} + needs: github + runs-on: ubuntu-latest + permissions: + contents: write + env: + TAG: ${{ github.ref_name }} + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Update manifest for ${{ env.TAG }} + run: './scripts/update-scoop.rb ./scoop/gengo.json "${TAG}"' + - name: Commit and push changes + run: | + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --global user.name 'github-actions[bot]' + git add ./scoop/gengo.json + git commit -m "Update scoop manifest for ${TAG}" + git push origin HEAD:main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d1912c8caf8ba652a070ce47f11fd98b92b70411 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Generated by tarpaulin +cobertura.xml + +# Temporary snapshot files generated by insta +*.snap.new + +# Assets +*.tar.gz diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f639032bbafb52c65440386778012d6f79553aca --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,5 @@ +include = ["**/Cargo.toml", ".taplo.toml"] + +[formatting] +indent_string = " " +reorder_arrays = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000000000000000000000000000000..d927a15012f92b8450bd5570ca6e1f34f5c72609 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "rust-lang.rust-analyzer", + "shopify.ruby-lsp", + "tomoyukim.vscode-mermaid-editor" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..90a13979b691fffe3afb7c2b1e0cda5ce287f9c4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "rust-analyzer.cargo.features": "all", + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "cSpell.words": [ + "gengo", + "indexmap", + "insta", + "prettyplease", + "rstest", + "serde", + "unparse" + ], +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000000000000000000000000000000000..db61a1286392e49478d99b2e8f0fa80b537ee7de --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +* @spenserblack +/gengo-language/languages.yaml @o2sh diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000000000000000000000000000000000000..e19d928bfd0a3897b185936f243292d98895e1df --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,37 @@ +# Credits + +A special thanks to all listed below ([emoji key](https://allcontributors.org/docs/emoji-key))! + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ANKDDEV
ANKDDEV

📦 🔣
Adam Perkowski
Adam Perkowski

📦 🔣
Alex Palade
Alex Palade

🔣
Ben Beasley
Ben Beasley

📦
Elías Leguizamón
Elías Leguizamón

🔣
Mertcan D.
Mertcan D.

🔣
Ossama Hjaji
Ossama Hjaji

💻 🚧 🔣
Po-Yu Hsieh
Po-Yu Hsieh

🔣
Rostislav Otevrel
Rostislav Otevrel

🔣
Sebastian Thiel
Sebastian Thiel

🤔 💻 📓
Ulices
Ulices

🔣
j
j

🔣
keito
keito

🔣
sandwich
sandwich

🔣
vinhegde
vinhegde

🔣
+ + + + + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..318a2a953b29fac8aecebf20430730e53b5feaa9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2548 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chromaterm" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d453f74367095b1143d735d921d1f524ba6a73c57910a33564ac1ad7ac6e11" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "clru" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gengo" +version = "0.14.3" +dependencies = [ + "criterion", + "gengo-language", + "gix", + "glob", + "ignore", + "indexmap", + "insta", + "rayon", + "regex", + "rstest", + "serde", + "serde_yaml", +] + +[[package]] +name = "gengo-bin" +version = "0.14.3" +dependencies = [ + "chromaterm", + "clap", + "gengo", + "indexmap", + "insta", + "relative-luminance", + "serde_json", +] + +[[package]] +name = "gengo-language" +version = "0.14.3" +dependencies = [ + "glob", + "indexmap", + "proc-macro2", + "quote", + "regex", + "rstest", + "serde", + "serde_yaml", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "gix" +version = "0.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0473c64d9ccbcfb9953a133b47c8b9a335b87ac6c52b983ee4b03d49000b0f3f" +dependencies = [ + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-merge", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "nonempty", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-actor" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e5e5b518339d5e6718af108fd064d4e9ba33caf728cf487352873d76411df35" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "winnow", +] + +[[package]] +name = "gix-archive" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "651c99be11aac9b303483193ae50b45eb6e094da4f5ed797019b03948f51aad6" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-object", + "gix-worktree-stream", +] + +[[package]] +name = "gix-attributes" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c233d6eaa098c0ca5ce03236fd7a96e27f1abe72fad74b46003fbd11fe49563c" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7add20f40d060db8c9b1314d499bac6ed7480f33eb113ce3e1cf5d6ff85d989" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77aaf9f7348f4da3ebfbfbbc35fa0d07155d98377856198dde6f695fd648705" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-chunk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1096b6608fbe5d27fb4984e20f992b4e76fb8c613f6acb87d07c5831b53a6959" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849c65a609f50d02f8a2774fe371650b3384a743c79c2a070ce0da49b7fb7da" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3196655fd1443f3c58a48c114aa480be3e4e87b393d7292daaa0d543862eb445" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08939b4c4ed7a663d0e64be9e1e9bdf23a1fb4fcee1febdf449f12229542e50d" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "smallvec", + "thiserror", + "unicode-bom", + "winnow", +] + +[[package]] +name = "gix-config-value" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441a300bc3645a1f45cba495b9175f90f47256ce43f2ee161da0031e3ac77c92" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror", +] + +[[package]] +name = "gix-date" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39acf819aa9fee65e4838a2eec5cb2506e47ebb89e02a5ab9918196e491571ea" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", + "smallvec", +] + +[[package]] +name = "gix-diff" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f3b3475e5d3877d7c30c40827cc2441936ce890efc226e5ba4afe3a7ae33f0" +dependencies = [ + "bstr", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "imara-diff 0.1.8", + "imara-diff 0.2.0", + "thiserror", +] + +[[package]] +name = "gix-dir" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da4604a360988f0ba8efe6f90093ca5a844f4a7f8e1a3dcda501ec44e600ea9" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror", +] + +[[package]] +name = "gix-discover" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c65bd3330fe0cb9d40d875bf862fd5e8ad6fa4164ddbc4842fbeb889c3f0b2c6" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror", +] + +[[package]] +name = "gix-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e86d01da904d4a9265def43bd42a18c5e6dc7000a73af512946ba14579c9fbd" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.46.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752493cd4b1d5eaaa0138a7493f65c96863fefa990fc021e0e519579e389ab20" +dependencies = [ + "bytes", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "thiserror", + "walkdir", + "zlib-rs", +] + +[[package]] +name = "gix-filter" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37598282a6566da6fb52667570c7fe0aedcb122ac886724a9e62a2180523e35" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-fs" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a964b4aec683eb0bacb87533defa80805bb4768056371a47ab38b00a2d377b72" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-glob" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e6cd88cc0dc1eafa1fddac0fb719e4e74b6ea58dd016e71125fde4a326bee" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fb896a02d9ab96fa518475a5f30ad3952010f801a8de5840f633f4a6b985dfb" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "thiserror", +] + +[[package]] +name = "gix-hashtable" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2664216fc5e89b51e756a4a3ac676315602ce2dac07acf1da959a22038d69b33" +dependencies = [ + "gix-hash", + "hashbrown 0.16.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f915dcf6911e3027537166d34e13f0fe101ed12225178d2ae29cd1272cff26" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bae54ab14e4e74d5dda60b82ea7afad7c8eb3be68283d6d5f29bd2e6d47fff7" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.16.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d406220ef9df105645a9ddcaa42e8c882ba920344ace866d0403570aea599" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-merge" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4606747466512d22c2dffc019142e1941238f543987ea51353c938cca80c500" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "imara-diff 0.1.8", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-negotiate" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea064c7595eea08fdd01c70748af747d9acc40f727b61f4c8a2145a5c5fc28c" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cafb802bb688a7c1e69ef965612ff5ff859f046bfb616377e4a0ba4c01e43d47" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-path", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-odb" +version = "0.78.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24833ae9323b4f7079575fb9f961cf9c414b0afbec428a536ab8e7dd93bc002b" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror", +] + +[[package]] +name = "gix-pack" +version = "0.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3484119cd19859d7d7639413c27e192478fa354d3f4ff5f7e3c041e8040f0f4" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "memmap2", + "smallvec", + "thiserror", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be19313dcdb7dff75a3ce2f99be00878458295bcc3b6c7f0005591597573345c" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + +[[package]] +name = "gix-path" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c31d4373bda7fab9eb01822927b55185a378d6e1bf737e0a54c743ad806658" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror", +] + +[[package]] +name = "gix-pathspec" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89611f13544ca5ebeb68a502673814ef57200df60c24a61c2ce7b96f612f08b" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + +[[package]] +name = "gix-protocol" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38666350736b5877c79f57ddae02bde07a4ce186d889adc391e831cddcbe76" +dependencies = [ + "bstr", + "gix-date", + "gix-features", + "gix-hash", + "gix-ref", + "gix-shallow", + "gix-transport", + "gix-utils", + "maybe-async", + "nonempty", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-quote" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68533db71259c8776dd4e770d2b7b98696213ecdc1f5c9e3507119e274e0c578" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2159978abb99b7027c8579d15211e262ef0ef2594d5cecb3334fbcbdfe2997c" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-refspec" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc806ee13f437428f8a1ba4c72ecfaa3f20e14f5f0d4c2bc17d0b33e794aa6ac" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c08f1ec5d1e6a524f8ba291c41f0ccaef64e48ed0e8cf790b3461cae45f6d3d" +dependencies = [ + "bitflags", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4b2b87772b21ca449249e86d32febadba5cba32b0fcce804ab9cefc6f2111c" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-sec" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf82ae037de9c62850ce67beaa92ec8e3e17785ea307cdde7618edc215603b4f" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf60711c9083b2364b3fac8a352444af76b17201f3682fdebe74fa66d89a772" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-status" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d6c598e3fdbc352fba1c5ba7e709e69402fafbc44d9295edad2e3c4738996b" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror", +] + +[[package]] +name = "gix-submodule" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5c3929c5e6821f651d35e8420f72fea3cfafe9fc1e928a61e718b462c72a5" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-tempfile" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d280bba7c547170e42d5228fc6e76c191fb5a7c88808ff61af06460404d1fd91" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" + +[[package]] +name = "gix-transport" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a521e39c6235ce63ed6c001e2dd79818c830b82c3b7b59247ee7b229c39ec9bb" +dependencies = [ + "bstr", + "gix-command", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-traverse" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963dc2afcdb611092aa587c3f9365e749ac0a0892ff27662dbc75f26c953fbec" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-url" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28e8af3d42581190da884f013caf254d2fd4d6ab102408f08d21bfa11de6c8d" +dependencies = [ + "bstr", + "gix-path", + "percent-encoding", + "thiserror", +] + +[[package]] +name = "gix-utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" +dependencies = [ + "bstr", + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-worktree" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bd5830cbc43c9c00918b826467d2afad685b195cb82329cde2b2d116d2c578" +dependencies = [ + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + +[[package]] +name = "gix-worktree-state" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644a1681f96e1be43c2a8384337d9d220e7624f50db54beda70997052aebf707" +dependencies = [ + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror", +] + +[[package]] +name = "gix-worktree-stream" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e3fb70a1f650a5cec7d5b8d10d6d6fe86daf3cf15bde08ba0c70988a2932c3" +dependencies = [ + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "imara-diff" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "imara-diff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" +dependencies = [ + "hashbrown 0.15.5", + "memchr", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "rayon", + "serde", + "serde_core", +] + +[[package]] +name = "insta" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99322078b2c076829a1db959d49da554fabc4342257fc0ba5a070a1eb3a01cd8" +dependencies = [ + "console", + "once_cell", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "kstring" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prodash" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-luminance" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80010a7606585ed19af01ebd8ffed344b1bf9cdcfef6eb10100ab07421324f05" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "samples-test" +version = "0.14.3" +dependencies = [ + "gengo", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uluru" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794a32261a1f5eb6a4462c81b59cec87b5c27d5deea7dd1ac8fc781c41d226db" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..6a8bf329256ee6fd6149d8fe70b5430e2762c81f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +members = ["gengo", "gengo-bin", "gengo-language", "samples-test"] +resolver = "2" + +[workspace.dependencies] +glob = "0.3" +indexmap = "2" +regex = "1" +rstest = { version = "0.26", default-features = false } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" + +[workspace.package] +description = "Get the language distribution stats of your repository" +version = "0.14.3" +edition = "2024" +repository = "https://github.com/spenserblack/gengo" +readme = "README.md" +license = "MIT OR Apache-2.0" +rust-version = "1.86" + +keywords = ["language-statistics"] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..72abae4fce87f93850d191b3557855283706273b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM rust:1-alpine3.20 AS builder +WORKDIR /app +COPY . . +RUN apk --update add cmake make musl-dev pkgconfig && \ + cargo build --release + +FROM alpine:3 AS runtime +COPY --from=builder /app/target/release/gengo /usr/local/bin/gengo + +ENTRYPOINT ["/usr/local/bin/gengo"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000000000000000000000000000000000000..b1151e9e6c960914d7e75bad417f435328019f50 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Spenser Black + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d9a1f507f1d8dd8d6c1bff89327bf1ca412468f8 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# gengo (言語) + +[![library](https://img.shields.io/crates/v/gengo.svg?label=gengo)](https://crates.io/crates/gengo) +[![binary](https://img.shields.io/crates/v/gengo-bin.svg?label=gengo-bin)](https://crates.io/crates/gengo-bin) +[![CI](https://github.com/spenserblack/gengo/actions/workflows/ci.yml/badge.svg)](https://github.com/spenserblack/gengo/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/spenserblack/gengo/branch/main/graph/badge.svg?token=ihIEUQWwSt)](https://codecov.io/gh/spenserblack/gengo) + +A [linguist][linguist]-inspired language classifier with multiple file source handlers. + +Its output is similar to [linguist][linguist], but by default the language stats are colorized. + +## Comparison + +| Feature/Behavior | [linguist][linguist] | gengo | +| :---------------------------------: | :------------------: | :--------: | +| **Analyze Git Revision** | Yes | Yes | +| **Analyze Directory** | No | Yes | +| **Requires Git Repository** | Yes | No | +| **Detect Language by Extension** | Yes | Yes | +| **Detect Language by Filename** | Yes | Yes | +| **Detect by Filepath Pattern** | No | Yes | +| **Detect Language with Heuristics** | Yes | Yes | +| **Detect Language with Classifier** | Yes | Not Yet ;) | + +## Installation + +[![Packaging status](https://repology.org/badge/vertical-allrepos/rust%3Agengo.svg)](https://repology.org/project/rust%3Agengo/versions) + +View [the installation documentation][install-docs]. + +## Usage + +This tool has multiple file sources. Each file source can have unique usage to take advantage of its +strengths and work around its weaknesses. + +### Directory File Source + +This is a very generic file source that tries not to make many assumptions about your environment +and workspace. + +#### Ignoring Files + +You can utilize a `.gitignore` file and/or an `.ignore` file to prevent files from +being scanned. See the [`ignore`][ignore-crate] for more details. + +### Git File Source + +The git file source is highly opinionated -- it tries to act like a git utility, and uses git tools. +Its goal is to behave similarly to [linguist]. This means that this file source does *not* need any +actual files present, and can work on a bare repository, making it suitable for usage with a Git +server. + +#### Overrides + +Like [linguist][linguist], you can override behavior using a `.gitattributes` file. +Basically, just replace `linguist-FOO` with `gengo-FOO`. _Unlike_ linguist, +`gengo-detectable` will _always_ make a file be included in statistics (linguist +will still exclude them if they're generated or vendored). + +```gitattributes +# .gitattributes + +# boolean attributes: + +# These can be *negated* by prefixing with `-` (`-gengo-documentation`). +# Mark a file as documentation +*.html gengo-documentation +# Mark a file as generated +my-built-files/* gengo-generated +# Mark a file as vendored +deps/* gengo-vendored + +# string attributes: +# Override the detected language for a file +# Use the Language enum's variant name (see docs.rs for more details) +templates/*.js gengo-language=PlainText +``` + +You will need to commit your `.gitattributes` file for it to take effect. + +[ignore-crate]: https://docs.rs/ignore +[install-docs]: ./docs/INSTALLATION.md +[linguist]: https://github.com/github-linguist/linguist diff --git a/crates-io.md b/crates-io.md new file mode 100644 index 0000000000000000000000000000000000000000..6e88d6d7a77bdcc5b2f7436615c26f31d2c27aec --- /dev/null +++ b/crates-io.md @@ -0,0 +1,62 @@ +# Gengo + +## Installing the binary + +```shell +cargo install gengo-bin +``` + +## Usage + +[API documentation][docs-rs] + +This tool has multiple file sources. Each file source can have unique usage to take advantage of its +strengths and work around its weaknesses. + +### Directory File Source + + +This is a very generic file source that tries not to make many assumptions about your environment +and workspace. + +#### Ignoring Files + +You can utilize a `.gitignore` file and/or an `.ignore` file to prevent files from +being scanned. See the [`ignore`][ignore-crate] for more details. + +### Git File Source + +The git file source is highly opinionated -- it tries to act like a git utility, and uses git tools. +Its goal is to behave similarly to [linguist]. + +#### Overrides + +Like [linguist][linguist], you can override behavior using a `.gitattributes` file. +Basically, just replace `linguist-FOO` with `gengo-FOO`. *Unlike* linguist, +`gengo-detectable` will *always* make a file be included in statistics (linguist +will still exclude them if they're generated or vendored). + +```gitattributes +# .gitattributes + +# boolean attributes: + +# These can be *negated* by prefixing with `-` (`-gengo-documentation`). +# Mark a file as documentation +*.html gengo-documentation +# Mark a file as generated +my-built-files/* gengo-generated +# Mark a file as vendored +deps/* gengo-vendored + +# string attributes: +# Override the detected language for a file +# Use the Language enum's variant name (see docs.rs for more details) +templates/*.js gengo-language=PlainText +``` + +You will need to commit your `.gitattributes` file for it to take effect. + +[docs-rs]: https://docs.rs/gengo +[ignore-crate]: https://docs.rs/ignore +[linguist]: https://github.com/github-linguist/linguist diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..c5f2f4a65249f7a21e4f18298422ddeb4cbf9cd5 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing + +Thank you for taking the time to contribute! + +## Development + +Please refer to [`HACKING`][hacking] for contributions that add/change language +support, update behavior, etc. + +## Suggesting a new language + +If you know about a language that this project should support, you can create a +[new issue][new-language-issue] to suggest adding support. You will be prompted +to provide information about the language that this project needs to add support. + +## Making a pull request + +Release notes are generated from pull request titles. So, when making a pull request, +consider what would make sense in a release note. The title shouldn't be too technical, +but should provide a clear summary that a user can understand. + +### Example + +#### OK + +>Explicitly use RGB(0, 0, 0) and RGB(255, 255, 255) instead of built-in white and black + +#### Better + +>Increase contrast of colored terminal output + +## For maintainers + +### Showing appreciation + +This project uses [all-contributors][all-contributors] to track contributions to help +show appreciation for all types of contributions. When a GitHub user contributes, don't +forget to ask the bot to track their contribution! For example, when adding support for +a new language, you can comment `@all-contributors add @USERNAME for data`. + +[all-contributors]: https://allcontributors.org/ +[hacking]: ./HACKING.md +[new-language-issue]: https://github.com/spenserblack/gengo/issues/new/choose diff --git a/docs/HACKING.md b/docs/HACKING.md new file mode 100644 index 0000000000000000000000000000000000000000..011b75f72c442dce7214d9a1d371f75b3984cf45 --- /dev/null +++ b/docs/HACKING.md @@ -0,0 +1,114 @@ +# Hacking + +## Getting Started + +### Tools + +#### EditorConfig + +This project uses [EditorConfig][editorconfig]. If necessary, install the plugin +for your preferred editor(s). + +#### Testing + +This project uses [insta][insta] for snapshot testing. Review the instructions +for using insta and installing the `cargo insta` executable. + +## Development + +### Language Support + +Language definitions are defined in [`./gengo-language/languages.yaml`][languages-file]. +This is where you register new languages, add extensions, filenames, filepath +patterns, or interpreters to help identify languages, etc. Pretty much anything +involving language detection or the returned language data comes from this file +(but there are a few exceptions). + +#### The `languages.yaml` file + +A language looks roughly like this: + +```yaml +My Language: + category: programming + color: "#AABBCC" + heuristics: + - "(?m)^my super unique syntax" + matchers: + extensions: + - ext + filenames: + - Mylang + interpreters: + - mylangexe + patterns: + - "**/special-path/*.ext" + priority: 100 +``` + +- `category` - Required. It should be `data`, `markup`, `programming`, `prose`, or `query`. +- `color` - Required. It should be a hexadecimal color. Since `#` is a comment + in YAML, make sure to add quotes! +- `heuristics` - Optional. Should be a list of regexes matching syntax unique to the + language. +- `matchers` - Required. Each key in this object is optional, but *at least one* must + be defined. + - `extensions` - A list of file extensions that the language is associated with. E.g. `rs` for Rust. + - `filenames` - A list of filenames that the language is associated with. E.g. `Dockerfile` for Docker. + - `interpreters` - Executables associated with the language. Used for matching shebangs. E.g. `python3` for Python. + - `patterns` - Glob patterns for the full filepath. This is useful for edge cases, like the containing directory or a sub-extension changing the language. +- `priority` - Optional, defaulting to `50`. When all else fails, and we fail narrow down to a single language, matching languages are sorted by priority to pick one. + +Note that regexes in this file use the syntax from the [`regex` crate][regex-syntax]. + +All entries in this file should be alphabetized. There is a +[script to check this file][check-languages-script] that is run +by the CI. But surely you'll run it locally so that the CI passes the first +time, right? ;) + +#### File Attributes + +This tool also tries to detect if a file is documentation, generated, or vendored. +These can be a bit too complex to be managed with a data file, so you'll need to +write some Rust if you want to update the behavior for these. + +Check `documentation.rs`, `generated.rs`, and `vendored.rs` in [`gengo/src`][lib-src] +to update detection for these. For performance reasons, checks that *don't* require +reading contents should always be done before checks that *do* read contents. + +### Adding a sample + +This project can test itself on code samples for accuracy. Not only that, but the samples can be used +in the future to train additional classification methods. If you would like to add a sample, add a +file to `samples-test/samples/LANGUAGE/`, where `LANGUAGE` is the name of the language as seen in +[`languages.yaml`][languages-file]. + +When adding a sample: +- It should preferably be a real-world sample, not a "hello world" example. +- State where the sample comes from. Did you write it? Did you copy it from an open-source repository? + - If you wrote it yourself, you agree that the sample is licensed under this repository's licenses. + - If you copied it from somewhere else, make sure that the code you copied is licensed under the + MIT or Apache 2.0 license. + +## Testing + +Because this project analyzes git revisions, some of the tests are run on +commits in this project that are unreachable from `main`. Make sure to +fetch branches that are prefixed with `test/` from this repository. + +## Updating dependencies + +For dependencies that use *ranges* for their version requirement, please don't jump up several +breaking versions in a single commit. Instead, increment the breaking version by one for each +commit. This makes it easier to bisect any issues with the dependency and ensure that each +breaking version in the range has been tested. For example, if you're bumping a version requirement +from `>= 1, < 2` to `>= 1, < 5`, then you should make *3 commits* (bump to `< 3`, bump to `< 4`, and +bump to `< 5`). + + +[check-languages-script]: ./scripts/check-languages-file.rb +[editorconfig]: https://editorconfig.org/ +[insta]: https://crates.io/crates/insta +[languages-file]: ./gengo-language/languages.yaml +[lib-src]: ./gengo/src +[regex-syntax]: https://docs.rs/regex/latest/regex/#syntax diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000000000000000000000000000000000000..1566c7401f232384a7abab8b305915946972bc28 --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,72 @@ +# Installation + +## Unix + +### Installation script + +This will download a binary from the latest GitHub release. +**This will activate `sudo` to write the executable.** Please review the install script +before running it. + +```shell +curl https://raw.githubusercontent.com/spenserblack/gengo/main/install.sh | sh +``` + +### Arch Linux + +gengo is available as [an AUR package](https://aur.archlinux.org/packages/gengo/). +It can be installed using an AUR helper (e.g. paru): + +```shell +paru -S gengo +``` + +## Windows + +### Installation script + +This will download a binary from the latest GitHub release. + +```powershell +Invoke-RestMethod "https://raw.githubusercontent.com/spenserblack/gengo/main/install.ps1" | Invoke-Expression +``` + +If you get an error regarding execution policy, please read the error carefully and +determine the execution policy that is right for you. You may try re-running the +installation script if you have updated the execution policy. + +### Scoop + +gengo is installable via [Scoop](https://scoop.sh). See the [Scoop manifest][scoop-manifest]. + +```batch +scoop install https://raw.githubusercontent.com/spenserblack/gengo/main/scoop/gengo.json +``` + +## Universal + +### From GitHub Releases + +Download the appropriate file from [a release](https://github.com/spenserblack/gengo/releases). + +### With `cargo` + +The following installs the `gengo` binary. + +```shell +cargo install gengo-bin +``` + +### Docker + +You can build a docker image and run it as well. + +```bash +docker build -t gengo . +docker run --rm -v $(pwd):$(pwd) -w $(pwd) -t gengo +``` + +Or pull it from the [GitHub Container Registry][ghcr-package]. + +[ghcr-package]: https://github.com/users/spenserblack/packages/container/package/gengo +[scoop-manifest]: ../scoop/gengo.json diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000000000000000000000000000000000000..86c146215d0894f7d4bd7828fe3267ac6c1f2e6a --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,18 @@ +# Releasing + +Releasing is a simple process -- just create a new GitHub release. Use the +auto-generate release notes feature, labelling PRs if necessary. The workflows should +take care of releasing to crates.io and making release assets. + +## Versioning + +### Major (breaking) changes to languages + +These may not be obvious at first, as these are changes made to the data, not the Rust +code. The following changes to `languages.yaml` are considered breaking: + +* Renaming a language +* Removing a language +* Removing a matcher (extension, interpreter, glob)[^1] + +[^1]: An exception to this is if the matcher is so rarely used that it shouldn't have been added in the first place. diff --git a/docs/analysis-flowchart.mmd b/docs/analysis-flowchart.mmd new file mode 100644 index 0000000000000000000000000000000000000000..b542f6c2bbd384120fec2cceef822f7e1abcc85f --- /dev/null +++ b/docs/analysis-flowchart.mmd @@ -0,0 +1,31 @@ +--- +title: Analysis Flowchart +--- +flowchart TD + start[Start] + pass1[Check shebangs] + pass1part2[Check filenames] + pass1part3[Check filepath patterns] + pass1part4[Check extensions] + result1[Return languages] + pass2[Heuristics] + result2branch1[Return languages from heuristics] + result2branch2[Return languages from first check] + pass3[Pick by priority] + stop[Stop] + start --> pass1 + pass1 -->|Shebang matches| result1 + pass1 -->|No matching shebangs| pass1part2 + pass1part2 -->|filename matches| result1 + pass1part2 -->|No matching filenames| pass1part3 + pass1part3 -->|filepath matches| result1 + pass1part3 -->|No matching filepaths| pass1part4 + pass1part4 -->result1 + result1 -->|0 or 1 matching languages| stop + result1 -->|2 or more matching languages| pass2 + pass2 -->|1 matching language| stop + pass2 -->|2 or more matching languages| result2branch1 + pass2 -->|No matching languages| result2branch2 + result2branch1 --> pass3 + result2branch2 --> pass3 + pass3 --> stop diff --git a/docs/snippets.md b/docs/snippets.md new file mode 100644 index 0000000000000000000000000000000000000000..f523bc1987f420bd37e3193d773472e01128160c --- /dev/null +++ b/docs/snippets.md @@ -0,0 +1,12 @@ +# Snippets + +These are snippets that might be fun for a user to try out. + +## Build a history of language distribution + +```shell +for rev in $(git log --reverse --format="%H"); do + gengo git --rev "$rev" + echo # add a newline +done +``` diff --git a/gengo-bin/Cargo.toml b/gengo-bin/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..9ccdb26996fd5fe3c63f68da71be9934e1d7ceff --- /dev/null +++ b/gengo-bin/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "gengo-bin" +description.workspace = true +readme = "crates-io.md" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +rust-version.workspace = true + +categories = ["command-line-utilities"] +keywords.workspace = true + +exclude = ["README.md"] + +[[bin]] +name = "gengo" + +[features] +default = ["color", "gengo/max-performance"] +color = ["dep:chromaterm", "dep:relative-luminance"] + +[dependencies] +chromaterm = { version = "0.1.1", optional = true } +clap = { version = "4", features = ["derive", "wrap_help"] } +gengo = { path = "../gengo", version = "0.14", default-features = false, features = [ + "directory", + "git", +] } +indexmap.workspace = true +relative-luminance = { version = "1", optional = true } +serde_json.workspace = true + +[dev-dependencies] +insta = { version = "1", features = ["json"] } +serde_json.workspace = true diff --git a/gengo-bin/LICENSE-APACHE b/gengo-bin/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..965b606f331b51d566b46025f9ff311a7aad0c12 --- /dev/null +++ b/gengo-bin/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/gengo-bin/LICENSE-MIT b/gengo-bin/LICENSE-MIT new file mode 120000 index 0000000000000000000000000000000000000000..76219eb72e8524f15c21ec93b9b2592da49b5460 --- /dev/null +++ b/gengo-bin/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/gengo-bin/README.md b/gengo-bin/README.md new file mode 120000 index 0000000000000000000000000000000000000000..32d46ee883b58d6a383eed06eb98f33aa6530ded --- /dev/null +++ b/gengo-bin/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/gengo-bin/crates-io.md b/gengo-bin/crates-io.md new file mode 120000 index 0000000000000000000000000000000000000000..ae66dc9c49cf24ae5d8353b3c97cfc5e9783fb96 --- /dev/null +++ b/gengo-bin/crates-io.md @@ -0,0 +1 @@ +../crates-io.md \ No newline at end of file diff --git a/gengo-bin/src/bin/gengo.rs b/gengo-bin/src/bin/gengo.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9f062ca5376a73255645a1068d65e876d2ebdd0 --- /dev/null +++ b/gengo-bin/src/bin/gengo.rs @@ -0,0 +1,8 @@ +use std::process::exit; +fn main() { + let cli = gengo_bin::cli::new(); + if let Err(err) = cli.run(std::io::stdout(), std::io::stderr()) { + eprintln!("{}", err); + exit(1); + } +} diff --git a/gengo-bin/src/cli.rs b/gengo-bin/src/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..407ab9e3ceebef9c5c1796b93a4fa1f25675c7d2 --- /dev/null +++ b/gengo-bin/src/cli.rs @@ -0,0 +1,303 @@ +#[cfg(feature = "color")] +use chromaterm::{colors, prelude::*}; +use clap::Error as ClapError; +use clap::{Parser, Subcommand, ValueEnum}; +use gengo::{Analysis, Builder, Directory, Git, analysis::SummaryOpts}; +use indexmap::IndexMap; +#[cfg(feature = "color")] +use relative_luminance::Luminance; +use std::error::Error as BaseError; +use std::io::{self, Write}; + +pub fn new() -> CLI { + CLI::parse() +} + +pub fn try_new_from(args: &[&str]) -> Result { + CLI::try_parse_from(args) +} + +/// Fetch language statistics for your source code. +#[derive(Parser)] +#[command(version)] +#[command(propagate_version = true)] +pub struct CLI { + #[command(subcommand)] + command: Commands, + /// The maximum number of bytes to read from each file. + /// + /// This is useful for large files that can impact performance. + /// + /// The format is in bytes. The default is 1 MiB. + #[arg(short = 'l', long, default_value = "1048576", global = true)] + read_limit: usize, + /// Report on all files, even if they are not detectable. + /// + /// This only applies to the pretty format, as machine-readable + /// formats always include all files. + #[arg(short = 'a', long, global = true)] + all: bool, + /// Include detailed statistics for each language. + /// + /// This only applies to the pretty format, as machine-readable + /// formats always include detailed statistics. + #[arg(short = 'b', long, global = true)] + breakdown: bool, + /// Control when colors are displayed. + #[cfg(feature = "color")] + #[arg(long, default_value = "auto", global = true)] + color: ColorControl, + /// The format to use for output. + #[arg(short = 'F', long, default_value = "pretty", global = true)] + format: Format, +} + +#[derive(Subcommand)] +enum Commands { + /// Analyze a git repository. + Git { + /// The path to the repository to analyze. + #[arg(short = 'R', long, default_value = ".")] + repository: String, + /// The git revision to analyze. + #[arg(short = 'r', long = "rev", default_value = "HEAD")] + revision: String, + }, + /// ***BETA*** Analyze a directory. + Directory { + /// The path to the directory to analyze. + #[arg(short = 'D', long, default_value = ".")] + directory: String, + }, +} + +#[cfg(feature = "color")] +#[derive(ValueEnum, Debug, Clone)] +enum ColorControl { + /// Automatically detect if colors are supported. + Auto, + /// Always use colors. + Always, + /// Use only the 8 ANSI colors. + Ansi, + /// Disable colors. + Never, +} + +#[derive(ValueEnum, Debug, Clone)] +enum Format { + /// Output for humans. + Pretty, + /// JSON output. + Json, +} + +impl CLI { + pub fn run(&self, mut out: impl Write, mut err: impl Write) -> Result<(), io::Error> { + #[cfg(feature = "color")] + { + use ColorControl::*; + use chromaterm::ColorSupport; + + chromaterm::config::convert_to_supported(true); + match self.color { + Auto => chromaterm::config::use_default_color_support(), + Always => chromaterm::config::use_color_support(ColorSupport::True), + Ansi => chromaterm::config::use_color_support(ColorSupport::Simple), + Never => chromaterm::config::use_color_support(ColorSupport::None), + } + } + let results = self.command.analyze(self.read_limit); + let results = match results { + Ok(results) => results, + Err(e) => { + writeln!(err, "failed to analyze repository: {}", e)?; + return Ok(()); + } + }; + + match self.format { + Format::Pretty => (), + Format::Json => return self.run_json(results, out, err), + } + + let mut summary_opts: SummaryOpts = Default::default(); + summary_opts.all = self.all; + let summary = results.summary_with(summary_opts); + let total = summary.total(); + let total = total as f64; + + let summary = { + let mut summary: Vec<(_, _)> = summary.iter().collect(); + summary.sort_by_key(|(language, size)| (usize::MAX - *size, language.name())); + summary + }; + + for (language, size) in summary.iter() { + let percentage = (*size * 100) as f64 / total; + #[cfg(feature = "color")] + let color = { + let (r, g, b) = language.rgb(); + chromaterm::colors::True::from_rgb(r, g, b) + }; + #[cfg(not(feature = "color"))] + let color = (); + + let stats = format!("{:>6.2}% {}", percentage, size); + let line = format!("{:<15} {}", stats, language.name()); + let line = self.colorize(&line, &color); + writeln!(out, "{}", line)?; + } + + if self.breakdown { + writeln!(out)?; + self.run_breakdown(out, err, results)?; + } + + Ok(()) + } + + fn run_json( + &self, + analysis: Analysis, + mut out: impl Write, + mut _err: impl Write, + ) -> Result<(), io::Error> { + match serde_json::to_string(&analysis) { + Ok(s) => writeln!(out, "{s}")?, + Err(e) => { + writeln!(out, "failed to serialize to JSON: {e}")?; + return Ok(()); + } + }; + Ok(()) + } + + fn run_breakdown( + &self, + mut out: impl Write, + mut _err: impl Write, + results: Analysis, + ) -> Result<(), io::Error> { + let files_per_language = { + let mut files_per_language = IndexMap::new(); + for (path, entry) in results.iter() { + if !(self.all || entry.detectable()) { + continue; + } + + let language = entry.language(); + let language_files = files_per_language.entry(language).or_insert_with(Vec::new); + language_files.push(path); + } + files_per_language + }; + + let files_per_language = { + let mut v: Vec<(_, _)> = files_per_language.into_iter().collect(); + v.sort_by_key(|(language, _)| language.name()); + v + }; + + for (language, files) in files_per_language.into_iter() { + #[cfg(feature = "color")] + let color = { + let (r, g, b) = language.rgb(); + chromaterm::colors::True::from_rgb(r, g, b) + }; + #[cfg(not(feature = "color"))] + let color = (); + + writeln!(out, "{}", self.colorize(language.name(), &color))?; + + let files = { + let mut files = files; + files.sort(); + files + }; + + for file in files { + writeln!( + out, + " {}", + self.colorize(&file.display().to_string(), &color) + )?; + } + writeln!(out)?; + } + Ok(()) + } + + #[cfg(feature = "color")] + fn colorize(&self, s: &str, color: &colors::True) -> String { + use chromaterm::{Color, colors::True}; + + let fg = if Self::is_bright(color) { + True::from_rgb(0, 0, 0) + } else { + True::from_rgb(0xFF, 0xFF, 0xFF) + }; + let (r, g, b) = color.rgb_u8(); + s.on_rgb(r, g, b).color(fg).to_string() + } + + #[cfg(feature = "color")] + fn is_bright<'a, T: Into>>(color: T) -> bool { + color.into().relative_luminance() > 0.5 + } + + #[cfg(not(feature = "color"))] + fn colorize(&self, s: &str, _: Anything) -> String { + String::from(s) + } +} + +impl Commands { + fn analyze(&self, read_limit: usize) -> Result> { + match self { + Commands::Git { + repository, + revision, + } => { + let git = Git::new(repository, revision)?; + let gengo = Builder::new(git).read_limit(read_limit).build()?; + gengo.analyze() + } + Commands::Directory { directory } => { + let directory = Directory::new(directory, read_limit)?; + let gengo = Builder::new(directory).read_limit(read_limit).build()?; + gengo.analyze() + } + } + } +} + +#[cfg(feature = "color")] +mod color_support { + use chromaterm::Color; + + use super::*; + + pub(super) struct RgbWrapper<'a>(&'a colors::True); + + impl Luminance for RgbWrapper<'_> { + fn luminance_rgb(&self) -> relative_luminance::Rgb { + let (r, g, b) = self.0.rgb_u8(); + // NOTE Normalize to the range [0.0, 1.0] + relative_luminance::Rgb::new( + f32::from(r) / 255.0, + f32::from(g) / 255.0, + f32::from(b) / 255.0, + ) + } + } + + impl<'a> From<&'a colors::True> for RgbWrapper<'a> { + fn from(rgb: &'a colors::True) -> Self { + Self(rgb) + } + } +} + +#[cfg(feature = "color")] +use color_support::*; diff --git a/gengo-bin/src/lib.rs b/gengo-bin/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f773726a20b821d11320ef75d522be0bc862871 --- /dev/null +++ b/gengo-bin/src/lib.rs @@ -0,0 +1 @@ +pub mod cli; diff --git a/gengo-bin/tests/snapshots/test_cli__breakdown_javascript_repo.snap b/gengo-bin/tests/snapshots/test_cli__breakdown_javascript_repo.snap new file mode 100644 index 0000000000000000000000000000000000000000..52b0f5255170060b1ffa02f8307325df41e53630 --- /dev/null +++ b/gengo-bin/tests/snapshots/test_cli__breakdown_javascript_repo.snap @@ -0,0 +1,18 @@ +--- +source: gengo-bin/tests/test_cli.rs +expression: stdout +--- + 40.79% 62 Plain Text + 40.79% 62 TypeScript + 18.42% 28 JavaScript + +JavaScript + bin.js + +Plain Text + dist/bin.js + +TypeScript + src/bin.ts + + diff --git a/gengo-bin/tests/snapshots/test_cli__color_breakdown_javascript_repo.snap b/gengo-bin/tests/snapshots/test_cli__color_breakdown_javascript_repo.snap new file mode 100644 index 0000000000000000000000000000000000000000..432baa3662743e12762126cdc3c6ed41c9d732b6 --- /dev/null +++ b/gengo-bin/tests/snapshots/test_cli__color_breakdown_javascript_repo.snap @@ -0,0 +1,16 @@ +--- +source: gengo-bin/tests/test_cli.rs +expression: stdout +--- + 40.79% 62 Plain Text + 40.79% 62 TypeScript + 18.42% 28 JavaScript + +JavaScript + bin.js + +Plain Text + dist/bin.js + +TypeScript + src/bin.ts diff --git a/gengo-bin/tests/snapshots/test_cli__color_javascript_repo.snap b/gengo-bin/tests/snapshots/test_cli__color_javascript_repo.snap new file mode 100644 index 0000000000000000000000000000000000000000..9794ed273bc16850d09de5d4f865b61452ca2425 --- /dev/null +++ b/gengo-bin/tests/snapshots/test_cli__color_javascript_repo.snap @@ -0,0 +1,7 @@ +--- +source: gengo-bin/tests/test_cli.rs +expression: stdout +--- + 40.79% 62 Plain Text + 40.79% 62 TypeScript + 18.42% 28 JavaScript diff --git a/gengo-bin/tests/snapshots/test_cli__javascript_repo.snap b/gengo-bin/tests/snapshots/test_cli__javascript_repo.snap new file mode 100644 index 0000000000000000000000000000000000000000..be6f9997e090377bbfd890d42e91773bf35cbe15 --- /dev/null +++ b/gengo-bin/tests/snapshots/test_cli__javascript_repo.snap @@ -0,0 +1,8 @@ +--- +source: gengo-bin/tests/test_cli.rs +expression: stdout +--- + 40.79% 62 Plain Text + 40.79% 62 TypeScript + 18.42% 28 JavaScript + diff --git a/gengo-bin/tests/snapshots/test_cli__json_output_on_javascript_repo.snap b/gengo-bin/tests/snapshots/test_cli__json_output_on_javascript_repo.snap new file mode 100644 index 0000000000000000000000000000000000000000..91ffb96700a3a7b1a6d68e20923376a2569ab6e2 --- /dev/null +++ b/gengo-bin/tests/snapshots/test_cli__json_output_on_javascript_repo.snap @@ -0,0 +1,71 @@ +--- +source: gengo-bin/tests/test_cli.rs +expression: json +--- +{ + "bin.js": { + "detectable": true, + "documentation": false, + "generated": false, + "language": { + "category": "programming", + "hex": "#F0DC4E", + "name": "JavaScript", + "nerd_font_glyph": "" + }, + "size": 28, + "vendored": false + }, + "dist/bin.js": { + "detectable": true, + "documentation": false, + "generated": true, + "language": { + "category": "prose", + "hex": "#000000", + "name": "Plain Text", + "nerd_font_glyph": null + }, + "size": 62, + "vendored": false + }, + "docs/index.html": { + "detectable": false, + "documentation": true, + "generated": false, + "language": { + "category": "markup", + "hex": "#E96228", + "name": "HTML", + "nerd_font_glyph": "" + }, + "size": 26, + "vendored": false + }, + "node_modules/my-dependency/index.js": { + "detectable": false, + "documentation": false, + "generated": false, + "language": { + "category": "programming", + "hex": "#F0DC4E", + "name": "JavaScript", + "nerd_font_glyph": "" + }, + "size": 29, + "vendored": true + }, + "src/bin.ts": { + "detectable": true, + "documentation": false, + "generated": false, + "language": { + "category": "programming", + "hex": "#2F74C0", + "name": "TypeScript", + "nerd_font_glyph": "" + }, + "size": 62, + "vendored": false + } +} diff --git a/gengo-bin/tests/test_cli.rs b/gengo-bin/tests/test_cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..765d0e5f2c74b700084be4136b4a533eaf76b782 --- /dev/null +++ b/gengo-bin/tests/test_cli.rs @@ -0,0 +1,98 @@ +use insta::{assert_json_snapshot, assert_snapshot}; +use std::io::{self, Write}; + +const ROOT: &str = env!("CARGO_MANIFEST_DIR"); + +struct NullWriter; + +impl Write for NullWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +macro_rules! assert_stdout_snapshot { + ($cli_args:expr $(,)?) => {{ + let cli = gengo_bin::cli::try_new_from($cli_args).unwrap(); + let mut stdout = Vec::new(); + let mut stderr = NullWriter; + cli.run(&mut stdout, &mut stderr).unwrap(); + let stdout = String::from_utf8(stdout).unwrap(); + assert_snapshot!(stdout); + }}; +} + +// TODO This feels like a bit of a hack. +macro_rules! assert_stdout_json_snapshot { + ($cli_args:expr $(,)?) => {{ + let cli = gengo_bin::cli::try_new_from($cli_args).unwrap(); + let mut stdout = Vec::new(); + let mut stderr = NullWriter; + cli.run(&mut stdout, &mut stderr).unwrap(); + let stdout = String::from_utf8(stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_json_snapshot!(json); + }}; +} + +#[test] +#[cfg(not(feature = "color"))] +fn test_javascript_repo() { + assert_stdout_snapshot!(&["gengo", "git", "-r", "test/javascript", "-R", ROOT]); +} + +#[test] +#[cfg_attr(windows, ignore)] +#[cfg(not(feature = "color"))] +fn test_breakdown_javascript_repo() { + assert_stdout_snapshot!(&[ + "gengo", + "--breakdown", + "git", + "-r", + "test/javascript", + "-R", + ROOT, + ]); +} + +#[test] +#[cfg(feature = "color")] +fn test_color_javascript_repo() { + assert_stdout_snapshot!(&["gengo", "git", "-r", "test/javascript", "-R", ROOT]); +} + +#[test] +#[cfg_attr(windows, ignore)] +#[cfg(feature = "color")] +fn test_color_breakdown_javascript_repo() { + assert_stdout_snapshot!(&[ + "gengo", + "--breakdown", + "git", + "-r", + "test/javascript", + "-R", + ROOT, + ]); +} + +// TODO Add test_javascript_repo_windows + +#[test] +#[cfg_attr(windows, ignore)] +fn test_json_output_on_javascript_repo() { + assert_stdout_json_snapshot!(&[ + "gengo", + "--format", + "json", + "git", + "-r", + "test/javascript", + "-R", + ROOT, + ]); +} diff --git a/gengo-language/Cargo.toml b/gengo-language/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..83bf3ed00e4e3d9d91b2790ecd75d1cd21d69e6d --- /dev/null +++ b/gengo-language/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "gengo-language" +description.workspace = true +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +rust-version.workspace = true + +keywords.workspace = true + +[dependencies] +glob.workspace = true +regex.workspace = true +serde.workspace = true + +[build-dependencies] +indexmap = { version = "2", features = ["serde"] } +proc-macro2 = "1" +serde_yaml.workspace = true +quote = "1" + +[dev-dependencies] +rstest.workspace = true +serde_yaml.workspace = true diff --git a/gengo-language/LICENSE-APACHE b/gengo-language/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..965b606f331b51d566b46025f9ff311a7aad0c12 --- /dev/null +++ b/gengo-language/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/gengo-language/LICENSE-MIT b/gengo-language/LICENSE-MIT new file mode 120000 index 0000000000000000000000000000000000000000..76219eb72e8524f15c21ec93b9b2592da49b5460 --- /dev/null +++ b/gengo-language/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/gengo-language/README.md b/gengo-language/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3ffc2373d09070c74fe5db60c5643ffd7f61d48b --- /dev/null +++ b/gengo-language/README.md @@ -0,0 +1,3 @@ +# gengo-language + +This provides the language definitions for gengo. diff --git a/gengo-language/build.rs b/gengo-language/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba8c0d76262e69d1f26a8dfc3648088791af0dcb --- /dev/null +++ b/gengo-language/build.rs @@ -0,0 +1,630 @@ +use indexmap::IndexMap; +use proc_macro2::{Ident, Literal, Span}; +use quote::quote; +use std::collections::HashMap; +use std::env; +use std::error::Error; +use std::fs; +use std::path::Path; + +const LANGUAGES: &str = include_str!("./languages.yaml"); + +const MINIMUM_PRIORITY: u8 = 0; +const MAXIMUM_PRIORITY: u8 = 100; +const DEFAULT_PRIORITY: u8 = 50; + +/// Converts `languages.yaml` to minified JSON and writes it to +/// `languages.json`. +fn main() -> Result<(), Box> { + // TODO This looks messy, and can use cleanup. + let languages: IndexMap = serde_yaml::from_str(LANGUAGES)?; + let languages_target_dir = Path::new(&env::var("OUT_DIR")?).join("languages"); + fs::create_dir_all(&languages_target_dir)?; + + struct LanguageDefinition { + variant: Ident, + /// See `Category`. + category: Ident, + name: Literal, + color_hex: Literal, + color_rgb: (Literal, Literal, Literal), + nerd_font_glyph: Option, + priority: Literal, + extensions: Vec, + filenames: Vec, + interpreters: Vec, + patterns: Vec, + heuristics: Vec, + } + + let language_definitions: Vec<_> = languages + .iter() + .map(|(language_name, language_attrs)| { + let language_attrs = language_attrs + .as_mapping() + .expect("language attributes to be an object"); + let variant = rustify_language_name(language_name); + let variant = Ident::new(&variant, Span::call_site()); + + let category = language_attrs["category"] + .as_str() + .expect("category to be a string"); + let category = match category { + "data" => "Data", + "markup" => "Markup", + "pattern" => "Pattern", + "programming" => "Programming", + "prose" => "Prose", + "query" => "Query", + unknown => unreachable!("Category {unknown}"), + }; + let category = Ident::new(category, Span::call_site()); + + let name = Literal::string(language_name); + + let color = language_attrs["color"] + .as_str() + .expect("color to be a string"); + + let color_hex = Literal::string(color); + + let color_rgb = { + let color = color.strip_prefix('#').expect("'#' prefix"); + assert_eq!(color.len(), 6, "Expected 6 characters"); + let channels = u32::from_str_radix(color, 16).expect("valid hex string"); + let r = ((channels >> 16) & 0xFF) as u8; + let g = ((channels >> 8) & 0xFF) as u8; + let b = (channels & 0xFF) as u8; + ( + Literal::u8_suffixed(r), + Literal::u8_suffixed(g), + Literal::u8_suffixed(b), + ) + }; + + let nerd_font_glyph = language_attrs.get("nerd-font-glyph").map(|glyph| { + let glyph = glyph.as_str().expect("nerd font glyph to be a string"); + Literal::string(glyph) + }); + + let priority = language_attrs + .get("priority") + .map(|priority| { + let priority = priority.as_u64().expect("priority to be a number"); + assert!( + priority >= MINIMUM_PRIORITY.into() && priority <= MAXIMUM_PRIORITY.into(), + "priority to be between {MINIMUM_PRIORITY} and {MAXIMUM_PRIORITY}" + ); + priority as u8 + }) + .unwrap_or(DEFAULT_PRIORITY); + let priority = Literal::u8_unsuffixed(priority); + + let matchers = language_attrs["matchers"] + .as_mapping() + .expect("matchers to be an object"); + + let extensions = matchers + .get("extensions") + .map(|extensions| { + extensions + .as_sequence() + .expect("extensions to be an array") + .to_owned() + }) + .unwrap_or_default() + .iter() + .map(|extension| { + extension + .as_str() + .expect("extension to be a string") + .to_string() + }) + .collect(); + + let filenames = matchers + .get("filenames") + .map(|filenames| { + filenames + .as_sequence() + .expect("filenames to be an array") + .to_owned() + }) + .unwrap_or_default() + .iter() + .map(|filename| { + filename + .as_str() + .expect("filename to be a string") + .to_string() + }) + .collect(); + + let interpreters = matchers + .get("interpreters") + .map(|interpreters| { + interpreters + .as_sequence() + .expect("interpreters to be an array") + .to_owned() + }) + .unwrap_or_default() + .iter() + .map(|interpreter| { + interpreter + .as_str() + .expect("interpreter to be a string") + .to_string() + }) + .collect(); + + let patterns = matchers + .get("patterns") + .map(|patterns| { + patterns + .as_sequence() + .expect("patterns to be an array") + .to_owned() + }) + .unwrap_or_default() + .iter() + .map(|pattern| { + pattern + .as_str() + .expect("pattern to be a string") + .to_string() + }) + .collect(); + + let heuristics = language_attrs + .get("heuristics") + .map(|heuristics| { + heuristics + .as_sequence() + .expect("heuristics to be an array") + .to_owned() + .iter() + .map(|heuristic| { + heuristic + .as_str() + .expect("heuristic to be a string") + .to_string() + }) + .collect() + }) + .unwrap_or_default(); + + LanguageDefinition { + variant, + category, + name, + color_hex, + color_rgb, + nerd_font_glyph, + priority, + extensions, + filenames, + interpreters, + patterns, + heuristics, + } + }) + .collect(); + + let variants = language_definitions.iter().map(|def| &def.variant); + let language = quote! { + /// The type of language. Returned by language detection. + #[non_exhaustive] + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] + pub enum Language { + #(#variants,)* + } + }; + fs::write( + languages_target_dir.join("language.rs"), + language.to_string(), + )?; + + let category_mappings = language_definitions.iter().map( + |LanguageDefinition { + variant, category, .. + }| { + quote! { + Self::#variant => Category::#category + } + }, + ); + let category_mixin = quote! { + impl Language { + /// Gets the category of the language. + pub const fn category(&self) -> Category { + match self { + #(#category_mappings ,)* + } + } + } + }; + fs::write( + languages_target_dir.join("category_mixin.rs"), + category_mixin.to_string(), + )?; + + let name_mappings = + language_definitions + .iter() + .map(|LanguageDefinition { variant, name, .. }| { + quote! { + Self::#variant => #name + } + }); + let name_mixin = quote! { + impl Language { + /// Gets the name of the language. + pub const fn name(&self) -> &'static str { + match self { + #(#name_mappings ,)* + } + } + } + }; + fs::write( + languages_target_dir.join("name_mixin.rs"), + name_mixin.to_string(), + )?; + + let reverse_variant_mappings = + language_definitions + .iter() + .map(|LanguageDefinition { variant, .. }| { + let variant_name = variant.to_string(); + quote! { + #variant_name => Some(Self::#variant) + } + }); + let parse_variant_mixin = quote! { + impl Language { + /// Converts a variant's name back to the language. + fn parse_variant(name: &str) -> Option { + match name { + #(#reverse_variant_mappings ,)* + _ => None, + } + } + } + }; + fs::write( + languages_target_dir.join("parse_variant_mixin.rs"), + parse_variant_mixin.to_string(), + )?; + + let color_hex_mappings = language_definitions.iter().map( + |LanguageDefinition { + variant, color_hex, .. + }| { + quote! { + Self::#variant => #color_hex + } + }, + ); + let color_hex_mixin = quote! { + impl Language { + /// Gets the hex code associated with the language. + pub const fn hex(&self) -> &'static str { + match self { + #(#color_hex_mappings ,)* + } + } + } + }; + fs::write( + languages_target_dir.join("color_hex_mixin.rs"), + color_hex_mixin.to_string(), + )?; + + let color_rgb_mappings = language_definitions.iter().map( + |LanguageDefinition { + variant, + color_rgb: (r, g, b), + .. + }| { + quote! { + Self::#variant => (#r, #g, #b) + } + }, + ); + let color_rgb_mixin = quote! { + impl Language { + /// Gets the RGB color associated with the language. + pub const fn rgb(&self) -> (u8, u8, u8) { + match self { + #(#color_rgb_mappings ,)* + } + } + } + }; + fs::write( + languages_target_dir.join("color_rgb_mixin.rs"), + color_rgb_mixin.to_string(), + )?; + + let nerd_font_glyph_mappings = language_definitions.iter().filter_map( + |LanguageDefinition { + variant, + nerd_font_glyph, + .. + }| { + nerd_font_glyph.as_ref().map(|glyph| { + quote! { + Self::#variant => Some(#glyph) + } + }) + }, + ); + let nerd_font_glyph_mixin = quote! { + impl Language { + /// Gets the Nerd Font glyph associated with the language. + pub const fn nerd_font_glyph(&self) -> Option<&'static str> { + match self { + #(#nerd_font_glyph_mappings ,)* + _ => None, + } + } + } + }; + fs::write( + languages_target_dir.join("nerd_font_glyph_mixin.rs"), + nerd_font_glyph_mixin.to_string(), + )?; + + let priority_mappings = language_definitions.iter().map( + |LanguageDefinition { + variant, priority, .. + }| { + quote! { + Self::#variant => #priority + } + }, + ); + let priority_mixin = quote! { + impl Language { + /// Gets the priority of the language. This is useful for sorting languages + /// when multiple languages are detected. + pub const fn priority(&self) -> u8 { + match self { + #(#priority_mappings ,)* + } + } + } + }; + fs::write( + languages_target_dir.join("priority_mixin.rs"), + priority_mixin.to_string(), + )?; + + let extension_to_langs: HashMap<_, Vec<_>> = language_definitions.iter().fold( + HashMap::new(), + |map, + LanguageDefinition { + variant, + extensions, + .. + }| { + extensions.iter().fold(map, |mut map, extension| { + map.entry(extension.clone()) + .or_default() + .push(variant.clone()); + map + }) + }, + ); + let extension_to_langs_mappings = extension_to_langs.iter().map(|(extension, langs)| { + quote! { + #extension => vec![#(Self::#langs),*] + } + }); + let from_extension_mixin = quote! { + impl Language { + /// Gets languages by extension. + pub fn from_extension(extension: &str) -> Vec { + match extension { + #(#extension_to_langs_mappings ,)* + _ => vec![], + } + } + } + }; + fs::write( + languages_target_dir.join("from_extension_mixin.rs"), + from_extension_mixin.to_string(), + )?; + + let filenames_to_langs: HashMap<_, Vec<_>> = language_definitions.iter().fold( + HashMap::new(), + |map, + LanguageDefinition { + variant, filenames, .. + }| { + filenames.iter().fold(map, |mut map, filename| { + map.entry(filename.clone()) + .or_default() + .push(variant.clone()); + map + }) + }, + ); + let filenames_to_langs_mappings = filenames_to_langs.iter().map(|(filename, langs)| { + quote! { + #filename => vec![#(Self::#langs),*] + } + }); + let from_filename_mixin = quote! { + impl Language { + /// Gets languages by filename. + pub fn from_filename(filename: &str) -> Vec { + match filename { + #(#filenames_to_langs_mappings ,)* + _ => vec![], + } + } + } + }; + fs::write( + languages_target_dir.join("from_filename_mixin.rs"), + from_filename_mixin.to_string(), + )?; + + let interpreters_to_langs: HashMap<_, Vec<_>> = language_definitions.iter().fold( + HashMap::new(), + |map, + LanguageDefinition { + variant, + interpreters, + .. + }| { + interpreters.iter().fold(map, |mut map, interpreter| { + map.entry(interpreter.clone()) + .or_default() + .push(variant.clone()); + map + }) + }, + ); + let interpreter_to_langs_mappings = interpreters_to_langs.iter().map(|(interpreter, langs)| { + quote! { + #interpreter => vec![#(Self::#langs),*] + } + }); + let from_interpreter_mixin = quote! { + impl Language { + /// Gets languages by interpreter (typically found as part of a shebang). + pub fn from_interpreter(interpreter: &str) -> Vec { + match interpreter { + #(#interpreter_to_langs_mappings ,)* + _ => vec![], + } + } + } + }; + fs::write( + languages_target_dir.join("from_interpreter_mixin.rs"), + from_interpreter_mixin.to_string(), + )?; + + let glob_matchers = language_definitions + .iter() + .filter(|def| !def.patterns.is_empty()) + .map( + |LanguageDefinition { + variant, patterns, .. + }| { + quote! { + ( + vec![#(#patterns),*], + Language::#variant, + ) + } + }, + ); + let glob_mappings_mixin = quote! { + impl Language { + /// Gets the mappings used to map a glob to its language. + fn glob_mappings() -> Vec<(Vec<&'static str>, Self)> { + vec![#(#glob_matchers),*] + } + } + }; + fs::write( + languages_target_dir.join("glob_mappings_mixin.rs"), + glob_mappings_mixin.to_string(), + )?; + + let heuristic_tuples = language_definitions + .iter() + .filter(|language_definition| !language_definition.heuristics.is_empty()) + .map( + |LanguageDefinition { + variant, + heuristics, + .. + }| { + quote! { + (Self::#variant, vec![#(#heuristics),*]) + } + }, + ); + let heuristic_mappings_mixin = quote! { + impl Language { + /// Gets the heuristics used to determine a language. + fn heuristic_mappings() -> Vec<(Self, Vec<&'static str>)> { + vec![#(#heuristic_tuples ,)*] + } + } + }; + fs::write( + languages_target_dir.join("heuristic_mappings_mixin.rs"), + heuristic_mappings_mixin.to_string(), + )?; + + Ok(()) +} + +/// Converts a language name to a valid Rust identifier to be used as an enum +/// variant. +/// +/// # Examples +/// +/// - `".Env"` -> `"Dotenv"` +/// - `"C++"` -> `"CPlusPlus"` +/// - `"C#"` -> `"CSharp"` +/// - `"HTML"` -> `"Html"` +/// - `"JavaScript"` -> `"Javascript"` +/// - `"Batch File"` -> `"BatchFile"` +fn rustify_language_name(name: &str) -> String { + let name = asciiify_language_name(name); + + // HACK This will break if there are any leading, trailing, or consecutive + // spaces in the name. + let name = + name.split(' ') + .flat_map(|name| name.split('.')) + .fold(String::new(), |name, word| { + let mut chars = word.chars(); + // NOTE If there is a special character like ß it will become SS, but + // that should never happen. + let first_char = chars.next().unwrap().to_uppercase(); + assert!(first_char.len() == 1); + let rest = chars + .map(|c| c.to_lowercase().to_string()) + .collect::(); + format!("{name}{first_char}{rest}") + }); + name +} + +/// Replaces special characters in a language name with their ASCII +/// equivalents. +fn asciiify_language_name(name: &str) -> String { + // NOTE .Env is a special case because the special character is at the beginning + // and it should be one word. + if name == ".Env" { + return "Dotenv".to_string(); + } + // NOTE Maps special characters to their ASCII equivalents. + let mappings = [ + ("-", ""), + ("'", ""), + ("+", "Plus"), + ("#", "Sharp"), + ("*", "Star"), + ]; + + let name = mappings + .iter() + .fold(name.to_string(), |name, (pattern, replacement)| { + // NOTE Adding a leading space to the replacement to ensure that it + // is treated as a word boundary. + name.replace(pattern, &format!(" {replacement}")) + }); + + name +} diff --git a/gengo-language/languages.yaml b/gengo-language/languages.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7940369f3ee6ef352f59e4a5df7ed75576033522 --- /dev/null +++ b/gengo-language/languages.yaml @@ -0,0 +1,1152 @@ +# Definition of languages supported by Gengo +# +# # Format +# +# ```yaml +# # The name of the language. +# Language: +# +# # data, markup, pattern, programming, prose, or query +# category: +# +# # The color associated with the language. +# color: +# +# # The Nerd Font glyph associated with the language (optional). +# nerd-font-glyph: +# +# # Regexes unique to the contents of the file to resolve multiple matches. These will +# # be used if the matchers (see below) cannot narrow down a file to a single +# # language. +# heuristics: +# - "(?m)^macro_rules!" +# +# # Entries to match against files. This is the first pass before more expensive +# # strategies are used. +# matchers: +# +# # The file extensions associated with the language (optional) +# extensions: +# - "rs" +# +# # The filenames associated with the language (optional) +# filenames: +# - "Dockerfile" +# +# # Used for checking shebangs. +# interpreters: # The interpreter(s) associated with the language (optional) +# - "python" +# - "python3" +# +# # Glob patterns associated with the language (optional) +# patterns: +# # See the format of the `glob` crate: https://docs.rs/glob/latest/glob/struct.Pattern.html +# - "__snapshots__/*.snap" +# +# # Value between 0 and 100 to pick a language if multiple matches couldn't be +# # narrowed down (optional, default: 50) +# priority: 50 +# ``` +# +# WARNING: Please alphabetize! +--- +".Env": + category: data + color: "#ECD53F" + nerd-font-glyph: "\U0000f462" + matchers: + extensions: + - env + filenames: + - ".env" + patterns: + - ".env.*" # .env.local, .env.example +ABAP: + category: programming + color: "#3C3C3C" + matchers: + extensions: + - abap +ABNF: + category: pattern + color: "#ABFABF" + matchers: + extensions: + - abnf +Ada: + category: programming + color: "#005A00" + nerd-font-glyph: "\U0000e6b5" + matchers: + extensions: + - ada + - adb + - ads +Agda: + category: programming + color: "#FFFFFF" + matchers: + extensions: + - agda +Arduino: + category: programming + color: "#189BA1" + nerd-font-glyph: "\U0000f34b" + matchers: + extensions: + - ino +Assembly: + category: programming + color: "#33AA33" + nerd-font-glyph: "\U0000e266" + matchers: + extensions: + - asm + - s +Astro: + category: programming + color: "#FF5A1D" + matchers: + extensions: + - astro +ATS: + category: programming + color: "#0005FB" + matchers: + extensions: + - atxt + - dats + - hats + - sats +AutoHotkey: + category: programming + color: "#334455" + nerd-font-glyph: "\U000f0b08" + matchers: + extensions: + - ahk +Batch File: + category: programming + color: "#818B91" + matchers: + extensions: + - bat + - cmd +C: + category: programming + color: "#8888CC" + nerd-font-glyph: "\U0000e61e" + heuristics: + - '(?m)^#include\s+<(stdio\.h|stdlib\.h|string\.h|math\.h)>' + - '(?m)^int\s+main\s*\(\s*(void|int\s+argc\s*,\s*char\s*\*\s*argv\[\s*\])\s*SW\)' + matchers: + extensions: + - c + - h + priority: 75 +"C#": + category: programming + color: "#178600" + nerd-font-glyph: "\U000f031b" + heuristics: + - '^\s*(using\s+[A-Z][\s\w.]+;|namespace\s*[\w\.]+\s*(\{|;)|\/\/)' + matchers: + extensions: + - cake + - cs + - csx +"C++": + category: programming + color: "#88CC88" + nerd-font-glyph: "\U0000e61d" + heuristics: + - '(?m)^#include\s+<(iostream|vector|string|algorithm)>' + - '(?m)^(namespace\s+\w+\s*\{|using\s+namespace\s+\w+;)' + - '(?m)^(class|template|typename)\b' + - '(?m)\b(std::|nullptr|constexpr)\b' + matchers: + extensions: + - c++ + - cpp + - h + - h++ + - hh + - hpp +Ceylon: + category: programming + color: "#F7941E" + matchers: + extensions: + - ceylon +Clojure: + category: programming + color: "#77F212" + nerd-font-glyph: "\U0000e76a" + matchers: + extensions: + - clj + - cljc + - cljs +CMake: + category: programming + color: "#CCCCCC" + matchers: + extensions: + - cmake + filenames: + - CMakeLists.txt +CoffeeScript: + category: programming + color: "#C0FFEE" + nerd-font-glyph: "\U0000e751" + matchers: + extensions: + - coffee + interpreters: + - coffee +ColdFusion: + category: programming + color: "#001C57" + nerd-font-glyph: "\U0000e645" + matchers: + extensions: + - cfm +Common Lisp: + category: programming + color: "#2D5FB4" + matchers: + extensions: + - asd + - lisp + - lsp +Coq: + category: programming + color: "#D0B68C" + heuristics: + - '(?m)^[ \t]*(?:From\s+\S+\s+Require\b|Require\s+(?:Import|Export)\b|(?:Lemma|Theorem|Example|Fact|Remark|Definition|Inductive|CoInductive|Record|Class|Instance|Fixpoint|CoFixpoint|Proposition|Corollary|Axiom|Parameter|Variable|Hypothesis|Ltac)\s+[A-Za-z_]|Proof[ \t]*\.)' + matchers: + extensions: + - v +Crystal: + category: programming + color: "#000000" + nerd-font-glyph: "\U0000e62f" + matchers: + extensions: + - cr + interpreters: + - crystal +CSS: + category: markup + color: "#AA88AA" + nerd-font-glyph: "\U0000e749" + matchers: + extensions: + - css +CSV: + category: data + color: "#1D6F42" + nerd-font-glyph: "\U0000e64a" + matchers: + extensions: + - csv +D: + category: programming + color: "#B03931" + matchers: + extensions: + - d +Dafny: + category: programming + color: "#FFEC25" + matchers: + extensions: + - dfy +Dart: + category: programming + color: "#238BDA" + nerd-font-glyph: "\U0000e64c" + matchers: + extensions: + - dart +Docker: + category: programming + color: "#2496ED" + nerd-font-glyph: "\U0000e650" + matchers: + filenames: + - "Dockerfile" + patterns: + - "Dockerfile.*" # Dockerfile. +Elixir: + category: programming + color: "#6B5674" + nerd-font-glyph: "\U0000e62d" + matchers: + extensions: + - ex + - exs + interpreters: + - elixir +Elm: + category: programming + color: "#1293D8" + nerd-font-glyph: "\U0000e62c" + matchers: + extensions: + - elm +Emacs Lisp: + category: programming + color: "#7F5AB6" + nerd-font-glyph: "\U0000e632" + matchers: + extensions: + - el +Emojicode: + category: programming + color: "#FCEA2B" + nerd-font-glyph: "\U000f0785" + matchers: + extensions: + - emojic + - "🍇" +Erlang: + category: programming + color: "#A90433" + nerd-font-glyph: "\U0000e7b1" + matchers: + extensions: + - erl + - hrl +"F#": + category: programming + color: "#F8008F" + nerd-font-glyph: "\U0000e7a7" + matchers: + extensions: + - fs + - fsx +"F*": + category: programming + color: "#572E30" + matchers: + extensions: + - fst + - fsti +fish: # NOTE: fish is always lowercase in their documentation + category: programming + color: "#4AAE47" + nerd-font-glyph: "\U0000ee41" + matchers: + extensions: + - fish + interpreters: + - fish +Forth: + category: programming + color: "#EF5350" + matchers: + extensions: + - 4th + - f + - forth + - fs + - fth +"FORTRAN Legacy": + category: programming + color: "#716152" + nerd-font-glyph: "\U000f121a" + matchers: + extensions: + - f + - f77 + - for + - ftn + - pfo +"Fortran Modern": + category: programming + color: "#725196" + nerd-font-glyph: "\U000f121a" + matchers: + extensions: + - f03 + - f08 + - f90 + - f95 +GDScript: + category: programming + color: "#355570" + nerd-font-glyph: "\U0000e65f" + matchers: + extensions: + - gd +GitHub Workflow: + category: programming + color: "#2088FF" + nerd-font-glyph: "\U0000eaff" + matchers: + patterns: + - ".github/workflows/*.yaml" + - ".github/workflows/*.yml" + priority: 75 +Gleam: + category: programming + color: "#ffaff3" + matchers: + extensions: + - gleam +GLSL: + category: programming + color: "#5586A4" + matchers: + extensions: + - comp + - frag + - geom + - glsl + - mesh + - rahit + - rcall + - rchit + - rgen + - rint + - rmiss + - task + - tesc + - tese + - vert +Go: + category: programming + color: "#00ADD8" + nerd-font-glyph: "\U0000e627" + matchers: + extensions: + - go +# NOTE: These files are unique to Go and are typically referenced in the documentation +# by their filenames. +"go.mod": + category: data + color: "#00ADD8" + nerd-font-glyph: "\U0000e627" + matchers: + filenames: + - go.mod +"go.sum": + category: data + color: "#00ADD8" + nerd-font-glyph: "\U0000e627" + matchers: + filenames: + - go.sum +GraphQL: + category: query + color: "#E10098" + nerd-font-glyph: "\U000f0877" + matchers: + extensions: + - graphql +Groovy: + category: programming + color: "#4298B8" + nerd-font-glyph: "\U0000e775" + matchers: + extensions: + - groovy + interpreters: + - groovy +Haskell: + category: programming + color: "#5E5086" + nerd-font-glyph: "\U0000e777" + matchers: + extensions: + - hs +Haxe: + category: programming + color: "#F68712" + matchers: + extensions: + - hx +HCL: + category: programming + color: "#7B42BC" + matchers: + extensions: + - hcl + - tf + - tfvars +HLSL: + category: programming + color: "#85BB00" + matchers: + extensions: + - hlsl +HolyC: + category: programming + color: "#FFFF00" + nerd-font-glyph: "\U0000eebe" + matchers: + extensions: + - hc +HTML: + category: markup + color: "#E96228" + nerd-font-glyph: "\U0000e736" + matchers: + extensions: + - html +Idris: + category: programming + color: "#C74350" + matchers: + extensions: + - idr + - lidr +Ignore List: + category: data + color: "#330000" + matchers: + filenames: + - ".dockerignore" + - ".gitignore" + - ".npmignore" + - ".vercelignore" +INI: + category: data + color: "#DDEEFF" + nerd-font-glyph: "\U0000e615" + matchers: + extensions: + - "ini" + filenames: + - ".editorconfig" + - ".flake8" + - ".isort.cfg" +Isabelle: + category: programming + color: "#FEFE00" + matchers: + extensions: + - thy + filenames: + - ROOT +Java: + category: programming + color: "#5283A2" + nerd-font-glyph: "\U0000e738" + matchers: + extensions: + - java +JavaScript: + category: programming + color: "#F0DC4E" + nerd-font-glyph: "\U0000e74e" + matchers: + extensions: + - js + - jsx + interpreters: + - node +Jinja-like: + category: markup + color: "#A00000" + nerd-font-glyph: "\U0000e66f" + heuristics: + - '^\{%\sextends\s' + - '\{%\s.+?\s%\}' + matchers: + extensions: + - html + - tera + priority: 25 +JSON: + category: data + color: "#AAAAAA" + nerd-font-glyph: "\U0000eb0f" + matchers: + extensions: + - json + filenames: + - .all-contributorsrc + priority: 75 +JSON with Comments: + category: data + color: "#CCCCCC" + nerd-font-glyph: "\U0000eb0f" + heuristics: + - '(?m)^\s*/[/\*]' + matchers: + extensions: + - json + - jsonc + filenames: + - devcontainer.json + - tsconfig.json + patterns: + - .vscode/*.json + - tsconfig.*.json + priority: 25 +Jsonnet: + category: programming + color: "#0064BD" + matchers: + extensions: + - jsonnet + - libsonnet +Jule: + category: programming + color: "#5F7489" + nerd-font-glyph: "\U0000e80c" + matchers: + extensions: + - jule +Julia: + category: programming + color: "#9558B2" + nerd-font-glyph: "\U0000e624" + matchers: + extensions: + - jl +Jupyter Notebook: + category: markup + color: "#F37726" + matchers: + extensions: + - ipynb +Kotlin: + category: programming + color: "#7F52FF" + nerd-font-glyph: "\U0000e634" + matchers: + extensions: + - kt + - kts + interpreters: + - kotlin +Lean: + category: programming + color: "#3D84C2" + matchers: + extensions: + - lean +LLVM: + category: programming + color: "#09637D" + matchers: + extensions: + - ll +Lua: + category: programming + color: "#02027D" + nerd-font-glyph: "\U0000e620" + matchers: + extensions: + - lua + interpreters: + - lua +Makefile: + category: programming + color: "#6B482F" # Arbitrary brown color representing a Gnu + nerd-font-glyph: "\U0000e673" + matchers: + filenames: + - "Makefile" + patterns: + - "Makefile.*" # Makefile. +Markdown: + category: prose + color: "#03A7DD" + nerd-font-glyph: "\U0000e73e" + matchers: + extensions: + - markdown + - md +Mermaid: + category: markup + color: "#FF3670" + matchers: + extensions: + - mermaid + - mmd +MoonBit: + category: programming + color: "#b92381" + matchers: + extensions: + - mbt + - mbti + - mbtx +Nim: + category: programming + color: "#ffe953" + nerd-font-glyph: "\U0000e677" + matchers: + extensions: + - nim +Nix: + category: programming + color: "#6898D3" # Average of the two colors in the logo + nerd-font-glyph: "\U0000f313" + matchers: + extensions: + - nix +Objective-C: + category: programming + color: "#0BC0BC" + matchers: + extensions: + - h + - m +OCaml: + category: programming + color: "#f48904" + nerd-font-glyph: "\U0000e67a" + matchers: + extensions: + - ml + - mli +Odin: + category: programming + color: "#3882D2" + matchers: + extensions: + - odin +OpenSCAD: + category: programming + color: "#F9D72C" + nerd-font-glyph: "\U0000f34e" + matchers: + extensions: + - scad +Org: + category: prose + color: "#77AA99" + matchers: + extensions: + - org +Oz: + category: programming + color: "#FCAF3E" + matchers: + extensions: + - oz +Pascal: + category: programming + color: "#79A0DE" + matchers: + extensions: + - pas +Perl: + category: programming + color: "#51547F" + nerd-font-glyph: "\U0000e67e" + matchers: + extensions: + - cow # cowsay files are Perl + - pl + interpreters: + - perl + priority: 75 +PHP: + category: programming + color: "#7A86B8" + nerd-font-glyph: "\U0000e608" + matchers: + extensions: + - php +Plain Text: + category: prose + color: "#000000" + matchers: + extensions: + - text + - txt + filenames: + - "LICENCE" + - "LICENSE" +PowerShell: + category: programming + color: "#012456" + nerd-font-glyph: "\U000f0a0a" + matchers: + extensions: + - ps1 + interpreters: + - pwsh +Processing: + category: programming + color: "#1E32AA" + matchers: + extensions: + - pde +Prolog: + category: programming + color: "#EF9439" + matchers: + extensions: + - P + - pl + - pro +Protocol Buffer: + category: data + color: "#33383E" + matchers: + extensions: + - proto +Pug: + category: markup + color: "#A86454" + matchers: + extensions: + - pug +PureScript: + category: programming + color: "#1D222D" + nerd-font-glyph: "\U0000e630" + matchers: + extensions: + - purs +Python: + category: programming + color: "#3472A6" + nerd-font-glyph: "\U0000e73c" + matchers: + extensions: + - py + interpreters: + - python + - python2 + - python3 +Python Requirements File: + category: data + color: "#FFD342" + nerd-font-glyph: "\U0000e73c" + matchers: + filenames: + - "requirements.txt" + patterns: + - "requirements-*.txt" + priority: 75 +QML: + category: markup + color: "#41CD52" + matchers: + extensions: + - qml +R: + category: programming + color: "#1F66B7" # Average of the two colors used in the logo gradient: https://www.r-project.org/logo/Rlogo.svg + nerd-font-glyph: "\U0000e68a" + matchers: + extensions: + - R +Racket: + category: programming + color: "#0679A7" + matchers: + extensions: + - rkt + - scrbl # NOTE Should this be separated into a "Racket Scribble" language? +Raku: + category: programming + color: "#FFFF00" + matchers: + extensions: + - p6 + - pl + - raku + - rakumod + - t + interpreters: + - perl6 + - raku +Regex: + category: pattern + color: "#44E03F" + nerd-font-glyph: "\U0000eb38" + matchers: + extensions: + - regex +"Ren'Py": + category: programming + color: "#FF7F7F" + matchers: + extensions: + - rpy +reStructured Text: + category: prose + color: "#141414" + matchers: + extensions: + - rest + - rst +Ruby: + category: programming + color: "#D21304" + nerd-font-glyph: "\U0000e23e" + matchers: + extensions: + - gemspec + - rb + filenames: + - Gemfile + - Rakefile + interpreters: + - ruby +Rust: + category: programming + color: "#DD3515" + nerd-font-glyph: "\U0000e7a8" + matchers: + extensions: + - rs +Sass: + category: markup + color: "#CF649A" + nerd-font-glyph: "\U0000e74b" + # NOTE: Sass has two syntaxes. See https://sass-lang.com/guide/ + matchers: + extensions: + - sass + - scss +Scala: + category: programming + color: "#DC3431" + matchers: + extensions: + - sbt + - sc + - scala +Scheme: + category: programming + color: "#8800FF" + nerd-font-glyph: "\U0000e6b1" + matchers: + extensions: + - scm + - ss +Shell: + category: programming + color: "#262E28" + nerd-font-glyph: "\U0000ebca" + matchers: + extensions: + - bash + - sh + - zsh + interpreters: + - bash + - sh + - zsh +SMT: + category: programming + color: "#8C64C5" + matchers: + extensions: + - smt + - smt2 +Solidity: + category: programming + color: "#2B247C" + matchers: + extensions: + - sol +SQL: + category: query + color: "#FFBF1E" + nerd-font-glyph: "\U0000e737" + matchers: + extensions: + - sql +Standard ML: + category: programming + color: "#DC566D" + matchers: + extensions: + - fun + - ML + - sig + - sml +Svelte: + category: programming + color: "#FF3E00" + nerd-font-glyph: "\U0000e697" + matchers: + extensions: + - svelte +SVG: + category: data + color: "#FFB13B" + nerd-font-glyph: "\U000f0721" + matchers: + extensions: + - svg +Swift: + category: programming + color: "#DC5114" + matchers: + extensions: + - swift +SystemVerilog: + category: programming + color: "#20C040" + matchers: + extensions: + - sv + - svh +Tcl: + category: programming + color: "#F14524" + matchers: + extensions: + - tcl +TeX: + category: markup + color: "#000080" + matchers: + extensions: + - tex +TLA: + category: programming + color: "#4B0079" + matchers: + extensions: + - tla +TOML: + category: data + color: "#9C4221" + nerd-font-glyph: "\U0000e6b2" + matchers: + extensions: + - toml + filenames: + - Cargo.lock + - poetry.lock +TSV: + category: data + color: "#1D6F42" + nerd-font-glyph: "\U0000e64a" + matchers: + extensions: + - tsv +TypeScript: + category: programming + color: "#2F74C0" + nerd-font-glyph: "\U0000e628" + heuristics: + - '(?m)^/// { + include!(concat!(env!("OUT_DIR"), "/languages/", $path)); + }; +} + +_include!("language.rs"); +_include!("category_mixin.rs"); +_include!("name_mixin.rs"); +_include!("parse_variant_mixin.rs"); +_include!("color_hex_mixin.rs"); +_include!("color_rgb_mixin.rs"); +_include!("nerd_font_glyph_mixin.rs"); +_include!("priority_mixin.rs"); +_include!("from_extension_mixin.rs"); +_include!("from_filename_mixin.rs"); +_include!("from_interpreter_mixin.rs"); +_include!("glob_mappings_mixin.rs"); +_include!("heuristic_mappings_mixin.rs"); + +impl Language { + /// Gets languages from a path's extension. + fn from_path_extension(path: impl AsRef) -> Vec { + let extension = path.as_ref().extension().and_then(|ext| ext.to_str()); + extension.map_or(vec![], Self::from_extension) + } + + /// Gets languages from a path's filename. + fn from_path_filename(path: impl AsRef) -> Vec { + let filename = path + .as_ref() + .file_name() + .and_then(|filename| filename.to_str()); + filename.map_or(vec![], Self::from_filename) + } + + /// Gets languages by a shebang. + fn from_shebang(contents: &[u8]) -> Vec { + const MAX_SHEBANG_LENGTH: usize = 50; + + let mut lines = contents.split(|&c| c == b'\n'); + let first_line = lines.next().unwrap_or_default(); + if first_line.len() < 2 || first_line[0] != b'#' || first_line[1] != b'!' { + return vec![]; + } + let first_line = if first_line.len() > MAX_SHEBANG_LENGTH { + &first_line[..MAX_SHEBANG_LENGTH] + } else { + first_line + }; + let first_line = String::from_utf8_lossy(first_line); + // NOTE Handle trailing spaces, `\r`, etc. + let first_line = first_line.trim_end(); + + static RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^#!(?:/usr(?:/local)?)?/bin/(?:env\s+)?([\w\d]+)\r?$").unwrap() + }); + + RE.captures(first_line) + .and_then(|c| c.get(1)) + .map_or(vec![], |m| { + let interpreter = m.as_str(); + Self::from_interpreter(interpreter) + }) + } + + /// Gets the languages that match a glob pattern. + pub fn from_glob(path: impl AsRef) -> Vec { + let path = path.as_ref(); + + struct GlobMapping { + patterns: Vec, + language: Language, + } + static GLOB_MAPPINGS: LazyLock> = LazyLock::new(|| { + Language::glob_mappings() + .into_iter() + .map(|(patterns, language)| { + let patterns = patterns + .into_iter() + .map(|pattern| glob::Pattern::new(pattern).unwrap()) + .collect(); + GlobMapping { patterns, language } + }) + .collect() + }); + + GLOB_MAPPINGS + .iter() + .filter(|gm| { + gm.patterns + .iter() + .any(|p| p.matches_path_with(path.as_ref(), GLOB_MATCH_OPTIONS)) + }) + .map(|gm| gm.language) + .collect() + } + + /// Filters an iterable of languages by heuristics. + fn filter_by_heuristics(languages: &[Self], contents: &str) -> Vec { + static HEURISTICS: LazyLock>> = LazyLock::new(|| { + Language::heuristic_mappings() + .into_iter() + .map(|(language, patterns)| { + let patterns = patterns + .into_iter() + .map(|pattern| Regex::new(pattern).unwrap()) + .collect(); + (language, patterns) + }) + .collect() + }); + + languages + .iter() + .filter(|language| { + HEURISTICS + .get(language) + .is_some_and(|heuristics| heuristics.iter().any(|re| re.is_match(contents))) + }) + .cloned() + .collect() + } + + /// Uses simple checks to find one or more matching languages. Checks by shebang, filename, + /// filepath glob, and extension. + fn find_simple(path: impl AsRef, contents: &[u8]) -> Vec { + let languages = Self::from_shebang(contents); + if !languages.is_empty() { + return languages; + } + let languages = Self::from_path_filename(&path); + if !languages.is_empty() { + return languages; + } + let languages = Self::from_glob(&path); + if !languages.is_empty() { + return languages; + } + Self::from_path_extension(&path) + } + + /// Picks the best guess from a file's name and contents. + /// + /// When checking heuristics, only the first `read_limit` bytes will be read. + pub fn pick(path: impl AsRef, contents: &[u8], read_limit: usize) -> Option { + let path = path.as_ref(); + let path = Self::maybe_strip_suffix_extensions(path); + let languages = Self::find_simple(path, contents); + if languages.len() == 1 { + return Some(languages[0]); + } + + let contents = if contents.len() > read_limit { + &contents[..read_limit] + } else { + contents + }; + let heuristic_contents = std::str::from_utf8(contents).unwrap_or_default(); + let by_heuristics = Self::filter_by_heuristics(&languages, heuristic_contents); + + let found_languages = match by_heuristics.len() { + 0 => languages, + 1 => return Some(by_heuristics[0]), + _ => by_heuristics, + }; + + found_languages.into_iter().max_by_key(Self::priority) + } + + /// Strips common extensions that may be appended after the "real" file extension. + fn maybe_strip_suffix_extensions(path: &Path) -> &Path { + /// Extensions that are appended to the "real" filename. + const EXTENSIONS: &[&[u8]] = &[b".bak", b".example"]; + debug_assert!(EXTENSIONS.iter().all(|bytes| bytes.is_ascii())); + + // NOTE Continuously removes the known extensions until the path doesn't have any more. + EXTENSIONS + .iter() + .cycle() + .scan(path.as_os_str().as_encoded_bytes(), |path, extension| { + *path = path.strip_suffix(*extension).unwrap_or(path); + Some(*path) + }) + // HACK Using `strip_suffix` to confirm we don't have the suffix + .find(|path| { + EXTENSIONS + .iter() + .all(|ext| path.strip_suffix(*ext).is_none()) + }) + .map(|bytes| { + // SAFETY: + // - Only ASCII suffixes have been stripped, retaining ASCII bounds + unsafe { OsStr::from_encoded_bytes_unchecked(bytes) } + }) + .map(Path::new) + .unwrap_or(path) + } + + /// Returns an object that implements `serde::Serialize` for the language to + /// serialize the language's attributes. This effectively turns the language + /// from an `enum` into a `struct`. + const fn serialize(&self) -> Serialize { + Serialize { + name: self.name(), + category: self.category(), + hex: self.hex(), + nerd_font_glyph: self.nerd_font_glyph(), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseError; + +impl FromStr for Language { + type Err = ParseError; + + /// Converts a string of the variant's name into that variant. + /// This can be useful for setting up language overrides. + fn from_str(s: &str) -> Result { + Self::parse_variant(s).ok_or(ParseError) + } +} + +impl serde::Serialize for Language { + /// Serializes the language into a string. + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // NOTE A bit redundant LOL + Self::serialize(self).serialize(serializer) + } +} + +/// A category for a language. +#[non_exhaustive] +#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Category { + /// Data files. Examples: JSON, YAML, XML, CSV, etc. + Data, + /// Markup languages. Examples: HTML, Pug, etc. + Markup, + /// Languages that define text patterns. Examples: Regex, ABNF, etc. + Pattern, + /// Programming languages. Examples: Rust, C, C++, Java, etc. + Programming, + /// Prose. Examples: Plain text, Markdown, etc. + Prose, + /// Query languages. Examples: SQL, GraphQL, etc. + Query, +} + +/// Helper struct for serializing the attributes of a `Language`. +#[derive(Debug, serde::Serialize)] +struct Serialize { + name: &'static str, + category: Category, + hex: &'static str, + nerd_font_glyph: Option<&'static str>, +} + +#[cfg(test)] +mod language_tests { + use super::*; + use rstest::rstest; + + #[rstest( + shebang, + language, + case::simple(b"#!/bin/sh", Language::Shell), + case::unix_newline(b"#!/bin/sh\n", Language::Shell), + case::windows_newline(b"#!/bin/sh\r\n", Language::Shell), + case::with_env(b"#!/usr/bin/env sh\r\n", Language::Shell) + )] + fn test_from_shebang(shebang: &[u8], language: Language) { + let languages = Language::from_shebang(shebang); + assert!(languages.contains(&language)); + } + + #[rstest( + input, + expected, + case("path/to/data.json", "path/to/data.json"), + case("path/to/data.json.example", "path/to/data.json"), + case("path/to/data.json.bak", "path/to/data.json"), + case("path/to/data.json.example.bak", "path/to/data.json") + )] + fn test_maybe_strip_suffix_extensions(input: &str, expected: &str) { + let input = Path::new(input); + let expected = Path::new(expected); + let actual = Language::maybe_strip_suffix_extensions(input); + assert_eq!(actual, expected); + } +} diff --git a/gengo-language/tests/test.rs b/gengo-language/tests/test.rs new file mode 100644 index 0000000000000000000000000000000000000000..f802dbf633ba6e0c316e8d25060fa8488b8ba900 --- /dev/null +++ b/gengo-language/tests/test.rs @@ -0,0 +1,96 @@ +use gengo_language::Language; +use std::collections::HashMap; +use std::sync::LazyLock; + +const LANGUAGES: &str = include_str!("../languages.yaml"); +static LANGUAGES_MAP: LazyLock> = + LazyLock::new(|| serde_yaml::from_str(LANGUAGES).unwrap()); + +fn get_matchers(name: &str) -> Vec<(String, Vec)> { + LANGUAGES_MAP + .iter() + .map(|(language_name, attrs)| { + let map = attrs.as_mapping().unwrap(); + let matchers = map["matchers"].as_mapping().unwrap(); + let matchers = matchers + .get(name) + .map(|matchers| matchers.as_sequence().unwrap()) + .map(|matchers| { + matchers + .iter() + .map(|matcher| matcher.as_str().unwrap().to_string()) + .collect() + }) + .unwrap_or_default(); + (language_name.to_string(), matchers) + }) + .collect() +} + +#[test] +fn test_from_extension() { + let extensions = get_matchers("extensions"); + extensions.iter().for_each(|(language_name, extensions)| { + extensions.iter().for_each(|extension| { + let languages = Language::from_extension(extension); + let languages: Vec<_> = languages.iter().map(|language| language.name()).collect(); + + assert!( + languages.contains(&language_name.as_str()), + "Language::from_extension({extension}) contains {language_name}" + ); + }); + }); +} + +#[test] +fn test_unused_extension() { + let languages = Language::from_extension("totally.unused.extension"); + assert!(languages.is_empty()); +} + +#[test] +fn test_from_filename() { + let filenames = get_matchers("filenames"); + filenames.iter().for_each(|(language_name, filenames)| { + filenames.iter().for_each(|filename| { + let languages = Language::from_filename(filename); + let languages: Vec<_> = languages.iter().map(|language| language.name()).collect(); + + assert!( + languages.contains(&language_name.as_str()), + "Language::from_filename({filename}) contains {language_name}" + ); + }); + }); +} + +#[test] +fn test_unused_filename() { + let languages = Language::from_filename("!!!totally-unused-filename!!!"); + assert!(languages.is_empty()); +} + +#[test] +fn test_from_interpreter() { + let interpreters = get_matchers("interpreters"); + interpreters + .iter() + .for_each(|(language_name, interpreters)| { + interpreters.iter().for_each(|interpreter| { + let languages = Language::from_interpreter(interpreter); + let languages: Vec<_> = languages.iter().map(|language| language.name()).collect(); + + assert!( + languages.contains(&language_name.as_str()), + "Language::from_interpreter({interpreter}) contains {language_name}" + ); + }); + }); +} + +#[test] +fn test_unused_interpreter() { + let languages = Language::from_interpreter(".totally-unused-interpreter"); + assert!(languages.is_empty()); +} diff --git a/gengo/Cargo.toml b/gengo/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..420c802d2f63373a77f7ce18c673e1820348583e --- /dev/null +++ b/gengo/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "gengo" +description.workspace = true +readme = "crates-io.md" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +rust-version.workspace = true + +keywords.workspace = true + +exclude = ["README.md"] + +[features] +default = ["directory", "git", "max-performance-safe"] + +# directory provides the ability to walk and analyze a filesystem directory +directory = ["dep:ignore"] + +# git provides the ability to read from git repositories +git = ["dep:gix"] + +# TODO Rename to gix-* +max-performance = ["dep:gix", "gix/max-performance"] +max-performance-safe = ["dep:gix", "gix/max-performance-safe"] + +[dependencies] +gengo-language = { path = "../gengo-language", version = "0.14" } +gix = { version = "0.81", optional = true, default-features = false, features = [ + "attributes", + "index", + "parallel", + "revision", + "sha1", +] } +glob.workspace = true +ignore = { version = "0.4", optional = true } +indexmap = { workspace = true, features = ["rayon", "serde"] } +rayon = "1" +regex.workspace = true +serde.workspace = true + +[dev-dependencies] +criterion = { version = "0.8", default-features = false, features = [ + "cargo_bench_support", + "rayon", +] } +insta = "1" +rstest.workspace = true +serde_yaml.workspace = true + +[[bench]] +# NOTE: We run on this repo as a real-world example +name = "run_on_self" +harness = false diff --git a/gengo/LICENSE-APACHE b/gengo/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..965b606f331b51d566b46025f9ff311a7aad0c12 --- /dev/null +++ b/gengo/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/gengo/LICENSE-MIT b/gengo/LICENSE-MIT new file mode 120000 index 0000000000000000000000000000000000000000..76219eb72e8524f15c21ec93b9b2592da49b5460 --- /dev/null +++ b/gengo/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/gengo/README.md b/gengo/README.md new file mode 120000 index 0000000000000000000000000000000000000000..32d46ee883b58d6a383eed06eb98f33aa6530ded --- /dev/null +++ b/gengo/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/gengo/benches/run_on_self.rs b/gengo/benches/run_on_self.rs new file mode 100644 index 0000000000000000000000000000000000000000..7f6f944891940023da68bb6979f87629104be8bd --- /dev/null +++ b/gengo/benches/run_on_self.rs @@ -0,0 +1,20 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use gengo::{Builder, Git}; + +fn git_benchmark(c: &mut Criterion) { + let revs = { + let mut revs: Vec<_> = (0..3).map(|n| format!("HEAD{}", "^".repeat(n))).collect(); + revs.extend_from_slice(&["test/javascript".into()]); + revs + }; + for rev in revs { + let git = Git::new(env!("CARGO_MANIFEST_DIR"), &rev).unwrap(); + let gengo = Builder::new(git).build().unwrap(); + c.bench_function(&format!("run on {}", rev), |b| { + b.iter(|| gengo.analyze().unwrap()) + }); + } +} + +criterion_group!(benches, git_benchmark); +criterion_main!(benches); diff --git a/gengo/crates-io.md b/gengo/crates-io.md new file mode 120000 index 0000000000000000000000000000000000000000..ae66dc9c49cf24ae5d8353b3c97cfc5e9783fb96 --- /dev/null +++ b/gengo/crates-io.md @@ -0,0 +1 @@ +../crates-io.md \ No newline at end of file diff --git a/gengo/src/analysis/mod.rs b/gengo/src/analysis/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..e2d0aeac3f1e79df67b6124dcf112e4fda7e1794 --- /dev/null +++ b/gengo/src/analysis/mod.rs @@ -0,0 +1,45 @@ +use super::Entry; +use indexmap::IndexMap; +use serde::Serialize; + +use std::path::PathBuf; + +pub use summary::Iter as SummaryIter; +pub use summary::Opts as SummaryOpts; +pub use summary::Summary; + +mod summary; + +/// The result of analyzing a repository along with all of its submodules. +#[derive(Debug, Serialize)] +pub struct Analysis(pub(super) IndexMap); + +impl Analysis { + pub fn iter(&self) -> impl Iterator { + let results = &self.0; + results.iter() + } + + /// Summarizes the analysis by language and size. Includes only + /// the entries that are detectable. + pub fn summary(&self) -> Summary { + let opts = SummaryOpts { + all: false, + ..Default::default() + }; + self.summary_with(opts) + } + + /// Summarizes the analysis by language and size. + pub fn summary_with(&self, opts: SummaryOpts) -> Summary { + let mut summary = IndexMap::new(); + for (_, entry) in self.iter() { + if !(opts.all || entry.detectable()) { + continue; + } + let language = *entry.language(); + *summary.entry(language).or_insert(0) += entry.size(); + } + Summary(summary) + } +} diff --git a/gengo/src/analysis/summary.rs b/gengo/src/analysis/summary.rs new file mode 100644 index 0000000000000000000000000000000000000000..02798b926bc0fbbf2978276320b6bd2bffaa9c21 --- /dev/null +++ b/gengo/src/analysis/summary.rs @@ -0,0 +1,46 @@ +use crate::Language; +use indexmap::IndexMap; +use indexmap::map::Iter as IndexMapIter; + +/// The summary of an analysis. +#[derive(Debug)] +pub struct Summary(pub(super) IndexMap); + +impl Summary { + /// Returns the total size of all languages. + pub fn total(&self) -> usize { + self.0.values().sum() + } + + /// Returns an iterator over the languages and their sizes. + pub fn iter(&self) -> Iter<'_> { + Iter(self.0.iter()) + } +} + +pub struct Iter<'map>(IndexMapIter<'map, Language, usize>); + +impl<'map> Iterator for Iter<'map> { + type Item = (&'map Language, &'map usize); + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl<'map> IntoIterator for &'map Summary { + type Item = (&'map Language, &'map usize); + type IntoIter = Iter<'map>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Options to use when creating a summary. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Opts { + /// Include all files, even if they are not detectable. + pub all: bool, +} diff --git a/gengo/src/binary.rs b/gengo/src/binary.rs new file mode 100644 index 0000000000000000000000000000000000000000..f1d2b924342022423a1b5a24505ba51c150b5906 --- /dev/null +++ b/gengo/src/binary.rs @@ -0,0 +1,53 @@ +use std::path::Path; + +pub struct Binary { + read_limit: usize, +} + +impl Binary { + pub fn new(read_limit: usize) -> Self { + Self { read_limit } + } + + /// Guesses if a file is binary. + pub fn is_binary(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.is_binary_no_read(&filepath) || self.is_binary_with_read(&filepath, contents) + } + + fn is_binary_no_read(&self, _filepath: impl AsRef) -> bool { + false + } + + fn is_binary_with_read(&self, _filepath: impl AsRef, contents: &[u8]) -> bool { + // NOTE This is currently very naive, and will need to be improved as issues + // are found. + + // TODO Simply return `false` if `contents.len()` is less than the read limit? + // This would be because exceptionally large files could be binary. + + // NOTE If any of the bytes is a null byte, this is likely binary. + contents.iter().take(self.read_limit).any(|&b| b == 0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest( + filepath, + contents, + expected, + case("foo.txt", "", false), + case("foo.txt", "Hello, world!", false), + case("foo.txt", "ab\0c", true) + )] + fn test_is_binary_with_read(filepath: &str, contents: &str, expected: bool) { + let binary = Binary::new(1 << 20); + assert_eq!( + binary.is_binary_with_read(filepath, contents.as_bytes()), + expected + ); + } +} diff --git a/gengo/src/builder.rs b/gengo/src/builder.rs new file mode 100644 index 0000000000000000000000000000000000000000..9aa9510732acb4c72e3c1e7fc856534bad8d2a2a --- /dev/null +++ b/gengo/src/builder.rs @@ -0,0 +1,50 @@ +use super::Gengo; +use super::binary::Binary; +use super::documentation::Documentation; +use super::generated::Generated; +use super::vendored::Vendored; + +use crate::file_source::FileSource; +use std::error::Error as ErrorTrait; + +/// Builds a new `Gengo` instance. +pub struct Builder FileSource<'fs>> { + file_source: FS, + read_limit: Option, +} + +impl FileSource<'fs>> Builder { + pub const DEFAULT_READ_LIMIT: usize = 1 << 20; + + pub fn new(file_source: FS) -> Self { + Self { + file_source, + read_limit: None, + } + } + + /// Sets the limit for how many bytes should be read from each file for + /// heuristic analysis. If this is not set, `DEFAULT_READ_LIMIT` will be + /// used. + pub fn read_limit(mut self, read_limit: usize) -> Self { + self.read_limit = Some(read_limit); + self + } + + pub fn build(self) -> Result, Box> { + let file_source = self.file_source; + let read_limit = self.read_limit.unwrap_or(Self::DEFAULT_READ_LIMIT); + let binary = Binary::new(read_limit); + let documentation = Documentation::new(); + let generated = Generated::new(); + let vendored = Vendored::new(); + Ok(Gengo { + file_source, + read_limit, + binary, + documentation, + generated, + vendored, + }) + } +} diff --git a/gengo/src/documentation.rs b/gengo/src/documentation.rs new file mode 100644 index 0000000000000000000000000000000000000000..3e6adb1c5a6bf7ba2bdbaf7767f1c5151e82c1cd --- /dev/null +++ b/gengo/src/documentation.rs @@ -0,0 +1,76 @@ +use super::GLOB_MATCH_OPTIONS; +use glob::Pattern; +use std::path::Path; + +pub struct Documentation { + globs: Vec, +} + +impl Documentation { + pub fn new() -> Self { + let globs = Self::globs(); + + Self { globs } + } + + pub fn is_documentation(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.is_documentation_no_read(&filepath) + || self.is_documentation_with_read(&filepath, contents) + } + + fn is_documentation_no_read(&self, filepath: impl AsRef) -> bool { + self.globs + .iter() + .any(|g| g.matches_path_with(filepath.as_ref(), GLOB_MATCH_OPTIONS)) + } + + fn is_documentation_with_read(&self, _filepath: impl AsRef, _contents: &[u8]) -> bool { + false + } + + fn globs() -> Vec { + [ + // Directories + "**/docs/**", + // Files + "**/CHANGELOG", + "**/CHANGELOG.*", + "**/HACKING", + "**/HACKING.*", + "**/README", + "**/README.*", + ] + .into_iter() + .map(|g| Pattern::new(g).unwrap()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest( + filepath, + expected, + case("docs/something.md", true), + case("src/something.rs", false), + case("docs/subfolder/something.md", true), + case("", false), + case("docs", false), + case("CHANGELOG", true), + case("CHANGELOG.txt", true), + case("CHANGELOG.md", true), + case("HACKING", true), + case("HACKING.txt", true), + case("HACKING.md", true), + case("README", true), + case("README.txt", true), + case("README.md", true) + )] + fn test_is_documentation_no_read(filepath: &str, expected: bool) { + let documentation = Documentation::new(); + assert_eq!(documentation.is_documentation_no_read(filepath), expected); + } +} diff --git a/gengo/src/error.rs b/gengo/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..26eb845325ee15f7619fc580118ae0b4a1550fa1 --- /dev/null +++ b/gengo/src/error.rs @@ -0,0 +1,64 @@ +use std::error::Error as ErrorTrait; +use std::fmt; + +macro_rules! error_kind { + ($($name:ident, $message:literal),*) => { + /// The kind of error that occurred. + #[derive(Debug)] + #[non_exhaustive] + pub enum ErrorKind { + $( + $name, + )* + } + + impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + Self::$name => write!(f, $message), + )* + } + } + } + }; +} + +error_kind!(NoRepository, "no repository found"); + +impl ErrorTrait for ErrorKind {} + +#[derive(Debug)] +pub struct Error { + kind: ErrorKind, + source: Option>, +} + +impl Error { + pub fn new(kind: ErrorKind) -> Self { + Self { kind, source: None } + } + + pub fn with_source(kind: ErrorKind, source: impl ErrorTrait + 'static) -> Self { + Self { + kind, + source: Some(Box::new(source)), + } + } + + pub fn kind(&self) -> &ErrorKind { + &self.kind + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.kind) + } +} + +impl ErrorTrait for Error { + fn source(&self) -> Option<&(dyn ErrorTrait + 'static)> { + self.source.as_ref().map(|s| s.as_ref()) + } +} diff --git a/gengo/src/file_source/directory.rs b/gengo/src/file_source/directory.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7942024169bd3b6983a197cd51b68e5eb7f99eb --- /dev/null +++ b/gengo/src/file_source/directory.rs @@ -0,0 +1,91 @@ +use super::FileSource; +use crate::Result; +use ignore::{Walk, WalkBuilder}; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +/// A file source that reads files from a directory. +/// +/// Will try to ignore files be default. See [`WalkBuilder`](ignore::WalkBuilder) +/// for more information. +pub struct Directory { + buf_size: usize, + walk_builder: WalkBuilder, +} + +pub struct Iter { + walk: Walk, +} + +// TODO Simpler API than having to copy the same read_limit/buf_size value to multiple places? +impl Directory { + /// Creates a new directory file source. + /// + /// Set `buf_size` to a reasonable value for your system. You most likely + /// would want to set this to the same value as `read_limit` when building + /// a [`Gengo`](crate::Gengo) instance with a [`Builder`](crate::Builder). + pub fn new(path: impl AsRef, buf_size: usize) -> Result { + let path = path.as_ref(); + if !path.is_dir() { + return Err("path is not a directory".into()); + } + let walk_builder = WalkBuilder::new(path); + let directory = Self { + buf_size, + walk_builder, + }; + Ok(directory) + } +} + +impl<'files> FileSource<'files> for Directory { + type Entry = PathBuf; + type Filepath = PathBuf; + type Contents = Vec; + type State = (); + type Iter = Iter; + + fn entries(&'files self) -> crate::Result { + // NOTE `new` should assert that `path` is always a directory + let walk = self.walk_builder.build(); + Ok(Iter { walk }) + } + + fn filepath( + &'files self, + entry: &Self::Entry, + _state: &mut Self::State, + ) -> crate::Result { + Ok(entry.to_owned()) + } + + fn contents( + &'files self, + entry: &Self::Entry, + _state: &mut Self::State, + ) -> crate::Result { + let mut reader = File::open(entry)?.take(self.buf_size.try_into()?); + let mut buf = vec![0; self.buf_size]; + let read_amount = reader.read(&mut buf)?; + buf.resize(read_amount, 0); + Ok(buf) + } + + fn state(&'files self) -> crate::Result { + Ok(()) + } +} + +impl Iterator for Iter { + /// Deeply iterates over the directory. + type Item = PathBuf; + + fn next(&mut self) -> Option { + // TODO Handle error? + self.walk + .next() + .and_then(|entry| entry.ok()) + .map(|dir_entry| dir_entry.path().to_owned()) + } +} diff --git a/gengo/src/file_source/git.rs b/gengo/src/file_source/git.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a2ea4658dad3a082847bc08e8e7a3e20221b683 --- /dev/null +++ b/gengo/src/file_source/git.rs @@ -0,0 +1,213 @@ +use super::{FileSource, Overrides}; +use crate::{Error, ErrorKind}; +use gix::{ + Repository, ThreadSafeRepository, + attrs::StateRef, + attrs::search::Outcome as AttrOutcome, + bstr::ByteSlice, + discover::Error as DiscoverError, + index::{self, entry::Mode as EntryMode}, + worktree::{Stack as WTStack, stack::state::attributes::Source as AttrSource}, +}; +use std::borrow::Cow; +use std::path::Path; + +use std::slice; + +struct Builder { + repository: ThreadSafeRepository, + rev: String, +} + +impl Builder { + fn new(path: impl AsRef, rev: &str) -> crate::Result { + let repository = match gix::discover(path) { + Ok(r) => r, + Err(DiscoverError::Discover(err)) => { + return Err(Box::new(Error::with_source(ErrorKind::NoRepository, err))); + } + Err(err) => return Err(err.into()), + }; + + let repository = repository.into_sync(); + let rev = rev.to_string(); + Ok(Self { repository, rev }) + } + + /// Constructs a [`State`] for the repository and rev. + fn state(&self) -> crate::Result<(State, index::State)> { + let repo = self.repository.to_thread_local(); + let tree_id = repo + .rev_parse_single(self.rev.as_str())? + .object()? + .peel_to_tree()? + .id; + let index = repo.index_from_tree(&tree_id)?; + let attr_stack = repo.attributes_only(&index, AttrSource::IdMapping)?; + let attr_matches = attr_stack.selected_attribute_matches(Git::OVERRIDE_ATTRS); + let (index_state, _) = index.into_parts(); + let attr_stack = attr_stack.detach(); + let state = State { + attr_stack, + attr_matches, + }; + Ok((state, index_state)) + } + + fn build(self) -> crate::Result { + let (state, index_state) = self.state()?; + let git = Git { + repository: self.repository, + state, + index_state, + }; + Ok(git) + } +} + +pub struct Git { + repository: ThreadSafeRepository, + state: State, + index_state: index::State, +} + +impl Git { + const OVERRIDE_ATTRS: [&'static str; 5] = [ + "gengo-language", + "gengo-documentation", + "gengo-generated", + "gengo-vendored", + "gengo-detectable", + ]; + const LANGUAGE_OVERRIDE: usize = 0; + const DOCUMENTATION_OVERRIDE: usize = 1; + const GENERATED_OVERRIDE: usize = 2; + const VENDORED_OVERRIDE: usize = 3; + const DETECTABLE_OVERRIDE: usize = 4; + pub fn new(path: impl AsRef, rev: &str) -> crate::Result { + Builder::new(path, rev)?.build() + } +} + +impl<'repo> FileSource<'repo> for Git { + type Entry = &'repo index::Entry; + type Filepath = Cow<'repo, Path>; + type Contents = Vec; + type State = (State, Repository); + type Iter = Iter<'repo>; + + fn entries(&'repo self) -> crate::Result { + let entries = self.index_state.entries().iter(); + Ok(Iter { entries }) + } + + fn filepath( + &'repo self, + entry: &Self::Entry, + _state: &mut Self::State, + ) -> crate::Result { + let path_storage = self.index_state.path_backing(); + let path = entry.path_in(path_storage); + let path = gix::path::try_from_bstr(path)?; + Ok(path) + } + + fn contents( + &'repo self, + entry: &Self::Entry, + (_, repository): &mut Self::State, + ) -> crate::Result { + let blob = repository.find_object(entry.id)?; + let blob = blob.detach(); + let contents = blob.data; + Ok(contents) + } + + fn state(&'repo self) -> crate::Result { + Ok((self.state.clone(), self.repository.to_thread_local())) + } + + fn overrides( + &self, + path: impl AsRef, + (state, repository): &mut Self::State, + ) -> Overrides { + let path = path.as_ref(); + let Ok(platform) = state.attr_stack.at_path( + path, + Some(EntryMode::FILE | EntryMode::FILE_EXECUTABLE), + &repository.objects, + ) else { + // NOTE If we cannot get overrides, simply don't return them. + return Default::default(); + }; + platform.matching_attributes(&mut state.attr_matches); + + let attrs = { + let mut attrs = [None, None, None, None, None]; + state + .attr_matches + .iter_selected() + .zip(attrs.iter_mut()) + .for_each(|(info, slot)| { + *slot = (info.assignment.state != StateRef::Unspecified).then_some(info); + }); + attrs + }; + + let language = + attrs[Self::LANGUAGE_OVERRIDE] + .as_ref() + .and_then(|info| match info.assignment.state { + StateRef::Value(v) => v.as_bstr().to_str().ok().and_then(|s| s.parse().ok()), + _ => None, + }); + // NOTE Unspecified attributes are None, so `state.is_set()` is + // implicitly `!state.is_unset()`. + // TODO This is really repetitive. Refactor to iteration? + let is_documentation = attrs[Self::DOCUMENTATION_OVERRIDE] + .as_ref() + .map(|info| info.assignment.state.is_set()); + let is_generated = attrs[Self::GENERATED_OVERRIDE] + .as_ref() + .map(|info| info.assignment.state.is_set()); + let is_vendored = attrs[Self::VENDORED_OVERRIDE] + .as_ref() + .map(|info| info.assignment.state.is_set()); + let is_detectable = attrs[Self::DETECTABLE_OVERRIDE] + .as_ref() + .map(|info| info.assignment.state.is_set()); + + Overrides { + language, + is_documentation, + is_generated, + is_vendored, + is_detectable, + } + } +} + +pub struct Iter<'repo> { + entries: slice::Iter<'repo, index::Entry>, +} + +impl<'repo> Iterator for Iter<'repo> { + type Item = &'repo index::Entry; + + fn next(&mut self) -> Option { + let entry = loop { + let entry = self.entries.next()?; + if matches!(entry.mode, EntryMode::FILE | EntryMode::FILE_EXECUTABLE) { + break entry; + } + }; + Some(entry) + } +} + +#[derive(Clone)] +pub struct State { + attr_stack: WTStack, + attr_matches: AttrOutcome, +} diff --git a/gengo/src/file_source/mod.rs b/gengo/src/file_source/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b09921bed3fb8495fa8b7e344b2a6f468624b37 --- /dev/null +++ b/gengo/src/file_source/mod.rs @@ -0,0 +1,114 @@ +//! Provides sources to get files and their attributes. + +use crate::Language; + +#[cfg(feature = "directory")] +pub use directory::Directory; + +#[cfg(feature = "git")] +pub use git::Git; +use std::marker::{Send, Sync}; +use std::path::Path; + +#[cfg(feature = "directory")] +mod directory; + +#[cfg(feature = "git")] +mod git; + +/// Provides files and overrides. +pub trait FileSource<'files>: Sync { + type Filepath: AsRef; + type Contents: AsRef<[u8]>; + type Entry: Send; + /// Sometimes it's necessary to share a state between iterations to reduce + /// expensive commands. + type State: Send + Clone; + type Iter: Iterator + Send; + + /// Returns an iterator over the entries use to get filenames and contents. + fn entries(&'files self) -> crate::Result; + + /// Gets a filename from an entry. + fn filepath( + &'files self, + entry: &Self::Entry, + state: &mut Self::State, + ) -> crate::Result; + + /// Gets file contents from an entry. + fn contents( + &'files self, + entry: &Self::Entry, + state: &mut Self::State, + ) -> crate::Result; + + /// Gets a state that can be shared between iterations. + fn state(&'files self) -> crate::Result; + + /// Provides combined overrides for the file. + fn overrides(&self, path: impl AsRef, state: &mut Self::State) -> Overrides { + Overrides { + language: self.language_override(&path, state), + is_documentation: self.is_documentation_override(&path, state), + is_generated: self.is_generated_override(&path, state), + is_vendored: self.is_vendored_override(&path, state), + is_detectable: self.is_detectable_override(&path, state), + } + } + + /// Provides an optional override for the detected language. + fn language_override( + &self, + _path: impl AsRef, + _state: &mut Self::State, + ) -> Option { + None + } + + /// Provides an optional override for documentation file detection. + fn is_documentation_override( + &self, + _path: impl AsRef, + _state: &mut Self::State, + ) -> Option { + None + } + + /// Provides an optional override for generated file detection. + fn is_generated_override( + &self, + _path: impl AsRef, + _state: &mut Self::State, + ) -> Option { + None + } + + /// Provides an optional override for vendored file detection. + fn is_vendored_override( + &self, + _path: impl AsRef, + _state: &mut Self::State, + ) -> Option { + None + } + + /// Provides an optional override for if the file is detectable. + fn is_detectable_override( + &self, + _path: impl AsRef, + _state: &mut Self::State, + ) -> Option { + None + } +} + +#[non_exhaustive] +#[derive(Default)] +pub struct Overrides { + pub language: Option, + pub is_documentation: Option, + pub is_generated: Option, + pub is_vendored: Option, + pub is_detectable: Option, +} diff --git a/gengo/src/generated.rs b/gengo/src/generated.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb31784a2dc048cf5efe798f194ade18252b5ee6 --- /dev/null +++ b/gengo/src/generated.rs @@ -0,0 +1,102 @@ +use super::GLOB_MATCH_OPTIONS; +use glob::Pattern; +use std::collections::HashSet; +use std::path::Path; + +pub struct Generated { + filenames: HashSet<&'static str>, + globs: Vec, +} + +impl Generated { + pub fn new() -> Self { + let filenames = Self::filenames(); + let globs = Self::globs(); + + Self { filenames, globs } + } + + pub fn is_generated(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.is_generated_no_read(&filepath) || self.is_generated_with_read(&filepath, contents) + } + + fn is_generated_no_read(&self, filepath: impl AsRef) -> bool { + self.matches_filenames(&filepath) || self.matches_globs(&filepath) + } + + fn is_generated_with_read(&self, _filepath: impl AsRef, contents: &[u8]) -> bool { + self.likely_minified(contents) + } + + fn matches_filenames(&self, filepath: impl AsRef) -> bool { + let filename = filepath.as_ref().file_name().and_then(|f| f.to_str()); + filename.is_some_and(|f| self.filenames.contains(f)) + } + + fn matches_globs(&self, filepath: impl AsRef) -> bool { + self.globs + .iter() + .any(|g| g.matches_path_with(filepath.as_ref(), GLOB_MATCH_OPTIONS)) + } + + fn likely_minified(&self, contents: &[u8]) -> bool { + // NOTE If the first 10 lines are really long, it's probably minified. + contents + .split(|&b| b == b'\n') + .take(10) + .any(|line| line.len() > 250) + } + + fn filenames() -> HashSet<&'static str> { + HashSet::from_iter(["gradlew", "gradlew.bat"]) + } + + fn globs() -> Vec { + [ + "dist/**", + "**/*.min.css", + "**/*.min.js", + ".yarn/**", + "**/migrations/*.py", + ] + .into_iter() + .map(|s| Pattern::new(s).unwrap()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest( + filepath, + expected, + case("dist/something.js", true), + case("src/something.rs", false), + case("dist/subfolder/something.js", true), + case("something.min.js", true), + case("something.min.css", true), + case("path/to/something.min.js", true), + case("path/to/something.min.css", true), + case(".yarn/releases/yarn-1.2.3.cjs", true), + case("migrations/0001_initial.py", true), + case("myapp/migrations/0001_initial.py", true), + case("gradlew", true), + case("gradlew.bat", true) + )] + fn test_is_generated_no_read(filepath: &str, expected: bool) { + let generated = Generated::new(); + assert_eq!(generated.is_generated_no_read(filepath), expected); + } + + #[test] + fn test_likely_minified() { + let generated = Generated::new(); + let header: Vec = b"/*!\n * This is my license etc etc\n */".to_vec(); + let contents = b"console.log('hello, world!');".repeat(50); + let contents = [header, contents].concat(); + assert!(generated.likely_minified(&contents)); + } +} diff --git a/gengo/src/language/mod.rs b/gengo/src/language/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..d32c3b4ab05baacb5087f3192c9ef3318eefac34 --- /dev/null +++ b/gengo/src/language/mod.rs @@ -0,0 +1 @@ +pub use gengo_language::*; diff --git a/gengo/src/lib.rs b/gengo/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..78cbb3ec7501b0741cbe680b02c8ab81d93ce7d2 --- /dev/null +++ b/gengo/src/lib.rs @@ -0,0 +1,228 @@ +//! Gengo is a language detection library for collections of files. +//! Currently, it supports reading from git repositories. +//! +//! # Features +//! +//! ## `directory` +//! +//! Provides the `Directory` file source, which reads files from a local directory. +//! +//! ## `git` +//! +//! Provides the `Git` file source, which reads files from a git repository. It reads +//! from a specified revision and supports git attributes for overrides, making it the +//! most similar to [GitHub Linguist][github-linguist]. Because of this, it also works +//! on bare repositories. +//! +//! # Example +//! +//! ```no_run +//! # #[cfg(feature = "git")] +//! # { +//! use gengo::{Builder, Git}; +//! let git = Git::new("path/to/repo", "HEAD").unwrap(); +//! let gengo = Builder::new(git).build().unwrap(); +//! let results = gengo.analyze().unwrap(); +//! # } +//! ``` +//! +//! [github-linguist]: https://github.com/github-linguist/linguist + +pub use analysis::Analysis; +use binary::Binary; +pub use builder::Builder; +use documentation::Documentation; + +pub use error::{Error, ErrorKind}; +use generated::Generated; + +#[cfg(feature = "directory")] +pub use file_source::Directory; + +#[cfg(feature = "git")] +pub use file_source::Git; + +pub use file_source::FileSource; +use glob::MatchOptions; +use indexmap::IndexMap; +use language::Category; +pub use language::Language; + +use std::error::Error as ErrorTrait; +use std::path::Path; + +use vendored::Vendored; + +use rayon::prelude::{FromParallelIterator, ParallelBridge, ParallelIterator}; +use serde::Serialize; + +pub mod analysis; +mod binary; +mod builder; +mod documentation; +mod error; +mod file_source; +mod generated; +pub mod language; +mod vendored; + +type GenericError = Box; +type Result = std::result::Result; + +/// Shared match options for consistent behavior. +const GLOB_MATCH_OPTIONS: MatchOptions = MatchOptions { + case_sensitive: true, + require_literal_separator: true, + require_literal_leading_dot: false, +}; + +/// The main entry point for Gengo. +pub struct Gengo FileSource<'fs>> { + file_source: FS, + read_limit: usize, + binary: Binary, + documentation: Documentation, + generated: Generated, + vendored: Vendored, +} + +impl FileSource<'fs>> Gengo { + /// Analyzes each file in the repository at the given revision. + pub fn analyze(&self) -> Result { + let state = self.file_source.state()?; + let entries = self + .file_source + .entries()? + .par_bridge() + .map_with(state, |state, entry| { + let filepath = self.file_source.filepath(&entry, state).ok()?; + let contents = self.file_source.contents(&entry, state).ok()?; + + let entry = self.analyze_blob(&filepath, contents, state)?; + Some((filepath.as_ref().to_owned(), entry)) + }) + .filter_map(|entry| entry); + let entries = IndexMap::from_par_iter(entries); + + Ok(Analysis(entries)) + } + + fn analyze_blob( + &self, + filepath: impl AsRef, + contents: impl AsRef<[u8]>, + state: &mut ::State, + ) -> Option { + let overrides = self.file_source.overrides(&filepath, state); + let filepath = filepath.as_ref(); + let contents = contents.as_ref(); + + // NOTE Users might be surprised if there is an override for a binary file but it + // is still skipped, but this should be a rare case. + if self.is_binary(filepath, contents) { + return None; + } + + let language = overrides + .language + .or_else(|| Language::pick(filepath, contents, self.read_limit))?; + let generated = overrides + .is_generated + .unwrap_or_else(|| self.is_generated(filepath, contents)); + let documentation = overrides + .is_documentation + .unwrap_or_else(|| self.is_documentation(filepath, contents)); + let vendored = overrides + .is_vendored + .unwrap_or_else(|| self.is_vendored(filepath, contents)); + + let detectable = match language.category() { + Category::Data | Category::Prose => false, + Category::Pattern | Category::Programming | Category::Markup | Category::Query => { + !(generated || documentation || vendored) + } + category => unimplemented!("Failed to check if category {category:?} is detectable"), + }; + let detectable = overrides.is_detectable.unwrap_or(detectable); + + let size = contents.len(); + let entry = Entry { + language, + size, + detectable, + generated, + documentation, + vendored, + }; + Some(entry) + } + + /// Guesses if a file is generated. + pub fn is_generated(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.generated.is_generated(filepath, contents) + } + + /// Guesses if a file is documentation. + pub fn is_documentation(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.documentation.is_documentation(filepath, contents) + } + + /// Guesses if a file is vendored. + pub fn is_vendored(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.vendored.is_vendored(filepath, contents) + } + + /// Guesses if a file is binary. + pub fn is_binary(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.binary.is_binary(filepath, contents) + } +} + +/// A single entry in the language statistics. +#[derive(Debug, Serialize)] +pub struct Entry { + /// The detected language. + language: Language, + /// The size of the file. + size: usize, + /// If the file is detectable (should not be ignored). + detectable: bool, + /// If the file was generated. + generated: bool, + /// If the file is documentation. + documentation: bool, + /// If the file is vendored. + vendored: bool, +} + +impl Entry { + /// The detected language. + pub fn language(&self) -> &Language { + &self.language + } + + /// The size of the file. + pub fn size(&self) -> usize { + self.size + } + + /// If the file is detectable (should not be ignored). + pub fn detectable(&self) -> bool { + self.detectable + } + + /// If the file was generated. + pub fn generated(&self) -> bool { + self.generated + } + + /// If the file is documentation. + pub fn documentation(&self) -> bool { + self.documentation + } + + /// If the file is vendored. + pub fn vendored(&self) -> bool { + self.vendored + } +} diff --git a/gengo/src/vendored.rs b/gengo/src/vendored.rs new file mode 100644 index 0000000000000000000000000000000000000000..9258f1592be0d4619d29dffd1d635dac375e0a6f --- /dev/null +++ b/gengo/src/vendored.rs @@ -0,0 +1,58 @@ +use super::GLOB_MATCH_OPTIONS; +use glob::Pattern; +use std::path::Path; + +pub struct Vendored { + globs: Vec, +} + +impl Vendored { + pub fn new() -> Self { + let globs = Self::globs(); + + Self { globs } + } + + pub fn is_vendored(&self, filepath: impl AsRef, contents: &[u8]) -> bool { + self.is_vendored_no_read(&filepath) || self.is_vendored_with_read(&filepath, contents) + } + + fn is_vendored_no_read(&self, filepath: impl AsRef) -> bool { + self.globs + .iter() + .any(|g| g.matches_path_with(filepath.as_ref(), GLOB_MATCH_OPTIONS)) + } + + fn is_vendored_with_read(&self, _filepath: impl AsRef, _contents: &[u8]) -> bool { + false + } + + fn globs() -> Vec { + ["**/node_modules/**", "**/tests/fixtures/**"] + .into_iter() + .map(|g| Pattern::new(g).unwrap()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest( + filepath, + expected, + case("node_modules/library/something.js", true), + case("src/something.rs", false), + case("node_modules/subfolder/something.js", true), + case("", false), + case("node_modules", false), + case("tests/fixtures/foo.json", true), + case("package/tests/fixtures/foo.json", true) + )] + fn test_is_vendored_no_read(filepath: &str, expected: bool) { + let vendored = Vendored::new(); + assert_eq!(vendored.is_vendored_no_read(filepath), expected); + } +} diff --git a/gengo/tests/gengo_test.rs b/gengo/tests/gengo_test.rs new file mode 100644 index 0000000000000000000000000000000000000000..a5523ffc14a79113c8832778cfcc07ac08df6c5e --- /dev/null +++ b/gengo/tests/gengo_test.rs @@ -0,0 +1,16 @@ +#![cfg(feature = "git")] +use gengo::{Builder, Git}; + +const ROOT: &str = env!("CARGO_MANIFEST_DIR"); + +#[test] +fn test_git_javascript() { + // TODO It's not great to use a snapshot test on a type that doesn't + // guarantee order. Improve this test. + let git = Git::new(ROOT, "test/javascript").unwrap(); + let gengo = Builder::new(git).build().unwrap(); + let analysis = gengo.analyze().unwrap(); + let mut results: Vec<_> = analysis.iter().collect(); + results.sort_by_key(|(path, _)| path.to_owned()); + insta::assert_debug_snapshot!(results); +} diff --git a/gengo/tests/snapshots/gengo_test__git_javascript.snap b/gengo/tests/snapshots/gengo_test__git_javascript.snap new file mode 100644 index 0000000000000000000000000000000000000000..ed58a8ca38abf74db1f21985cf44bb41a6efb5be --- /dev/null +++ b/gengo/tests/snapshots/gengo_test__git_javascript.snap @@ -0,0 +1,61 @@ +--- +source: gengo/tests/gengo_test.rs +expression: results +--- +[ + ( + "bin.js", + Entry { + language: Javascript, + size: 28, + detectable: true, + generated: false, + documentation: false, + vendored: false, + }, + ), + ( + "dist/bin.js", + Entry { + language: PlainText, + size: 62, + detectable: true, + generated: true, + documentation: false, + vendored: false, + }, + ), + ( + "docs/index.html", + Entry { + language: Html, + size: 26, + detectable: false, + generated: false, + documentation: true, + vendored: false, + }, + ), + ( + "node_modules/my-dependency/index.js", + Entry { + language: Javascript, + size: 29, + detectable: false, + generated: false, + documentation: false, + vendored: true, + }, + ), + ( + "src/bin.ts", + Entry { + language: Typescript, + size: 62, + detectable: true, + generated: false, + documentation: false, + vendored: false, + }, + ), +] diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..5f3e7fd384a529e08e6672dd7c8f346383f9afb2 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,31 @@ +# Create gengo directory in AppData/Roaming +$gengoPath = "$env:APPDATA\gengo" +New-Item -ItemType Directory -Force -Path $gengoPath -ErrorAction SilentlyContinue | Out-Null +Write-Host "Installing gengo to $gengoPath" + +# Create and use temporary directory +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) +New-Item -ItemType Directory -Force -Path $tempDir -ErrorAction SilentlyContinue | Out-Null +Push-Location $tempDir + +try { + # Download and extract gengo + $tempFile = "gengo-installer.tar.gz" + Invoke-WebRequest -Uri "https://github.com/spenserblack/gengo/releases/latest/download/gengo-x86_64-pc-windows-msvc.tar.gz" -OutFile $tempFile + tar.exe -xzf $tempFile + + # Move executable to AppData/Roaming/gengo + Move-Item -Force "gengo.exe" $gengoPath + + # Add to PATH + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($userPath -notlike "*$gengoPath*") { + [Environment]::SetEnvironmentVariable("Path", "$userPath;$gengoPath", "User") + } +} finally { + # Clean up and restore location + Pop-Location + Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue +} + +Write-Host "Gengo has been installed and added to your PATH. Please restart your terminal for the changes to take effect." diff --git a/install.sh b/install.sh new file mode 100755 index 0000000000000000000000000000000000000000..5dcd77eb787dac217be2672b6e072eb202254b9a --- /dev/null +++ b/install.sh @@ -0,0 +1,37 @@ +#!/bin/sh +set -e + +OS_NAME=$(uname -s) +ARCH_NAME=$(uname -m) +OS="" +ARCH="" +INSTALL_DIR="/usr/local/bin" +NO_RELEASE_ASSET="" + +echo "Installing gengo to $INSTALL_DIR" +echo "This script will activate sudo to install to $INSTALL_DIR" +sudo echo "sudo activated" + +# TODO Handle other architectures +if [ "$OS_NAME" = "Linux" ]; then + OS="Linux" + BUILD_TARGET="x86_64-unknown-linux-gnu" +elif [ "$OS_NAME" = "Darwin" ]; then + OS="macOS" + BUILD_TARGET="x86_64-apple-darwin" +else + NO_RELEASE_ASSET="true" + echo "There is no Unix release for this OS: $OS_NAME" >&2 +fi + +if [ "$ARCH_NAME" != "x86_64" ]; then + NO_RELEASE_ASSET="true" + echo "There is no release for this architecture: $ARCH_NAME" >&2 +fi + +if [ "$NO_RELEASE_ASSET" ]; then + exit 1 +fi + +curl -sSL "https://github.com/spenserblack/gengo/releases/latest/download/gengo-$BUILD_TARGET.tar.gz" | sudo tar -C "$INSTALL_DIR" -xzf - +echo "successfully installed gengo" diff --git a/languages.yaml b/languages.yaml new file mode 120000 index 0000000000000000000000000000000000000000..d878a8a0fcdf332c5c05c2a7a8398f7040b87297 --- /dev/null +++ b/languages.yaml @@ -0,0 +1 @@ +gengo-language/languages.yaml \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000000000000000000000000000000000..8825102061887345acddf342f28afaca45b73ca1 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.86.0" +components = ["clippy", "rustfmt"] diff --git a/samples-test/Cargo.toml b/samples-test/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..87acb0c7688c0f09431c6897be33258e542ee09e --- /dev/null +++ b/samples-test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "samples-test" +publish = false +edition.workspace = true +description.workspace = true +version.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true + +[dependencies] +gengo = { path = "../gengo", version = "0.14", default-features = false } diff --git a/samples-test/samples/Batch File/calculator.bat b/samples-test/samples/Batch File/calculator.bat new file mode 100644 index 0000000000000000000000000000000000000000..08498a4791ae2c6ec1ccccb9a9cde61e09e37959 --- /dev/null +++ b/samples-test/samples/Batch File/calculator.bat @@ -0,0 +1,32 @@ +@echo off +:start +cls +echo Simple Calculator +echo ================ +echo 1. Addition +echo 2. Subtraction +echo 3. Multiplication +echo 4. Division +echo 5. Exit +echo. + +set /p choice="Enter your choice (1-5): " +if %choice%==5 goto end + +set /p num1="Enter first number: " +set /p num2="Enter second number: " + +if %choice%==1 set /a result=%num1%+%num2% +if %choice%==2 set /a result=%num1%-%num2% +if %choice%==3 set /a result=%num1%*%num2% +if %choice%==4 set /a result=%num1%/%num2% + +echo. +echo Result: %result% +echo. +pause +goto start + +:end +echo Thanks for using the calculator! +pause \ No newline at end of file diff --git a/samples-test/samples/Batch File/sysinfo.cmd b/samples-test/samples/Batch File/sysinfo.cmd new file mode 100644 index 0000000000000000000000000000000000000000..f75afdecd750637a21ee74a2c58095d9c5e010b3 --- /dev/null +++ b/samples-test/samples/Batch File/sysinfo.cmd @@ -0,0 +1,12 @@ +@echo off +echo ================ Some System Information ================ + +echo. +echo Windows Version: +ver + +echo. +echo System Architecture: +wmic os get osarchitecture + +pause diff --git a/samples-test/samples/C++/progress-bar.cpp b/samples-test/samples/C++/progress-bar.cpp new file mode 100644 index 0000000000000000000000000000000000000000..bc3112163f5e6570550c7ec20a435c9db1a2a239 --- /dev/null +++ b/samples-test/samples/C++/progress-bar.cpp @@ -0,0 +1,90 @@ +#include "progress-bar.h" +#include +#include + +ProgressBar::ProgressBar(char symbol, int length, int total) + : symbol(symbol) + , length(length) + , progress(0) + , total(total) + , startSymbol('[') + , endSymbol(']') + , format("{bar} {percent} {count}") + , showPercent(false) + , showCount(false) {} + +ProgressBar& ProgressBar::update(int progress) { + this->progress = progress; + return *this; +} + +ProgressBar& ProgressBar::showPercentage(bool show) { + showPercent = show; + return *this; +} + +ProgressBar& ProgressBar::showCounter(bool show) { + showCount = show; + return *this; +} + +ProgressBar& ProgressBar::setStartEndSymbols(char start, char end) { + startSymbol = start; + endSymbol = end; + return *this; +} + +ProgressBar& ProgressBar::setCustomFormat(const std::string& format) { + this->format = format; + return *this; +} + +ProgressBar& ProgressBar::tick() { + progress++; + return *this; +} + +void ProgressBar::print() const { + std::ostringstream bar; + std::string percent_str; + std::string count_str; + std::string result; + + // Generate bar component + bar << startSymbol; + int scaled_progress = static_cast(static_cast(progress) * length / total); + + for (int i = 0; i < length; i++) { + bar << (i < scaled_progress ? symbol : ' '); + } + bar << endSymbol; + + // Generate percent component + int percent = (progress * 100) / total; + if (showPercent) { + percent_str = std::to_string(percent) + "%"; + } + + // Generate count component + if (showCount) { + count_str = std::to_string(progress) + "/" + std::to_string(total); + } + + // Process format string + size_t pos = 0; + std::string formatStr = format; + + // Replace placeholders with actual values + while ((pos = formatStr.find("{bar}")) != std::string::npos) { + formatStr.replace(pos, 5, bar.str()); + } + while ((pos = formatStr.find("{percent}")) != std::string::npos) { + formatStr.replace(pos, 9, percent_str); + } + while ((pos = formatStr.find("{count}")) != std::string::npos) { + formatStr.replace(pos, 7, count_str); + } + + std::cout << formatStr << (percent < 100 ? "\r" : "\n"); + std::cout.flush(); +} diff --git a/samples-test/samples/C++/progress-bar.h b/samples-test/samples/C++/progress-bar.h new file mode 100644 index 0000000000000000000000000000000000000000..75487d6e1a3ea594f9a09a743301eb844af70a05 --- /dev/null +++ b/samples-test/samples/C++/progress-bar.h @@ -0,0 +1,30 @@ +#ifndef PROGRESS_BAR_H +#define PROGRESS_BAR_H + +#include + +class ProgressBar { +private: + char symbol; + char startSymbol; + char endSymbol; + int length; + int progress; + int total; + std::string format; + bool showPercent; + bool showCount; + +public: + ProgressBar(char symbol, int length, int total); + + ProgressBar& update(int progress); + ProgressBar& showPercentage(bool show); + ProgressBar& showCounter(bool show); + ProgressBar& setStartEndSymbols(char start, char end); + ProgressBar& setCustomFormat(const std::string& format); + ProgressBar& tick(); + void print() const; +}; + +#endif diff --git a/samples-test/samples/C/progress-bar.c b/samples-test/samples/C/progress-bar.c new file mode 100644 index 0000000000000000000000000000000000000000..ab4edad656aee11401969b0b1d81d56537226c00 --- /dev/null +++ b/samples-test/samples/C/progress-bar.c @@ -0,0 +1,115 @@ +#include "progress-bar.h" +#include +#include +#include + +ProgressBar init(char symbol, int length, int total) { + ProgressBar pb; + pb.symbol = symbol; + pb.length = length; + pb.progress = 0; + pb.showPercent = false; + pb.total = total; + pb.startSymbol = '['; + pb.endSymbol = ']'; + pb.format = "{bar} {percent} {count}"; + return pb; +} + +ProgressBar update(ProgressBar *pb, int progress) { + pb->progress = progress; + return *pb; +} + +ProgressBar showPercent(ProgressBar *pb, bool show) { + pb->showPercent = show; + return *pb; +} + +ProgressBar showCount(ProgressBar *pb, bool show) { + pb->showCount = show; + return *pb; +} + +ProgressBar setStartEndSymbols(ProgressBar *pb, char start, char end) { + pb->startSymbol = start; + pb->endSymbol = end; + return *pb; +} + +ProgressBar setCustomFormat(ProgressBar *pb, char* format) { + pb->format = format; + return *pb; +} + +ProgressBar tick(ProgressBar *pb) { + pb->progress++; + return *pb; +} + +void print(ProgressBar pb) { + char bar[256] = ""; + char percent_str[32] = ""; + char count_str[32] = ""; + char result[512] = ""; + char *format = pb.format; + + // Generate bar component + sprintf(bar, "%c", pb.startSymbol); + int scaled_progress = (int)((float)pb.progress * pb.length / pb.total); + + for (int i = 0; i < pb.length; i++) { + if (i < scaled_progress) { + sprintf(bar + strlen(bar), "%c", pb.symbol); + } else { + strcat(bar, " "); + } + } + sprintf(bar + strlen(bar), "%c", pb.endSymbol); + + // Generate percent component + int percent = (pb.progress * 100) / pb.total; + if (pb.showPercent) { + sprintf(percent_str, "%d%%", percent); + } + + // Generate count component + if (pb.showCount) { + sprintf(count_str, "%d/%d", pb.progress, pb.total); + } + + // Process format string + char *ptr = format; + while (*ptr) { + if (*ptr == '{') { + if (strncmp(ptr, "{bar}", 5) == 0) { + strcat(result, bar); + ptr += 5; + } else if (strncmp(ptr, "{percent}", 9) == 0) { + if (pb.showPercent) { + strcat(result, percent_str); + } + ptr += 9; + } else if (strncmp(ptr, "{count}", 7) == 0) { + if (pb.showCount) { + strcat(result, count_str); + } + ptr += 7; + } else { + strncat(result, ptr, 1); + ptr++; + } + } else { + strncat(result, ptr, 1); + ptr++; + } + } + + printf("%s", result); + + if (percent < 100) { + printf("\r"); + } else { + printf("\n"); + } +} diff --git a/samples-test/samples/C/progress-bar.h b/samples-test/samples/C/progress-bar.h new file mode 100644 index 0000000000000000000000000000000000000000..c4ec05c21e22713b8626f4947f207a30dad246ce --- /dev/null +++ b/samples-test/samples/C/progress-bar.h @@ -0,0 +1,24 @@ +#include +#include +#include + +typedef struct { + char symbol; + char startSymbol; + char endSymbol; + int length; + int progress; + int total; + char* format; + bool showPercent; + bool showCount; +} ProgressBar; + +ProgressBar init(char symbol, int length, int total); +ProgressBar update(ProgressBar *pb, int progress); +ProgressBar showPercent(ProgressBar *pb, bool show); +ProgressBar showCount(ProgressBar *pb, bool show); +ProgressBar setStartEndSymbols(ProgressBar *pb, char start, char end); +ProgressBar setCustomFormat(ProgressBar *pb, char* format); +ProgressBar tick(ProgressBar *pb); +void print(ProgressBar pb); diff --git a/samples-test/samples/CSS/style.css b/samples-test/samples/CSS/style.css new file mode 100644 index 0000000000000000000000000000000000000000..56723bdd1721a2b6c7e8e7ec1ab2706abee5fac8 --- /dev/null +++ b/samples-test/samples/CSS/style.css @@ -0,0 +1,51 @@ +/* Basic reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Body styles */ +body { + font-family: Arial, sans-serif; + line-height: 1.6; + background-color: #f4f4f4; +} + +/* Header styles */ +.header { + background-color: #333; + color: white; + padding: 1rem; + text-align: center; +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* Button styles */ +.button { + display: inline-block; + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; +} + +.button:hover { + background-color: #0056b3; +} + +/* Responsive design */ +@media screen and (max-width: 768px) { + .container { + padding: 10px; + } +} diff --git a/samples-test/samples/Common Lisp/cp.lisp b/samples-test/samples/Common Lisp/cp.lisp new file mode 100644 index 0000000000000000000000000000000000000000..2f2dac2e0a90d29e466ef40d46ed4de239e1ba6a --- /dev/null +++ b/samples-test/samples/Common Lisp/cp.lisp @@ -0,0 +1,12 @@ +;; Copy contents of one file to another + +(require 'uiop) + +(let* ((args (uiop:command-line-arguments)) + (in_file (first args)) + (out_file (second args))) + (with-open-file (in-stream in_file) + (with-open-file (out-stream out_file :direction :output :if-exists :supersede :if-does-not-exist :create) + (loop for line = (read-line in-stream nil) + while line + do (write-line line out-stream))))) diff --git a/samples-test/samples/Gleam/math.gleam b/samples-test/samples/Gleam/math.gleam new file mode 100644 index 0000000000000000000000000000000000000000..4ee84eb7bc46051ac0cf2f7069d2f852af716fd5 --- /dev/null +++ b/samples-test/samples/Gleam/math.gleam @@ -0,0 +1,82 @@ +//// Simple math demonstration on Gleam + +import gleam/io + +// Main function +pub fn main() { + // Test Fibonacci function + io.debug(fib(10)) + // Test add function + io.debug(add(first: 1, second: -999)) + // Test abs function + io.debug(abs(-1)) + io.debug(abs(91_837_524)) + // Test min and max functions + io.debug(min(140, 0)) + io.debug(min(0, 0)) + io.debug(max(-1, 0)) + // Test is_even function + io.debug(is_even(0)) + io.debug(is_even(10)) + // Test clamp function + io.debug(clamp(10, min: 11, max: 20)) +} + +/// Get Fibonacci for given number +pub fn fib(n: Int) { + case n { + _ if n < 0 -> panic as "n should not be negative!" + 0 -> 0 + 1 -> 1 + _ -> fib(n - 1) + fib(n - 2) + } +} + +/// Add one number to other +pub fn add(first num1: Int, second num2: Int) -> Int { + num1 + num2 +} + +/// Get absolute number of value +pub fn abs(x: Int) -> Int { + case x >= 0 { + True -> x + False -> x * -1 + } +} + +/// Return min value of given pair +pub fn min(a: Int, b: Int) -> Int { + case a < b { + True -> a + False -> b + } +} + +/// Return max value of given pair +pub fn max(a: Int, b: Int) -> Int { + case a > b { + True -> a + False -> b + } +} + +/// Returns true if given number is even and false if it's odd +pub fn is_even(x: Int) -> Bool { + x % 2 == 0 +} + +/// Restricts an int between a lower and upper bound. +pub fn clamp(x: Int, min min_bound: Int, max max_bound: Int) -> Int { + x + |> min(max_bound) + |> max(min_bound) +} + +fn todo_func() { + todo as "Not implemented yet" +} + +fn panic_func() { + panic as "VERY BAD ERROR!!!" +} diff --git a/samples-test/samples/Go/language_stats.go b/samples-test/samples/Go/language_stats.go new file mode 100644 index 0000000000000000000000000000000000000000..430a67e0d54c8ca3d225d063d137574b2431e063 --- /dev/null +++ b/samples-test/samples/Go/language_stats.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "sort" +) + +// Language represents a programming language +type Language struct { + // Name of the language + Name string + // Color associated with the language + Color string + // Rank of the language in popularity + Rank int + // Year the language was created + YearCreated int + // Features of the language + Features []string +} + +// LanguageStats represents a collection of programming languages +type LanguageStats struct { + // List of languages + languages []Language +} + +// AddLanguage adds a new language to the collection +func (ls *LanguageStats) AddLanguage(lang Language) { + ls.languages = append(ls.languages, lang) +} + +// SortByRank sorts the languages by rank +func (ls *LanguageStats) SortByRank() { + sort.Slice(ls.languages, func(i, j int) bool { + return ls.languages[i].Rank < ls.languages[j].Rank + }) +} + +func main() { + stats := LanguageStats{} + + // Add Go + stats.AddLanguage(Language{ + Name: "Go", + Color: "Blue", + Rank: 8, + YearCreated: 2009, + Features: []string{"Concurrent", "Compiled", "Static typing"}, + }) + + // Add Python + stats.AddLanguage(Language{ + Name: "Python", + Color: "Yellow and Blue", + Rank: 3, + YearCreated: 1991, + Features: []string{"Dynamic typing", "Interpreted", "Object-oriented"}, + }) + + // Add JavaScript + stats.AddLanguage(Language{ + Name: "JavaScript", + Color: "Yellow", + Rank: 1, + YearCreated: 1995, + Features: []string{"Dynamic typing", "Interpreted", "Prototype-based"}, + }) + + stats.SortByRank() + + fmt.Println("Programming Languages by Popularity Rank:") + fmt.Println("----------------------------------------") + + for _, lang := range stats.languages { + fmt.Printf("%s (Rank: %d)\n", lang.Name, lang.Rank) + fmt.Printf("Created in: %d\n", lang.YearCreated) + fmt.Printf("Features: %v\n", lang.Features) + fmt.Println("----------------------------------------") + } +} diff --git a/samples-test/samples/HCL/spec.hcl b/samples-test/samples/HCL/spec.hcl new file mode 100644 index 0000000000000000000000000000000000000000..816db5d1d15e182a20d5dcbf6b8c4081d30c0c19 --- /dev/null +++ b/samples-test/samples/HCL/spec.hcl @@ -0,0 +1,320 @@ +// MIT License +// +// Copyright (c) 2021 Christian Frichot +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// To cater for multiple spec versions we specify this in our HCL files +spec_version = "0.1.9" + +// You can include variables outside your threatmodel blocks + +variable "variable_name" { + value = "Variable text here" +} + +// To use this, simply swap in a text attribute for var.variable_name + +// There may be multiple threatmodel blocks in a single file, but their names must be unique + +threatmodel "threatmodel name" { + // the author attribute is required + author = "@xntrik" + + // the including attribute is optional + // you can inherit all the information from another threat model + // + // the included file must include only a single threatmodel block + // + // any duplicate fields will be overwritten by this threatmodel + // + // You can leverage go-getter's format for downloading too, this means that + // you can refer to remote URLs, or Git Repos etc + // See https://github.com/hashicorp/go-getter for examples + // + // There are some issues to select an individual file from a Git repo, as the + // entire repo is downloaded by default. To use an individual file, you can + // github.com/threatcl/threatcl|examples/aws-security-checklist.hcl + // with the | being a separator between the git repo and the file to use + // after cloning. + + including = "shared/city-threatmodel.hcl" + + // The description is optional + description = "A description of the system being assessed" + + // The link is optional + link = "https://link/to/docs" + + // The diagram_link is optional + // If it ends in .jpg or .png then it'll be embedded in the resultant dashboard + + // If a diagram_link isn't set, but the threatmodel includes a + // data_flow_diagram, this will be automatically generated and included + // when running threatcl dashboard + + diagram_link = "https://link/to/diagram" + + // created_at and updated_at are optional integer, UNIX time stamps + created_at = 1594033151 + updated_at = 1594033160 + + // the attributes block is optional, but recommended + + attributes { + new_initiative = "true" // boolean + internet_facing = "true" // boolean + + // initiative_size must be one of 'Undefined, Small, Medium, Large' + initiative_size = "Undefined" + } + + // you can set mutiple additional attribute key/value blocks as well + + additional_attribute "network_segment" { + value = "DMZ" + } + + // Each threatmodel may contain a number of information_assets + // the names must be unique per threatmodel though + + information_asset "cred store" { + // The description is optional + description = "This is where creds are stored" + + // information_classification must be one of 'Restricted, Confidential, Public' + information_classification = "Confidential" + + // source is optional, and can be used to specify if this asset was sourced + // from an external resource, such as terraform + source = "terraform" + } + + information_asset "special sauce" { + // Here is how you can refer to your variables set above + description = var.variable_name + information_classification = "Confidential" + } + + // Each threatmodel may contain a number of usecases + + usecase { + // The description is required + // Similar to threats, the description may also use multiline entries too + description = "Users access data from the system" + } + + // Each threatmodel may contain a number of exclusions + + exclusion { + // The description is required + // Similar to threats, the description may also use multiline entries too + description = "Crypto operations are offloaded to a KMS" + } + + // Each threatmodel may contain a number of third party dependencies + + third_party_dependency "dependency name" { + // The description is required, and may use multiline entries + description = "What the dependency is used for" + + // The following boolean attributes are optional and will default to false if unset + saas = "true" + paying_customer = "true" + open_source = "false" + infrastructure = "false" + + // The uptime dependency is required, and must be one of "none", "degraded", "hard", "operational" + // This specifies the impact to our system if the dependency is unavailable + uptime_dependency = "none" + + // Uptime notes are optional + uptime_notes = "If this dependency goes down users can't login" + } + + // Each threatmodel may contain a number of threats + + threat { + // The description is required + description = "System is compromised by hackers" + + // The impact is an optional array of potential impact values + // The available values are 'Confidentiality, Integrity, Availability' + impacts = ["Confidentiality", "Integrity", "Availability"] + + // A threat may contain multiple expanded_control blocks + // These blocks will be replacing the older "control" string or + // "proposed_control" blocks + + expanded_control "control name" { + description = "The control must have a description" + + // implemented is optional, but defaults to false + implemented = true + + // implementation_notes are optional + implementation_notes = "This string is optional" + + // risk_reduction, while optional, is recommended + // this value takes an integer + risk_reduction = 50 + + // a control may contain optional attribute blocks + attribute "Attribute Name" { + value = "This value string must be set though for each attribute" + } + + // a good use for these may be to refer to OWASP URLs + attribute "OWASP Proactive Control" { + value = "" + } + } + + // WARNING: The "control" string value is going to be deprecated in + // favor of expanded_control block! + + // The control is optional, and allows the author to capture controls + // or circumstances that may reduce the likelihood of impact of the threat + // control = "We require 2FA for access" + + // The stride is an optional array of STRIDE elements that apply to this threat + // The available values are: + // Spoofing + // Tampering + // Repudiation + // Info Disclosure + // Denial Of Service + // Elevation Of Privilege + stride = ["Spoofing", "Tampering", "Repudiation", "Info Disclosure", "Denial Of Service", "Elevation Of Privilege"] + + // The information_asset_refs are an optional array of information_assets + // the elements must much existing information_assets - as above + information_asset_refs = ["cred store"] + + // WARNING: The "proposed_control" blocks are going to be deprecated in + // favor of expanded_control blocks! + + // The proposed_control blocks are optional, and are used to track + // proposed controls + // proposed_control { + // The Description is required + // description = "This is a proposed control" + + // The implemented boolean is optional, and defaults to false + // implemented = true + // } + } // End of threat block + + // You can import an external .hcl file that includes control descriptions + // Remember to do this at the threatmodel block level + // Similar to the "including" block, this can be a remote file too + + // An example of what may be in controls.hcl: + // + // spec_version = "0.1.9" + // component "control" "control_name" { + // description = "A control that can be used in multiple places" + // } + + imports = ["controls.hcl"] + + threat { + + // To reference the above component + expanded_control "Control Name" { + description = import.control.control_name.description + risk_reduction = 50 + } + + description = < + + + + + Centered Button and Label + + + +
+ Hello World! + + Welcome to our site! + + Contact Us + + Newsletter + + Our Products + + Special Offers + + Customer Support + + About Us + + Careers + + Blog Posts + + FAQ Section + + Privacy Policy + + Terms of Service + + Social Media + + Testimonials + + Events + + Downloads + + Partners + +
+ + \ No newline at end of file diff --git a/samples-test/samples/INI/.editorconfig b/samples-test/samples/INI/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..64b2b330ceb4347d90d4b34c64dd6fba7ad92268 --- /dev/null +++ b/samples-test/samples/INI/.editorconfig @@ -0,0 +1,43 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[.all-contributorsrc] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[*.{mmd,mermaid}] +indent_style = space +indent_size = 4 + +[*.rb] +indent_style = space +indent_size = 2 + +[*.rs] +indent_style = space +indent_size = 4 + +[*.sh] +indent_style = tab +indent_size = 8 + +[*.toml] +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 + +[Dockerfile] +indent_style = space +indent_size = 4 diff --git a/samples-test/samples/Ignore List/.dockerignore b/samples-test/samples/Ignore List/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..b955a69fae157f15350fdde5b8362f876a67be95 --- /dev/null +++ b/samples-test/samples/Ignore List/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +dist +build +*.exe +*.dll +*.so +*.dylib +.idea/ +.vscode/ +*.log +*.sql +*.sqlite \ No newline at end of file diff --git a/samples-test/samples/Ignore List/.gitignore b/samples-test/samples/Ignore List/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..12865a1273909ef3521dad1d1af20bb46739aaca --- /dev/null +++ b/samples-test/samples/Ignore List/.gitignore @@ -0,0 +1,3 @@ +debug/ +target/ +*.pdb \ No newline at end of file diff --git a/samples-test/samples/Ignore List/.npmignore b/samples-test/samples/Ignore List/.npmignore new file mode 100644 index 0000000000000000000000000000000000000000..520ee38dc9e755f8cfdd1c5cb91119e4149208a3 --- /dev/null +++ b/samples-test/samples/Ignore List/.npmignore @@ -0,0 +1,5 @@ +LICENSE +README.md +docs +*.html +*.test.js \ No newline at end of file diff --git a/samples-test/samples/Ignore List/.vercelignore b/samples-test/samples/Ignore List/.vercelignore new file mode 100644 index 0000000000000000000000000000000000000000..f410b905b49e08e26d6f150808802d1fe6fa6a01 --- /dev/null +++ b/samples-test/samples/Ignore List/.vercelignore @@ -0,0 +1,13 @@ +.env +.env.local +node_modules +README.md +.gitignore +.vscode +.idea +tests +*.test.js +*.spec.js +coverage +dist +build \ No newline at end of file diff --git a/samples-test/samples/JSON with Comments/devcontainer.json b/samples-test/samples/JSON with Comments/devcontainer.json new file mode 100644 index 0000000000000000000000000000000000000000..d160b250616ab406f34c8d4868429394105a6df5 --- /dev/null +++ b/samples-test/samples/JSON with Comments/devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "Development Container", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": {}, + "customizations": { + "vscode": { + "extensions": [] + } + }, + "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created + "postCreateCommand": "echo 'Container is ready!'", + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/samples-test/samples/JSON with Comments/tsconfig.json b/samples-test/samples/JSON with Comments/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..3527c316e8c2eb0435d44d574629433c0065d87b --- /dev/null +++ b/samples-test/samples/JSON with Comments/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/samples-test/samples/JSON/data.json b/samples-test/samples/JSON/data.json new file mode 100644 index 0000000000000000000000000000000000000000..ca8ca58b60b70d4a417109781c4abd8124e01a94 --- /dev/null +++ b/samples-test/samples/JSON/data.json @@ -0,0 +1,16 @@ +{ + "type": "user", + "name": "John Doe", + "age": 25, + "job": "Software Developer", + "projects": [ + { + "name": "Project 1", + "description": "This is project 1" + }, + { + "name": "Project 2", + "description": "This is project 2" + } + ] +} \ No newline at end of file diff --git a/samples-test/samples/JavaScript/button.jsx b/samples-test/samples/JavaScript/button.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5e9313946f4defb3cbbf8ddee56d064769de3c6d --- /dev/null +++ b/samples-test/samples/JavaScript/button.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +const Button = ({ onClick, children, disabled = false, className = '' }) => { + return ( + + ); +}; + +export default Button; \ No newline at end of file diff --git a/samples-test/samples/JavaScript/count_expression.js b/samples-test/samples/JavaScript/count_expression.js new file mode 100644 index 0000000000000000000000000000000000000000..a8a34c505bc591fd21c67459d5cbee1dee096934 --- /dev/null +++ b/samples-test/samples/JavaScript/count_expression.js @@ -0,0 +1,24 @@ +/** + * Parses and evaluates a mathematical expression string + * @param {string} expression - The mathematical expression to evaluate + * @returns {number} The result of the expression + */ +function parseExpression(expression) { + try { + // Remove whitespace and validate input + const cleanExpression = expression.replace(/\s+/g, ''); + + // Check for invalid characters + if (/[^0-9+\-*/().]/.test(cleanExpression)) { + throw new Error('Invalid characters in expression'); + } + + // Use Function constructor to safely evaluate the expression + // This handles basic arithmetic operations: +, -, *, / + return Function(`return ${cleanExpression}`)(); + } catch (error) { + throw new Error('Invalid expression: ' + error.message); + } +} + +module.exports = parseExpression; \ No newline at end of file diff --git a/samples-test/samples/Jule/generic.jule b/samples-test/samples/Jule/generic.jule new file mode 100644 index 0000000000000000000000000000000000000000..1affbcf936d7a1bf669726862d10061b0b8183a6 --- /dev/null +++ b/samples-test/samples/Jule/generic.jule @@ -0,0 +1,42 @@ +// This work is marked with CC0 1.0 Universal +// https://creativecommons.org/publicdomain/zero/1.0 + +use "std/math/rand" +use "std/time" + +fn Max[S: []E, E: ordered](mut s: S): E { + if len(s) == 0 { + panic("Max: empty slice") + } + mut m := s[0] + for _, e in s[1:] { + if m < e { + m = e + } + } + ret m +} + +fn Min[S: []E, E: ordered](mut s: S): E { + if len(s) == 0 { + panic("Min: empty slice") + } + mut m := s[0] + for _, e in s[1:] { + if m > e { + m = e + } + } + ret m +} + +fn main() { + rand := rand::Rand.New(rand::NewSource(u64(time::Now().Nanosecond()))) + mut s := make([]int, 10) + for i in s { + s[i] = rand.Intn(100) + } + println(s) + println(Max(s)) + println(Min(s)) +} \ No newline at end of file diff --git a/samples-test/samples/Jupyter Notebook/simple.ipynb b/samples-test/samples/Jupyter Notebook/simple.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..79edb249ae5119459c22a048bfe859a73ef5fbcc --- /dev/null +++ b/samples-test/samples/Jupyter Notebook/simple.ipynb @@ -0,0 +1,34 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Simple factorial function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def factorial(n):\n", + " if n == 0:\n", + " return 1\n", + " else:\n", + " return n * factorial(n-1)\n", + "\n", + "# Example usage\n", + "print(factorial(5)) # Output: 120" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/samples-test/samples/Makefile/Makefile b/samples-test/samples/Makefile/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..7f733b141f671f46255591475704d05fc232bf50 --- /dev/null +++ b/samples-test/samples/Makefile/Makefile @@ -0,0 +1,30 @@ +# Compiler settings +CC = gcc +CFLAGS = -Wall -Wextra -std=c11 + +# Directories +SRC_DIR = src +OBJ_DIR = obj +BIN_DIR = bin + +# Files +SRCS = $(wildcard $(SRC_DIR)/*.c) +OBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o) +TARGET = $(BIN_DIR)/program + +# Main targets +all: $(TARGET) + +$(TARGET): $(OBJS) + @mkdir -p $(BIN_DIR) + $(CC) $(OBJS) -o $(TARGET) + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c + @mkdir -p $(OBJ_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# Cleaning +clean: + rm -rf $(OBJ_DIR) $(BIN_DIR) + +.PHONY: all clean \ No newline at end of file diff --git a/samples-test/samples/Markdown/sample.md b/samples-test/samples/Markdown/sample.md new file mode 100644 index 0000000000000000000000000000000000000000..787d4f9867217982fe1625b9654b3fa608f3fac5 --- /dev/null +++ b/samples-test/samples/Markdown/sample.md @@ -0,0 +1,34 @@ +# Title + +This is the main description of the document. + +## What is a level 2 header? + +Here is a section underneath the main body of the document. + +## What is some styling? + +- You can +- write +- lists + +1. Lists can +2. be +3. ordered + +You can _emphasize_ or make text **strong** or make it +`inline code`. + +A strikethrough is ~~sometimes~~ supported. + +--- + +| tables | status | +| :----: | :----: | +| ??? | yes | + +```typescript +declare const codeBlock: boolean; + +if (codeBlock) console.log('Code blocks are supported.'); +``` diff --git a/samples-test/samples/MoonBit/expr.mbt b/samples-test/samples/MoonBit/expr.mbt new file mode 100644 index 0000000000000000000000000000000000000000..bb986390eba1b76087447c21c9b86afe47be9084 --- /dev/null +++ b/samples-test/samples/MoonBit/expr.mbt @@ -0,0 +1,15 @@ +// Define a recursive enum for expressions +enum Expr { Num(Int); Mul(Expr, Expr) } + +// Evaluate expression with pattern matching +fn eval(expr : Expr) -> Int { + match expr { + Num(n) => n + Mul(a, b) => eval(a) * eval(b) + } +} + +fn main { + let expr = Mul(Mul(Num(2), Num(3)), Num(4)) + println(eval(expr)) // 24 +} \ No newline at end of file diff --git a/samples-test/samples/MoonBit/pkg.mbti b/samples-test/samples/MoonBit/pkg.mbti new file mode 100644 index 0000000000000000000000000000000000000000..001f435f33031445867292a5d19ccae07c5e2f73 --- /dev/null +++ b/samples-test/samples/MoonBit/pkg.mbti @@ -0,0 +1,10 @@ +package "moonbitlang/core/abort" + +// Values +fn[T] abort(String) -> T + +// Types and methods + +// Type aliases + +// Traits diff --git a/samples-test/samples/MoonBit/script.mbtx b/samples-test/samples/MoonBit/script.mbtx new file mode 100644 index 0000000000000000000000000000000000000000..e361b7a6bbcbf7200a8dbd1b9dc02d49cf843b13 --- /dev/null +++ b/samples-test/samples/MoonBit/script.mbtx @@ -0,0 +1,10 @@ +///| +import { + "moonbitlang/core/list" @ls +} + +///| +async fn main { + let xs = @ls.from_array(["1", "2", "3"]) + debug(xs) +} \ No newline at end of file diff --git a/samples-test/samples/OpenSCAD/sFeeder.scad b/samples-test/samples/OpenSCAD/sFeeder.scad new file mode 100644 index 0000000000000000000000000000000000000000..e02513bb988500f855d68e4244d709f10e2225de --- /dev/null +++ b/samples-test/samples/OpenSCAD/sFeeder.scad @@ -0,0 +1,272 @@ +/* +Author: Michael + +2017/10/28 + +*/ + +/* [basic parameters] */ + +//How wide is the tape? +tapeWidth=8; // [8, 12, 16, 24, 36, 48, 72] +//Optimize for Plastic or Paper tape? Plastic tapes fit in Paper, but not reverse. Plastic tapes may be too loose in paper feeder. +tapeHeight=0.9; // [0.9:paper,0.3:plastic] +//How many feeder to print ganged +numberOfFeeders=2; // [1:1:20] +//Overall length of feeder? +feederLength=180; +//Height of tape's bottom side above bed +tapeLayerHeight=22.8; +//Bank ID: To identify the feeder in OpenPnP unique IDs for each bank are built and embossed into the ganged feeder. -1: no identifier. +bankID=1; //[-1:1:9] +//Change the starting letter according to ASCII code: 65 -> 'A', 75 -> 'K', 85 -> 'U' and so on. +startingLetter=65; //[65:1:90] +//diameter of pockets on the side to put a magnet in +magnetDiameter=6; +//height of the pocket for the magnet +magnetHeight=3; +//How many magnet pockets in the right side? +magnetPocketsRightSide=3; +//How many magnet pockets on the left side? +magnetPocketsLeftSide=4; + +/* [advanced] */ + +//controls the force to keep the tape in place. lower value higher force and though friction +tapeClearance=-0.3; // [-0.5:0.05:0.5] +bodyHeight=6; +tapeSupportHoleSide=2.8; +tapeSupportNonHoleSide=0.9; + +//Separation from the magnet pocket to the edge, right side +separationRightSide=24.5; +//Separation from the magnet pocket to the edge, left side +separationLeftSide=7.5; + +// Change this value to get a thicker space under the magnet +layerBelow=0.35; + +/* [expert] */ +//higher values make the left arm stronger +additionalWidth=3; +topFinishingLayer=0.3; +tapeGuideUpperOverhang=0.4; +//lower values make the spring smaller thus less force on tape, should be slightly less than a multiple of extrusion-width +springWidth=1.3; +springSkew=1.2; +//if two tapeloaded lanes touch each other raise this value a little +springClearance=0.4; + +overallWidth=tapeWidth+additionalWidth; +overallHeight=tapeLayerHeight+tapeHeight+tapeGuideUpperOverhang+topFinishingLayer; +tapeXcenter=(overallWidth/2)+tapeClearance/2; + + + +//make the feeders +gang_feeder(); + + +// customizer specific stuff +// preview[view:north west, tilt:top diagonal] + + +module gang_feeder() { + + difference() { + union() { + + + //stack up feeders + for(i=[0:1:numberOfFeeders-1]) { + translate([i*(tapeWidth+additionalWidth),0,0]) + feeder_body(i); + } + + + } + + //magnet pockets + //echo((feederLength-2*separationRightSide)/(magnetPocketsRightSide-1)) + for(j=[0:1:magnetPocketsRightSide-1]) { + //magnet pockets right side + translate([0,j*((feederLength-2*separationRightSide)/(magnetPocketsRightSide-1))+separationRightSide,0]) { + translate([(numberOfFeeders)*(tapeWidth+additionalWidth),0,0 ]) { + //magnetic_fixation_pocket(0,feederLength/2); + magnetic_fixation_pocket(); + } + } + + } + + //echo((feederLength-2*separationLeftSide)/(magnetPocketsLeftSide-1)) + for(j=[0:1:magnetPocketsLeftSide-1]) { + //magnet pockets left side + translate([0,j*((feederLength-2*separationLeftSide)/(magnetPocketsLeftSide-1))+separationLeftSide,0]) { + rotate([0,0,180]) + //magnetic_fixation_pocket(0,feederLength/2); + magnetic_fixation_pocket(); + } + } + } +} + +module feeder_cover() { + cube([2,feederLength,overallHeight]); +} + +module feeder_body(feederNo) { + translate([0,feederLength,0]) { + rotate([90,0,0]) { + difference() { + + //main form + linear_extrude(feederLength) { + polygon(points=[ + //base + [0,0], + [overallWidth,0], + + //right arm way up ("spring", outer part) + [overallWidth,bodyHeight*0.8], + [overallWidth-springSkew,tapeLayerHeight-3], + [overallWidth-springClearance,tapeLayerHeight], + + //right arm tape guide + [overallWidth-springClearance,overallHeight], + [tapeXcenter+tapeWidth/2+tapeClearance-tapeGuideUpperOverhang,overallHeight], + [tapeXcenter+tapeWidth/2+tapeClearance-tapeGuideUpperOverhang,tapeLayerHeight+tapeHeight+tapeGuideUpperOverhang], + [tapeXcenter+tapeWidth/2+tapeClearance,tapeLayerHeight+tapeHeight], + [tapeXcenter+tapeWidth/2+tapeClearance,tapeLayerHeight], + [tapeXcenter+tapeWidth/2+tapeClearance-tapeSupportHoleSide,tapeLayerHeight], + [tapeXcenter+tapeWidth/2+tapeClearance-tapeSupportHoleSide,tapeLayerHeight-0.6], + + //right arm way down ("spring", inner part) + [overallWidth-springSkew-springWidth,tapeLayerHeight-3], + [overallWidth-springWidth,bodyHeight*0.8], + [overallWidth-springWidth-1,bodyHeight], + + //base (inner part) + [tapeXcenter-tapeWidth/2+tapeSupportNonHoleSide,bodyHeight], + + //left arm up (inner part) + [tapeXcenter-tapeWidth/2+tapeSupportNonHoleSide,tapeLayerHeight], + + //left arm tape guide + [tapeXcenter-tapeWidth/2,tapeLayerHeight], + [tapeXcenter-tapeWidth/2,tapeLayerHeight+tapeHeight], + [tapeXcenter-tapeWidth/2+tapeGuideUpperOverhang,tapeLayerHeight+tapeHeight+tapeGuideUpperOverhang], + [tapeXcenter-tapeWidth/2+tapeGuideUpperOverhang,overallHeight], + + //left arm down (outer part) + [0,overallHeight], + [0,0] + + ]); + } + + //direction of travel while picking with OpenPnP + if(feederLength>=100) { + for (i=[0:1:3]) { + translate([additionalWidth+3/2+0.5,bodyHeight+0.1,feederLength-25-(i*6)]) + rotate([90,90,0]) + linear_extrude(1) + circle(3,$fn=3); + } + } + + //4 identification marks + translate([additionalWidth,bodyHeight-0.9,feederLength-2]) + rotate([90,90,180]) + identification_mark(feederNo,"left","top"); + + translate([additionalWidth,bodyHeight-0.9,2]) + rotate([90,90,180]) + identification_mark(feederNo,"right","top"); + + translate([tapeXcenter,bodyHeight-0.9,feederLength-0.9]) + rotate([0,0,0]) + identification_mark(feederNo,"center","top"); + + translate([tapeXcenter,bodyHeight-0.9,0.9]) + rotate([0,180,0]) + identification_mark(feederNo,"center","top"); + + //reference hole + translate([tapeXcenter+tapeWidth/2+tapeClearance-1.75,tapeLayerHeight,feederLength-4]) + rotate([90,90,0]) + cylinder(h=0.6,d=1.4,center=false,$fn=20); + + //3 registration points (for magnets, bolts or to screw from top) + //bottom_fixation(feederLength/2); + bottom_fixation(17); + bottom_fixation(feederLength-17); + + + } + } + } +} + +module identification_mark(feederNo,_halign,_valign) { + + if(bankID!=-1) { + linear_extrude(height=.91) { + text( str(bankID, chr(feederNo+startingLetter) ),font=":style=Bold", size=4, valign=_valign, halign=_halign); + } + } + +} + +module magnetic_fixation_pocket() { + // change 'layerBelow' to get a thicker space under the magnet, 2 layers is ok, check your slicer's settings and preview!. Default: layerBelow=0.25; + magnetInset=1; + magnetDiameterOversizedFor3dPrinting=magnetDiameter+0.2; + + translate([0,0,layerBelow]) { + union() { + translate([-(magnetDiameterOversizedFor3dPrinting)/2-magnetInset,0,0]) + cylinder(d=magnetDiameterOversizedFor3dPrinting,h=magnetHeight+0.3,$fn=20); + + hull() { + translate([-(magnetDiameterOversizedFor3dPrinting)/2-magnetInset,0,0]) + cylinder(d=magnetDiameterOversizedFor3dPrinting-1.4,h=magnetHeight+0.3,$fn=20); + translate([0,0,(magnetHeight+0.3)/2]) + cube([0.1,magnetDiameterOversizedFor3dPrinting+0.4,magnetHeight+0.3],center=true); + } + + translate([-(magnetDiameterOversizedFor3dPrinting)/2-magnetInset,0,0]) { + difference() { + cylinder(d=magnetDiameterOversizedFor3dPrinting+3,h=magnetHeight+0.3,$fn=20); + cylinder(d=magnetDiameterOversizedFor3dPrinting+2,h=magnetHeight+0.3,$fn=20); + } + } + } + translate([-(magnetDiameterOversizedFor3dPrinting-magnetInset+1)/2-magnetInset,0,0]) + cube([magnetDiameterOversizedFor3dPrinting,1,1],center=true); + } +} + +module bottom_fixation(pos_y) { + layerForBridging=0.3; + cutoutbelow=3.5; + union() { + translate([tapeXcenter,bodyHeight-1,pos_y]) + rotate([-90,0,0]) + cylinder(h = 2.1, r=6.0/2, $fn=20); + + translate([tapeXcenter,-0.1,pos_y]) + rotate([-90,0,0]) + cylinder(h = bodyHeight+1, r=3.5/2, $fn=20); + + //old pocket below feeder for magnet + *translate([tapeXcenter,cutoutbelow,pos_y]) + rotate([90,0,0]) + cylinder(h = 10, r=6.0/2, $fn=20); + + //chamfer + *translate([tapeXcenter,0.3,pos_y]) + rotate([90,0,0]) + cylinder(h = 0.3, r1=6.0/2, r2=6.3/2, $fn=20); + } +} diff --git a/samples-test/samples/Org/org.org b/samples-test/samples/Org/org.org new file mode 100644 index 0000000000000000000000000000000000000000..7074b2ccd5e52c3ae26da5fdc48083e40e83d8ed --- /dev/null +++ b/samples-test/samples/Org/org.org @@ -0,0 +1,32 @@ +* Org +** Text + +Paragraphs are blocks of text. They are separated by a new line. + +See? This is a new paragraph. + +Text can by styled. /Italicize/ with a slash or *bold* with asterisks, +or mark something as ~code~ with tildes. + +----- + +Oh, and you can separate with 5 hyphens. + +** Lists + ++ unordered ++ with ++ plus symbol + +1. order +2. with +3. numbers + +----- + +Go to [[https://example.com][a link]]! + +#+begin_quote +The syntax for blocks is too weird to describe in text. Just read this file's source +code. +#+end_quote diff --git a/samples-test/samples/PowerShell/check_for_updates.ps1 b/samples-test/samples/PowerShell/check_for_updates.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..0dfddf72666835d2e686bea71c8fd9fb55830def --- /dev/null +++ b/samples-test/samples/PowerShell/check_for_updates.ps1 @@ -0,0 +1,23 @@ +# Define the current version of your program +$currentVersion = "1.0.0" + +# Define the URL where version information is stored (replace with your actual URL) +$versionCheckUrl = "https://example.com/version.txt" + +try { + # Get the latest version from remote source + $latestVersion = Invoke-WebRequest -Uri $versionCheckUrl -UseBasicParsing | Select-Object -ExpandProperty Content + + # Compare versions + if ($latestVersion -gt $currentVersion) { + Write-Host "Update available! Current version: $currentVersion, Latest version: $latestVersion" + Write-Host "Please download the latest version from our website." + } + else { + Write-Host "Your program is up to date. Version: $currentVersion" + } +} +catch { + Write-Host "Unable to check for updates. Please check your internet connection." + Write-Host "Error: $($_.Exception.Message)" +} diff --git a/samples-test/samples/Python/minecraft.py b/samples-test/samples/Python/minecraft.py new file mode 100644 index 0000000000000000000000000000000000000000..52724fd94883036ece2c4fb6223c0a3963a48d23 --- /dev/null +++ b/samples-test/samples/Python/minecraft.py @@ -0,0 +1,32 @@ +from uuid import uuid4 +from unittest import TestCase + +class Item: + def __init__(self): + self.id = uuid4() + +class Charcoal(Item): + pass + +class Stick(Item): + def __add__(self, other): + return Torch(self, other) + +class Torch(Item): + def __init__(self, stick: Stick, charcoal: Charcoal): + super().__init__() + self.stick = stick + self.charcoal = charcoal + + def components(self): + return [self.stick, self.charcoal] + +class TestMinecraft(TestCase): + def test_torch(self): + stick = Stick() + charcoal = Charcoal() + torch = stick + charcoal + components = torch.components() + + self.assertIn(charcoal, components) + self.assertIn(stick, components) diff --git a/samples-test/samples/Ruby/repofetch.rb b/samples-test/samples/Ruby/repofetch.rb new file mode 100644 index 0000000000000000000000000000000000000000..dcbb316a9a4c744cf824019361b1cfb3e96cb594 --- /dev/null +++ b/samples-test/samples/Ruby/repofetch.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'action_view' +require 'git' +require 'repofetch/config' +require 'repofetch/env' +require 'repofetch/exceptions' +require 'repofetch/plugin' +require 'repofetch/stat' +require 'repofetch/timespan_stat' +require 'repofetch/theme' +require 'repofetch/util' +require 'repofetch/version' + +# Main class for repofetch +class Repofetch + MAX_ASCII_WIDTH = 40 + MAX_ASCII_HEIGHT = 20 + DEFAULT_THEME = Theme.new.freeze + @plugins = [] + + class << self + attr_accessor :config + attr_reader :plugins + end + + # Loads the config, without affecting the file system. + def self.load_config + @config = Config.load + end + + # Loads the config, writing a default config if it doesn't exist. + def self.load_config! + @config = Config.load! + end + + # Registers a plugin. + # + # @param [Plugin] plugin The plugin to register + def self.register_plugin(plugin) + @plugins << plugin + end + + # Replaces an existing plugin. If the existing plugin does not exist, + # then it registers the plugin instead. + # + # @param [Plugin] old The plugin to be replaced + # @param [Plugin] new The new plugin + def self.replace_or_register_plugin(old, new) + index = @plugins.find_index(old) + if index.nil? + register_plugin(new) + else + @plugins[index] = new + @plugins + end + end + + # Returns the plugin that should be used. + # Raises a +Repofetch::NoPluginsError+ if no plugins are found. + # Raises a +Repofetch::TooManyPluginsError+ if more than one plugin is found. + # + # @param [String] path The path to check. + # @param [Array] args The arguments passed to the program. + # + # @raise [NoPluginsError] If no plugins were selected. + # @raise [TooManyPluginsError] If more than one plugin was selected. + # + # @return [Plugin] A plugin to use. + def self.get_plugin(path, args) + path_plugin = get_plugin_for_path(path) + repo_plugin = get_plugin_for_repo(path) + + raise TooManyPluginsError if path_plugin && repo_plugin + + raise NoPluginsError if path_plugin.nil? && repo_plugin.nil? + + return path_plugin.from_path(path, args) unless path_plugin.nil? + + git = Git.open(path) + repo_plugin.from_git(git, args) + end + + # Gets the plugins that matches the given path. + # + # @param [String] path The path to check. + # + # @return [Array] The plugins that match the path. + def self.get_plugins_for_path(path) + @plugins.filter { |plugin_class| plugin_class.matches_path?(path) } + end + + # Gets a single plugin that matches the given path. + # + # @param [String] path The path to check. + # + # @return [Plugin, nil] The plugin that matches the path. + def self.get_plugin_for_path(path) + plugins = get_plugins_for_path(path) + + raise TooManyPluginsError if plugins.length > 1 + + plugins[0] + end + + # Gets the plugins that matches the given repository. + # + # @param [String] path The repository to check. + # + # @return [Array] The plugins that match the repository. + def self.get_plugins_for_repo(path) + begin + git = Git.open(path) + rescue ArgumentError + return [] + end + + @plugins.filter { |plugin_class| plugin_class.matches_repo?(git) } + end + + # Gets a single plugin that matches the given repository. + # + # @param [String] path The repository to check. + # + # @return [Plugin, nil] The plugin that matches the repository. + def self.get_plugin_for_repo(path) + plugins = get_plugins_for_repo(path) + + raise TooManyPluginsError if plugins.length > 1 + + plugins[0] + end + + def self.clear_plugins + @plugins = [] + end + private_class_method :clear_plugins +end diff --git a/samples-test/samples/Rust/error.rs b/samples-test/samples/Rust/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..26eb845325ee15f7619fc580118ae0b4a1550fa1 --- /dev/null +++ b/samples-test/samples/Rust/error.rs @@ -0,0 +1,64 @@ +use std::error::Error as ErrorTrait; +use std::fmt; + +macro_rules! error_kind { + ($($name:ident, $message:literal),*) => { + /// The kind of error that occurred. + #[derive(Debug)] + #[non_exhaustive] + pub enum ErrorKind { + $( + $name, + )* + } + + impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + Self::$name => write!(f, $message), + )* + } + } + } + }; +} + +error_kind!(NoRepository, "no repository found"); + +impl ErrorTrait for ErrorKind {} + +#[derive(Debug)] +pub struct Error { + kind: ErrorKind, + source: Option>, +} + +impl Error { + pub fn new(kind: ErrorKind) -> Self { + Self { kind, source: None } + } + + pub fn with_source(kind: ErrorKind, source: impl ErrorTrait + 'static) -> Self { + Self { + kind, + source: Some(Box::new(source)), + } + } + + pub fn kind(&self) -> &ErrorKind { + &self.kind + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.kind) + } +} + +impl ErrorTrait for Error { + fn source(&self) -> Option<&(dyn ErrorTrait + 'static)> { + self.source.as_ref().map(|s| s.as_ref()) + } +} diff --git a/samples-test/samples/SQL/job_info.sql b/samples-test/samples/SQL/job_info.sql new file mode 100644 index 0000000000000000000000000000000000000000..a7eb4ce858950c8ea9d6ae3ff6017f2ab5839cc4 --- /dev/null +++ b/samples-test/samples/SQL/job_info.sql @@ -0,0 +1,6 @@ +SELECT employee.id + , employee.name + , job.title AS job_title + FROM employee + LEFT JOIN job ON employee.id = job.employee_id + WHERE employee.id = @id diff --git a/samples-test/samples/SVG/shapes.svg b/samples-test/samples/SVG/shapes.svg new file mode 100644 index 0000000000000000000000000000000000000000..49ae06059b06281189268e6943f01670ab7788fb --- /dev/null +++ b/samples-test/samples/SVG/shapes.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/samples-test/samples/Scheme/example.scm b/samples-test/samples/Scheme/example.scm new file mode 100644 index 0000000000000000000000000000000000000000..5fb8bf963df71c23edcace5e81dadf59db2c24f0 --- /dev/null +++ b/samples-test/samples/Scheme/example.scm @@ -0,0 +1,19 @@ +; Provides basic mathematical utilities + +; Gets n! +(define (factorial n) + (if (<= n 1) + 1 + (* n (factorial (- n 1))))) + +; Gets the nth element of the fibonacci sequence +(define (fibonacci n) + (cond ((< n 0) (error "n cannot be negative")) + ((<= n 1) n) + (else (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))) + +; Reverses a list +(define (reverse-list l) + (if (null? l) + '() + (append (reverse-list (cdr l)) (list (car l))))) diff --git a/samples-test/samples/Shell/rename.bash b/samples-test/samples/Shell/rename.bash new file mode 100644 index 0000000000000000000000000000000000000000..ff6fef60dcc8558f3283fbaf74270905185f1d5c --- /dev/null +++ b/samples-test/samples/Shell/rename.bash @@ -0,0 +1,22 @@ +#!/bin/bash + +# Check if correct number of arguments is provided +if [ $# -ne 2 ]; then + echo "Usage: $0 old_filename new_filename" + exit 1 +fi + +# Check if the source file exists +if [ ! -f "$1" ]; then + echo "Error: File '$1' does not exist" + exit 1 +fi + +# Rename the file +mv "$1" "$2" + +if [ $? -eq 0 ]; then + echo "File successfully renamed from '$1' to '$2'" +else + echo "Error: Failed to rename the file" +fi diff --git a/samples-test/samples/Shell/rename.sh b/samples-test/samples/Shell/rename.sh new file mode 100644 index 0000000000000000000000000000000000000000..db84e3511edf9a2aaf9efad363762125317ddc78 --- /dev/null +++ b/samples-test/samples/Shell/rename.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# Check if correct number of arguments is provided +if [ $# -ne 2 ]; then + echo "Usage: $0 old_filename new_filename" + exit 1 +fi + +# Check if the source file exists +if [ ! -f "$1" ]; then + echo "Error: File '$1' does not exist" + exit 1 +fi + +# Rename the file +mv "$1" "$2" + +if [ $? -eq 0 ]; then + echo "File successfully renamed from '$1' to '$2'" +else + echo "Error: Failed to rename the file" +fi diff --git a/samples-test/samples/Shell/rename.zsh b/samples-test/samples/Shell/rename.zsh new file mode 100644 index 0000000000000000000000000000000000000000..4483b9da1cf8bd02f17b2b5ce5ded7062c4d8596 --- /dev/null +++ b/samples-test/samples/Shell/rename.zsh @@ -0,0 +1,22 @@ +#!/bin/zsh + +# Check if correct number of arguments is provided +if (( $# != 2 )); then + print "Usage: $0 old_filename new_filename" + exit 1 +fi + +# Check if the source file exists +if [[ ! -f $1 ]]; then + print "Error: File '$1' does not exist" + exit 1 +fi + +# Rename the file +mv $1 $2 + +if (( $? == 0 )); then + print "File successfully renamed from '$1' to '$2'" +else + print "Error: Failed to rename the file" +fi diff --git a/samples-test/samples/TOML/config.toml b/samples-test/samples/TOML/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..b2f0953e0e9f2ef93d4a6341f5b2e2dadb5cfc7c --- /dev/null +++ b/samples-test/samples/TOML/config.toml @@ -0,0 +1,13 @@ +# App's settings +[app] +# App's name +name = "My App" +# App's version +version = "1.0.0" +# App's description +description = "My App's description" +# App's license +license = "MIT" +# App's repository +repository = "https://github.com/ghost/example.git" + diff --git a/samples-test/samples/TypeScript/App.tsx b/samples-test/samples/TypeScript/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..08f1e3be37894b2d828a5c2b70c4c2847576b8ec --- /dev/null +++ b/samples-test/samples/TypeScript/App.tsx @@ -0,0 +1,26 @@ +import React, { useState } from 'react'; + +function App() { + const [count, setCount] = useState(0); + return ( +
+
+

Welcome to My React App

+

+ Start editing to see some magic happen! +

+
+

Count: {count}

+ + +
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/samples-test/samples/TypeScript/calculate.ts b/samples-test/samples/TypeScript/calculate.ts new file mode 100644 index 0000000000000000000000000000000000000000..249b2a6f980789f54343b0ec6acbd794c421319d --- /dev/null +++ b/samples-test/samples/TypeScript/calculate.ts @@ -0,0 +1,17 @@ +/** + * Evaluates a mathematical expression string and returns the result + * @param expression The mathematical expression to evaluate + * @returns The calculated result + */ +export function calculate(expression: string): number { + try { + // Remove all whitespace from the expression + const cleanExpression = expression.replace(/\s+/g, ''); + + // Use Function constructor to safely evaluate the expression + // This is safer than using eval() + return new Function(`return ${cleanExpression}`)(); + } catch (error) { + throw new Error('Invalid mathematical expression'); + } +} \ No newline at end of file diff --git a/samples-test/samples/TypeSpec/main.tsp b/samples-test/samples/TypeSpec/main.tsp new file mode 100644 index 0000000000000000000000000000000000000000..642424e07897d2e4518bafd9a051b8a64901471f --- /dev/null +++ b/samples-test/samples/TypeSpec/main.tsp @@ -0,0 +1,44 @@ +import "@typespec/http"; + +using Http; +@service(#{ title: "Widget Service" }) +namespace DemoService; + +model Widget { + id: string; + weight: int32; + color: "red" | "blue"; +} + +model WidgetList { + items: Widget[]; +} + +@error +model Error { + code: int32; + message: string; +} + +model AnalyzeResult { + id: string; + analysis: string; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** List widgets */ + @get list(): WidgetList | Error; + /** Read widgets */ + @get read(@path id: string): Widget | Error; + /** Create a widget */ + @post create(@body body: Widget): Widget | Error; + /** Update a widget */ + @patch update(@path id: string, @body body: MergePatchUpdate): Widget | Error; + /** Delete a widget */ + @delete delete(@path id: string): void | Error; + + /** Analyze a widget */ + @route("{id}/analyze") @post analyze(@path id: string): AnalyzeResult | Error; +} diff --git a/samples-test/samples/XML/types.xml b/samples-test/samples/XML/types.xml new file mode 100644 index 0000000000000000000000000000000000000000..64782e14b009f832e59a84ad8c040a7fac3776be --- /dev/null +++ b/samples-test/samples/XML/types.xml @@ -0,0 +1,25 @@ + + + + + Integer + Whole number data type + 42 + + + String + Text data type + Hello World + + + Boolean + Logical data type + true + + + Float + Decimal number data type + 3.14 + + + \ No newline at end of file diff --git a/samples-test/samples/YAML/dependabot.yml b/samples-test/samples/YAML/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..f0e75dcf4d27d5e9445ff3765f807c044e9ed4e8 --- /dev/null +++ b/samples-test/samples/YAML/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + groups: + proc-macro: + patterns: + - "prettyplease" + - "proc-macro2" + - "quote" + - "syn" + serde: + patterns: + - serde + - "serde_*" + - "serde-*" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/samples-test/samples/fish/mkcd.fish b/samples-test/samples/fish/mkcd.fish new file mode 100644 index 0000000000000000000000000000000000000000..d6ce2badeea3b0877b460c58ef168df0022c9b8e --- /dev/null +++ b/samples-test/samples/fish/mkcd.fish @@ -0,0 +1,4 @@ +function mkcd --wraps=mkdir --description 'Make a directory and cd into it' --argument directory +mkdir -p $directory +cd $directory +end diff --git a/samples-test/samples/reStructured Text/example.rst b/samples-test/samples/reStructured Text/example.rst new file mode 100644 index 0000000000000000000000000000000000000000..1484dcbc8cdad2c29b76519d8cea9713713c3d72 --- /dev/null +++ b/samples-test/samples/reStructured Text/example.rst @@ -0,0 +1,119 @@ +=============================== +reStructuredText Syntax Guide +=============================== + +Basic Formatting +--------------- +*italic text* +**bold text** +``inline code`` + +Headers +------- +Level 1 +======= + +Level 2 +------- + +Level 3 +~~~~~~~ + +Lists +----- +* Bullet point +* Another point + * Subpoint + * Another subpoint + +1. Numbered list +2. Second item +#. Auto-numbered item + +Links +----- +`Link text `_ +External link_ +.. _link: https://example.com + +Code Blocks +---------- +.. code-block:: python + + def hello(): + print("Hello World") + +Tables +------ ++------------+------------+ +| Header 1 | Header 2 | ++============+============+ +| Cell 1 | Cell 2 | ++------------+------------+ + +Simple Table: +=========== =========== +Header 1 Header 2 +=========== =========== +Row 1 Value +Row 2 Value +=========== =========== + +Images +------ +.. image:: path/to/image.jpg + :width: 100 + :alt: Alt text + +Notes & Warnings +--------------- +.. note:: + This is a note + +.. warning:: + This is a warning + +Directives +---------- +.. contents:: Table of Contents + :depth: 2 + +.. sidebar:: Sidebar Title + :subtitle: Optional subtitle + + Sidebar content + +.. topic:: Topic Title + + Topic content + +Cross-References +--------------- +.. _my-reference-label: + +Section Title +------------ +Reference to :ref:`my-reference-label` + +Footnotes +--------- +A footnote reference [1]_ + +.. [1] This is the footnote content + +Comments +-------- +.. This is a comment + +Line Blocks +---------- +| Line blocks preserve +| line breaks and +| leading whitespace + +Definition Lists +--------------- +term + Definition of the term +another term + Definition of another term \ No newline at end of file diff --git a/samples-test/src/lib.rs b/samples-test/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..b00a186a6bb338276057c32ee4d519204d0c633a --- /dev/null +++ b/samples-test/src/lib.rs @@ -0,0 +1,73 @@ +pub use gengo; + +#[cfg(test)] +mod tests { + use super::*; + use std::error::Error; + use std::fmt::{self, Display}; + use std::fs; + use std::path::{Path, PathBuf}; + + #[derive(Debug)] + struct TestError { + expected_language_name: String, + actual_language_name: String, + filename: PathBuf, + } + + impl Display for TestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Expected {} to be {}, got {}", + self.filename.display(), + self.expected_language_name, + self.actual_language_name + ) + } + } + + impl Error for TestError {} + + #[test] + fn test_samples() -> Result<(), Vec> { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let samples_dir = manifest_dir.join("samples"); + let errors: Vec<_> = samples_dir + .read_dir() + .expect("samples directory to be readable") + .flat_map(|entry| { + let samples_dir = samples_dir.clone(); + let path = entry.expect("entry to be readable").path(); + let language_dir = path.strip_prefix(&samples_dir).unwrap(); + path.read_dir() + .expect("language directory to be readable") + .map(|entry| { + let path = entry.expect("entry to be readable").path(); + let contents = fs::read(&path).expect("file to be readable"); + let relative_path = path.strip_prefix(&samples_dir).unwrap(); + let actual = gengo::Language::pick(relative_path, &contents, 1 << 20) + .expect("language to exist"); + let actual_language_name = actual.name().to_string(); + let expected_language_name = language_dir.display().to_string(); + if actual_language_name != expected_language_name { + Err(TestError { + expected_language_name, + actual_language_name, + filename: relative_path.to_owned(), + }) + } else { + Ok(()) + } + }) + .collect::>() + }) + .filter_map(Result::err) + .collect(); + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} diff --git a/scoop/gengo.json b/scoop/gengo.json new file mode 100644 index 0000000000000000000000000000000000000000..aa6791bb2cd58d96af900b5d203b68252fdd6a37 --- /dev/null +++ b/scoop/gengo.json @@ -0,0 +1,13 @@ +{ + "version": "0.14.2", + "description": "A linguist-inspired language classifier with multiple file source handlers", + "homepage": "https://github.com/spenserblack/gengo", + "license": "MIT | Apache-2.0", + "url": "https://github.com/spenserblack/gengo/releases/download/v0.14.2/gengo-x86_64-pc-windows-msvc.tar.gz", + "hash": "c799d64cf2cb8ef5ce5407ca76cd3bd949a8847bee14e79a29865f1e984a8034", + "bin": "gengo.exe", + "checkver": "github", + "autoupdate": { + "url": "https://github.com/spenserblack/gengo/releases/download/v$version/gengo-x86_64-pc-windows-msvc.tar.gz" + } +} diff --git a/scripts/check-languages-file.rb b/scripts/check-languages-file.rb new file mode 100755 index 0000000000000000000000000000000000000000..f7ca3ac46eb3ac24f7d05a5ca5e6c32990138865 --- /dev/null +++ b/scripts/check-languages-file.rb @@ -0,0 +1,96 @@ +#!/usr/bin/env ruby +require 'yaml' + +def sorted?(array) + sorted = true + array.each_cons(2) do |a, b| + if a.downcase > b.downcase + sorted = false + yield a, b if block_given? + end + end + sorted +end + +if ARGV.empty? + warn "Usage: #{$PROGRAM_NAME} " + exit 1 +end + +LANGUAGES = YAML.load_file(ARGV[0]) + +exit_code = 0 +sorted?(LANGUAGES.keys) do |lang1, lang2| + STDERR.puts "Language '#{lang1}' and '#{lang2}' are out of order" + exit_code = 1 +end + +LANGUAGES.each do |langname, langdef| + if !langdef.is_a?(Hash) + STDERR.puts "#{langname}: must be an object" + exit_code = 1 + next + end + missing_required = ['category', 'color', 'matchers'].any? do |key| + if langdef.nil? || !langdef.has_key?(key) + STDERR.puts "#{langname}: missing required key '#{key}'" + exit_code = 1 + true + end + end + next if missing_required + + warn "#{langname}: color should be in format #RRGGBB" unless langdef['color'] =~ /^#[0-9a-f]{6}$/i + + if langdef.key?('heuristics') && !langdef['heuristics'].is_a?(Array) + STDERR.puts "#{langname}: 'heuristics' must be an array" + exit_code = 1 + end + + bad_matchers = false + matchers = langdef['matchers'] + matcher_keys = ['extensions', 'filenames', 'interpreters', 'patterns'] + if !matchers.is_a?(Hash) + STDERR.puts "#{langname}: 'matchers' must be an object" + exit_code = 1 + bad_matchers = true + else + matcher_keys.each do |key| + if matchers.has_key?(key) && !matchers[key].is_a?(Array) + STDERR.puts "#{langname}: 'matchers.#{key}' must be an array" + exit_code = 1 + bad_matchers = true + end + end + end + next if bad_matchers + + unless matcher_keys.any? { |key| matchers.has_key?(key) } + STDERR.puts "#{langname}: 'matchers' must have at least one of #{matcher_keys.join(', ')}" + exit_code = 1 + end + + sorted?(matchers.keys) do |a, b| + STDERR.puts "#{langname}: 'matchers' are out of order: '#{a}' and '#{b}'" + exit_code = 1 + end + + matchers.each do |matcher_name, matcher| + unless matcher.is_a?(Array) + STDERR.puts "#{langname}: matcher '#{matcher_name}' must be an array" + exit_code = 1 + next + end + sorted?(matcher) do |a, b| + STDERR.puts "#{langname}: matcher '#{matcher_name}' is out of order: '#{a}' and '#{b}'" + exit_code = 1 + end + end + + if langdef.key?('priority') && !langdef['priority'].is_a?(Integer) && langdef['priority'].between?(0, 100) + STDERR.puts "#{langname}: 'priority' must be an integer between 0 and 100" + exit_code = 1 + end +end + +exit exit_code diff --git a/scripts/conflicts.rb b/scripts/conflicts.rb new file mode 100755 index 0000000000000000000000000000000000000000..38eafd7286f21db0ae13628b48155c2d3cff8f9d --- /dev/null +++ b/scripts/conflicts.rb @@ -0,0 +1,41 @@ +#!/usr/bin/env ruby +require 'yaml' + +if ARGV.empty? + warn "Usage: #{$PROGRAM_NAME} " + exit 1 +end + +LANGUAGES = YAML.load_file(ARGV[0]) + +extensions = Hash.new { |h, k| h[k] = [] } +filenames = Hash.new { |h, k| h[k] = [] } +interpreters = Hash.new { |h, k| h[k] = [] } + +LANGUAGES.each do |language, info| + next if info['heuristics']&.any? + + # TODO: Check for conflicting regexes? + matchers = info['matchers'] + matchers['extensions']&.each do |ext| + extensions[ext] << language + end + matchers['filenames']&.each do |filename| + filenames[filename] << language + end + matchers['interpreters']&.each do |interpreter| + interpreters[interpreter] << language + end +end + +extensions.each do |ext, langs| + puts "Extension #{ext} is used by #{langs.join(', ')}" if langs.size > 1 +end + +filenames.each do |filename, langs| + puts "Filename #{filename} is used by #{langs.join(', ')}" if langs.size > 1 +end + +interpreters.each do |interpreter, langs| + puts "Interpreter #{interpreter} is used by #{langs.join(', ')}" if langs.size > 1 +end diff --git a/scripts/preview-nerd-font-glyphs.rb b/scripts/preview-nerd-font-glyphs.rb new file mode 100755 index 0000000000000000000000000000000000000000..c40606ae6f03dcbd7fdc33fe764adc5b2cb6e232 --- /dev/null +++ b/scripts/preview-nerd-font-glyphs.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +require 'yaml' + +if ARGV.empty? + warn "Usage: #{$PROGRAM_NAME} " + exit 1 +end + +LANGUAGES = YAML.load_file(ARGV[0]) + +LANGUAGES.entries.each do |language, data| + glyph = data['nerd-font-glyph'] + puts "#{language}: #{glyph}" +end diff --git a/scripts/should-tag-new-language.rb b/scripts/should-tag-new-language.rb new file mode 100755 index 0000000000000000000000000000000000000000..d3bd6baef39475a541c34e0362c6201c215894a2 --- /dev/null +++ b/scripts/should-tag-new-language.rb @@ -0,0 +1,26 @@ +#!/usr/bin/env ruby +# Checks two files. If the new file has exactly one new language compared to +# the old file, and there weren't any removed or renamed languages, then this +# will exit with status 0. Otherwise, it will exit with status 1. +require 'set' +require 'yaml' + +if ARGV.length != 2 + warn "Usage: #{$PROGRAM_NAME} " + exit 2 +end + +old_languages_yaml = YAML.load_file(ARGV[0]) || {} +new_languages_yaml = YAML.load_file(ARGV[1]) || {} + +old_languages = old_languages_yaml.keys.to_set +new_languages = new_languages_yaml.keys.to_set + +removed_languages = old_languages - new_languages +added_languages = new_languages - old_languages + +$stderr.puts "Removed languages: #{removed_languages.to_a.join(', ')}" +$stderr.puts "Added languages: #{added_languages.to_a.join(', ')}" + +exit_code = removed_languages.size == 0 && added_languages.size == 1 ? 0 : 1 +exit exit_code diff --git a/scripts/update-scoop.rb b/scripts/update-scoop.rb new file mode 100755 index 0000000000000000000000000000000000000000..0fe6e946362d959a87a9beb80bd0cde5fee4f162 --- /dev/null +++ b/scripts/update-scoop.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +require "json" + +if ARGV.length != 2 + warn "Usage: #{$PROGRAM_NAME} " + exit 1 +end + +path = ARGV[0] +tag = ARGV[1] +# Make version plain X.Y.Z without "v" prefix +version = tag.delete_prefix("v") + +manifest = JSON.load_file(path) +manifest["version"] = version +manifest["url"] = "https://github.com/spenserblack/gengo/releases/download/#{tag}/gengo-x86_64-pc-windows-msvc.tar.gz" +pretty = JSON.pretty_generate(manifest) + +File.open(path, "w") do |file| + file.write(pretty) + # Final newline + file.write("\n") +end