[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [lukas-reineke]\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'cargo'\n    directory: '/'\n    schedule:\n      interval: 'monthly'\n"
  },
  {
    "path": ".github/workflows/pr_check.yml",
    "content": "name: Pull request check\n\non:\n  pull_request:\n\njobs:\n  block-fixup:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Block Fixup Commit Merge\n        uses: 13rac1/block-fixup-merge-action@v2.0.0\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        toolchain:\n          - stable\n          - beta\n          - nightly\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          submodules: recursive\n      - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}\n      - run: cargo build --verbose\n  format:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          submodules: recursive\n      - run: rustup update stable && rustup default stable\n      - run: rustup component add rustfmt\n      - run: cargo fmt --all -- --check\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: upload\n\n# copied from https://github.com/rust-lang/rustfmt/blob/master/.github/workflows/upload-assets.yml\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n\njobs:\n  build-release:\n    name: build-release\n    strategy:\n      matrix:\n        build:\n          [\n            linux-x86_64,\n            linux-x86_64-musl,\n            macos-x86_64,\n            windows-x86_64-gnu,\n            windows-x86_64-msvc,\n          ]\n        include:\n          - build: linux-x86_64\n            os: ubuntu-latest\n            rust: nightly\n            target: x86_64-unknown-linux-gnu\n            build_command: build\n          - build: linux-x86_64-musl\n            os: ubuntu-latest\n            rust: nightly\n            target: x86_64-unknown-linux-musl\n            build_command: zigbuild\n          - build: macos-x86_64\n            os: macos-latest\n            rust: nightly\n            target: x86_64-apple-darwin\n            build_command: build\n          - build: windows-x86_64-gnu\n            os: windows-latest\n            rust: nightly-x86_64-gnu\n            target: x86_64-pc-windows-gnu\n            build_command: build\n          - build: windows-x86_64-msvc\n            os: windows-latest\n            rust: nightly-x86_64-msvc\n            target: x86_64-pc-windows-msvc\n            build_command: build\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v3\n\n        # Run build\n      - name: install rustup\n        run: |\n          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh\n          sh rustup-init.sh -y --default-toolchain none\n          rustup target add ${{ matrix.target }}\n\n      - name: Add mingw64 to path for x86_64-gnu\n        run: echo \"C:\\msys64\\mingw64\\bin\" >> $GITHUB_PATH\n        if: matrix.rust == 'nightly-x86_64-gnu'\n        shell: bash\n\n      - name: Install dependencies for x86_64-musl\n        run: |\n          sudo apt install musl-tools python3-pip\n          sudo pip3 install ziglang\n          cargo install cargo-zigbuild\n        if: matrix.target == 'x86_64-unknown-linux-musl'\n        shell: bash\n\n      - name: Build release binaries\n        uses: actions-rs/cargo@v1\n        with:\n          command: ${{ matrix.build_command }}\n          args: --release --target ${{ matrix.target }}\n\n      - name: Build archive\n        shell: bash\n        run: |\n          staging=\"cbfmt_${{ matrix.build }}_${{ github.event.release.tag_name }}\"\n          mkdir -p \"$staging\"\n\n          cp {README.md,LICENSE.md} \"$staging/\"\n\n          if [ \"${{ matrix.os }}\" = \"windows-latest\" ]; then\n            cp target/${{ matrix.target }}/release/cbfmt.exe \"$staging/\"\n            7z a \"$staging.zip\" \"$staging\"\n            echo \"ASSET=$staging.zip\" >> $GITHUB_ENV\n          else\n            cp target/${{ matrix.target }}/release/cbfmt \"$staging/\"\n            tar czf \"$staging.tar.gz\" \"$staging\"\n            echo \"ASSET=$staging.tar.gz\" >> $GITHUB_ENV\n          fi\n\n      - name: Upload Release Asset\n        if: github.event_name == 'release'\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ github.event.release.upload_url }}\n          asset_path: ${{ env.ASSET }}\n          asset_name: ${{ env.ASSET }}\n          asset_content_type: application/octet-stream\n\n  publish-to-cargo:\n    name: Publishing to Cargo\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@master\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          profile: minimal\n          override: true\n      - uses: actions-rs/cargo@v1\n        with:\n          command: publish\n          args: --token ${{ secrets.CARGO_API_KEY }} --allow-dirty\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"cbfmt\"\nversion = \"0.2.0\"\nedition = \"2021\"\ndescription = \"A tool to format codeblocks inside markdown, org, and restructuredtext documents\"\nrepository = \"https://github.com/lukas-reineke/cbfmt\"\ncategories = [\"development-tools\"]\nkeywords = [\"format\", \"markdown\", \"org\", \"codeblock\"]\nlicense = \"MIT\"\n\n[[bin]]\nname = \"cbfmt\"\ndoc = false\n\n[dependencies]\natty = \"0.2.14\"\nclap = \"3.2.8\"\nfutures = \"0.3.21\"\nignore = \"0.4.18\"\nserde = { version = \"1.0.138\", features = [\"derive\"] }\ntermcolor = \"1.1.3\"\ntextwrap = \"0.15.0\"\nthiserror = \"1.0.31\"\ntokio = { version = \"1.20.0\", features = [\"macros\", \"fs\", \"rt-multi-thread\"] }\ntoml = \"0.5.9\"\ntree-sitter = \"~0.20\"\ntree-sitter-md = \"0.1.1\"\ntree-sitter-org = \"1.3.0\"\ntree-sitter-rst = \"0.1.0\"\n\n[build-dependencies]\ncc = \"1.0.73\"\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT Licence\n\nCopyright (c) 2022 Lukas Reineke\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img height=\"400\" src=\"https://user-images.githubusercontent.com/12900252/188409065-2149a392-e5cb-4486-8d2b-b80f9022ef4c.png\" alt=\"cbfmt\" />\n</p>\n\n# cbfmt (codeblock format)\n\nA tool to format codeblocks inside markdown, org, and restructuredtext documents.  \nIt iterates over all codeblocks, and formats them with the tool(s) specified for\nthe language of the block.\n\n## Install\n\n### Download from GitHub\n\nDownload the latest release binaries from [github.com/lukas-reineke/cbfmt/releases](https://github.com/lukas-reineke/cbfmt/releases)\n\n### Cargo\n\n```bash\ncargo install cbfmt\n```\n\n### Build from source\n\n1. Clone this repository\n2. Build with [cargo](https://github.com/rust-lang/cargo/)\n\n```bash\ngit clone https://github.com/lukas-reineke/cbfmt.git && cd cbfmt\ncargo install --path .\n```\n\nThis will install `cbfmt` in your `~/.cargo/bin`. Make sure to add `~/.cargo/bin` directory to your `PATH` variable.\n\n## Config\n\nA configuration file is required. By default the file is called\n`.cbfmt.toml`\n\nExample:\n\n```toml\n[languages]\nrust = [\"rustfmt\"]\ngo = [\"gofmt\"]\nlua = [\"stylua -s -\"]\npython = [\"black --fast -\"]\n```\n\n### Sections\n\n#### languages\n\nThis section specifies which commands should run for which language.  \nEach entry is the name of the language as the key, and a list of format commands\nto run in sequence as the value. Each format command needs to read from stdin\nand write to stdout.\n\n## Usage\n\n### With arguments\n\nYou can run `cbfmt` on files and or directories by passing them as\narguments.\n\n```bash\ncbfmt [OPTIONS] [file/dir/glob]...\n```\n\nThe default behaviour checks formatting for all files that were passed as\narguments. If all files are formatted correctly, it exits with status code 0,\notherwise it exits with status code 1.\n\nWhen a directory is passed as an argument, `cbfmt` will recursively run on all files\nin that directory which have a valid parser and are not ignored by git.\n\n### With stdin\n\nIf no arguments are specified, `cbfmt` will read from stdin and write the format\nresult to stdout.\n\n```bash\ncbfmt [OPTIONS] < [file]\n```\n\n### Without arguments and stdin\n\nIf there are no arguments and nothing is written to stdin, `cbfmt` will print\nthe help text and exit.\n\n### Options\n\nThese are the most important options. To see all options, please run\n`cbfmt --help`\n\n#### check `-c|--check`\n\nWorks the same as the default behaviour, but only prints the path to files that\nfail.\n\n#### write `-w|--write`\n\nWrites the format result back into the files.\n\n#### parser `-p|--parser`\n\nSpecifies which parser to use. This is inferred from the file ending when\npossible.\n"
  },
  {
    "path": "src/config.rs",
    "content": "use serde::Deserialize;\nuse std::collections::HashMap;\n\n#[derive(Debug, Deserialize)]\npub struct Conf {\n    pub languages: HashMap<String, Vec<String>>,\n}\n\npub fn get(name: &str) -> Result<Conf, std::io::Error> {\n    let toml_string = std::fs::read_to_string(name)?;\n    let conf: Conf = toml::from_str(&toml_string)?;\n    Ok(conf)\n}\n"
  },
  {
    "path": "src/format.rs",
    "content": "use super::config::Conf;\nuse super::tree;\nuse super::utils;\nuse futures::{stream::FuturesOrdered, StreamExt};\nuse std::char;\nuse std::fmt;\nuse std::io::{self, prelude::*, Error, ErrorKind, Write};\nuse std::process::{Command, Stdio};\nuse textwrap::dedent;\n\n#[derive(thiserror::Error, Debug)]\npub struct FormatError {\n    pub msg: String,\n    pub filename: Option<String>,\n    pub command: Option<String>,\n    pub language: Option<String>,\n    pub start: Option<String>,\n}\n\nimpl fmt::Display for FormatError {\n    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n        if let Some(filename) = &self.filename {\n            write!(formatter, \"{filename}\")?;\n        }\n        if let Some(start) = &self.start {\n            write!(formatter, \"{start}\")?;\n        }\n        if let Some(language) = &self.language {\n            write!(formatter, \" [{language}] ->\")?;\n        }\n        if let Some(command) = &self.command {\n            write!(formatter, \" [{command}] \")?;\n        }\n        write!(formatter, \"\\n{}\", self.msg)\n    }\n}\n\npub enum FormatResult {\n    Unchanged(String),\n    Changed(String),\n    Err(FormatError),\n}\n\npub async fn run_file(\n    conf: &Conf,\n    filename: String,\n    parser: Option<&str>,\n    write: bool,\n    best_effort: bool,\n) -> FormatResult {\n    let parser = match utils::get_parser(Some(&filename), parser) {\n        Ok(p) => p,\n        Err(e) => return FormatResult::Err(e),\n    };\n\n    let file = match tokio::fs::read(&filename).await {\n        Err(error) => {\n            return FormatResult::Err(FormatError {\n                msg: error.to_string(),\n                filename: Some(filename),\n                command: None,\n                language: None,\n                start: None,\n            })\n        }\n        Ok(f) => f,\n    };\n    let buf = file.lines().map(|l| l.unwrap()).collect::<Vec<_>>();\n\n    match run(buf, conf, &parser, !write, best_effort).await {\n        FormatResult::Changed(r) => {\n            if write {\n                if let Some(error) = tokio::fs::write(&filename, r).await.err() {\n                    return FormatResult::Err(FormatError {\n                        msg: error.to_string(),\n                        filename: Some(filename),\n                        command: None,\n                        language: None,\n                        start: None,\n                    });\n                }\n            }\n            FormatResult::Changed(filename)\n        }\n        FormatResult::Unchanged(_) => FormatResult::Unchanged(filename),\n        FormatResult::Err(mut error) => {\n            error.filename = Some(filename);\n            FormatResult::Err(error)\n        }\n    }\n}\n\npub async fn run_stdin(\n    conf: &Conf,\n    filename: Option<&str>,\n    parser: Option<&str>,\n    best_effort: bool,\n) -> FormatResult {\n    let parser = match utils::get_parser(filename, parser) {\n        Ok(p) => p,\n        Err(e) => return FormatResult::Err(e),\n    };\n\n    let buf = io::stdin().lines().map(|l| l.unwrap()).collect::<Vec<_>>();\n\n    match run(buf, conf, &parser, false, best_effort).await {\n        FormatResult::Changed(r) => {\n            let mut stdout = io::stdout().lock();\n            stdout.write_all(r.as_bytes()).unwrap();\n            FormatResult::Changed(\"stdin\".to_string())\n        }\n        FormatResult::Unchanged(r) => {\n            let mut stdout = io::stdout().lock();\n            stdout.write_all(r.as_bytes()).unwrap();\n            FormatResult::Unchanged(\"stdin\".to_string())\n        }\n        FormatResult::Err(e) => FormatResult::Err(e),\n    }\n}\n\nstruct FormatCtx {\n    language: String,\n    codeblock_start: usize,\n    start: usize,\n    end: usize,\n    input_hash: u64,\n}\n\nasync fn run(\n    mut buf: Vec<String>,\n    conf: &Conf,\n    parser: &str,\n    fail_fast: bool,\n    best_effort: bool,\n) -> FormatResult {\n    let src = buf.join(\"\\n\");\n    let src_bytes = src.as_bytes();\n    let tree = match tree::get_tree(parser, src_bytes) {\n        Some(t) => t,\n        None => {\n            return FormatResult::Err(FormatError {\n                msg: format!(\"No parser found for {}.\", parser),\n                filename: None,\n                command: None,\n                language: None,\n                start: None,\n            })\n        }\n    };\n    let query = tree::get_query(parser).unwrap();\n\n    let mut futures: FuturesOrdered<_> = FuturesOrdered::new();\n\n    let mut cursor = tree_sitter::QueryCursor::new();\n    for each_match in cursor.matches(&query, tree.root_node(), src_bytes) {\n        let mut content = String::new();\n        let mut ctx = FormatCtx {\n            language: String::new(),\n            codeblock_start: 0,\n            start: 0,\n            end: 0,\n            input_hash: 0,\n        };\n\n        for capture in each_match.captures.iter() {\n            let mut range = capture.node.range();\n\n            for predicate in query.general_predicates(each_match.pattern_index) {\n                range = tree::handle_directive(&predicate.operator, &range, &predicate.args)\n                    .unwrap_or(range);\n            }\n\n            let capture_name = &query.capture_names()[capture.index as usize];\n\n            if capture_name == \"language\" {\n                ctx.language = String::from(&src[range.start_byte..range.end_byte]);\n            }\n            if capture_name == \"content\" {\n                ctx.start = range.start_point.row;\n                ctx.end = range.end_point.row;\n                let mut end_byte = range.end_byte;\n\n                // Workaround for bug in markdown parser when the codeblock is the last thing in a\n                // buffer\n                if parser == \"markdown\" && &src[(end_byte - 3)..end_byte] == \"```\" {\n                    end_byte -= 3\n                }\n\n                content = String::from(dedent(&src[range.start_byte..end_byte]));\n            }\n            if capture_name == \"codeblock\" {\n                ctx.codeblock_start = range.start_point.row;\n            }\n        }\n\n        let formatter = conf.languages.get(&ctx.language);\n        let formatter = match formatter {\n            Some(f) => f,\n            None => continue,\n        };\n        let formatter = formatter.iter().map(|f| f.to_owned()).collect();\n\n        ctx.input_hash = utils::get_hash(&content);\n        futures.push_back(tokio::spawn(async move {\n            format(ctx, formatter, &content).await\n        }));\n    }\n\n    let mut formatted = false;\n    let mut offset: i32 = 0;\n    while let Some(output) = futures.next().await {\n        let output = match output {\n            Ok(o) => o,\n            Err(e) => {\n                return FormatResult::Err(FormatError {\n                    msg: e.to_string(),\n                    filename: None,\n                    command: None,\n                    language: None,\n                    start: None,\n                });\n            }\n        };\n        let (ctx, output) = match output {\n            Ok(o) => o,\n            Err(e) => {\n                if best_effort {\n                    continue;\n                }\n                return FormatResult::Err(e);\n            }\n        };\n\n        let indent = utils::get_start_whitespace(&buf[(ctx.start as i32 + offset) as usize]);\n\n        let mut fixed_output = String::new();\n        for line in output.lines() {\n            fixed_output.push_str(&indent);\n            fixed_output.push_str(line);\n            fixed_output.push('\\n');\n        }\n\n        // trim start for the hash because treesitter ignores leading indent\n        let output_hash = utils::get_hash(fixed_output.trim_start());\n        if ctx.input_hash != output_hash {\n            formatted = true;\n            if fail_fast {\n                break;\n            }\n        }\n\n        buf.drain((ctx.start as i32 + offset) as usize..(ctx.end as i32 + offset) as usize);\n\n        let mut counter = 0;\n        for (i, line) in fixed_output.lines().enumerate() {\n            buf.insert(i + (ctx.start as i32 + offset) as usize, line.to_string());\n            counter += 1;\n        }\n\n        offset += counter - (ctx.end as i32 - ctx.start as i32);\n    }\n\n    let output = buf.join(\"\\n\") + \"\\n\";\n    if formatted {\n        return FormatResult::Changed(output);\n    }\n    FormatResult::Unchanged(output)\n}\n\n#[derive(Debug, PartialEq)]\nstruct ParsedCommand<'a> {\n    cmd: &'a str,\n    args: Vec<&'a str>,\n}\n\nfn parse_command<'a>(raw_command: &'a str) -> Result<ParsedCommand<'a>, &str> {\n    let mut parsed_components = raw_command.split(char::is_whitespace);\n    let cmd = parsed_components.next().ok_or(\"No command found.\")?;\n    if cmd.is_empty() {\n        return Err(\"No command provided.\");\n    }\n    Ok(ParsedCommand {\n        cmd,\n        args: parsed_components.collect(),\n    })\n}\n\nasync fn format(\n    ctx: FormatCtx,\n    formatter: Vec<String>,\n    content: &str,\n) -> Result<(FormatCtx, String), FormatError> {\n    let mut result = String::from(content);\n    let language = Some(ctx.language.to_owned());\n    let start = Some(format!(\":{}\", ctx.start));\n\n    for f in formatter.iter() {\n        match parse_command(f) {\n            Ok(parsed_command) => {\n                result = match format_single(&parsed_command, &result) {\n                    Err(e) => {\n                        return Err(FormatError {\n                            msg: e.to_string(),\n                            filename: None,\n                            command: Some(parsed_command.cmd.to_string()),\n                            language,\n                            start,\n                        });\n                    }\n                    Ok(o) => o,\n                }\n            }\n            Err(msg) => {\n                return Err(FormatError {\n                    msg: msg.to_owned(),\n                    filename: None,\n                    command: None,\n                    language,\n                    start,\n                })\n            }\n        }\n    }\n\n    Ok((ctx, result))\n}\n\nfn format_single(formatter: &ParsedCommand, input: &str) -> Result<String, Error> {\n    let mut child = Command::new(formatter.cmd)\n        .args(&formatter.args)\n        .stdin(Stdio::piped())\n        .stderr(Stdio::piped())\n        .stdout(Stdio::piped())\n        .spawn()?;\n\n    let stdin = child.stdin.as_mut().ok_or_else(|| {\n        Error::new(\n            ErrorKind::Other,\n            String::from(\"Child process stdin has not been captured.\"),\n        )\n    })?;\n    stdin.write_all(input.as_bytes())?;\n\n    let output = child.wait_with_output()?;\n\n    if output.status.success() {\n        Ok(String::from_utf8(output.stdout).unwrap())\n    } else {\n        Err(Error::new(\n            ErrorKind::Other,\n            String::from_utf8(output.stderr).unwrap(),\n        ))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse_empty_command() {\n        assert_eq!(Err(\"No command provided.\"), parse_command(\"\"));\n        assert_eq!(Err(\"No command provided.\"), parse_command(\"      \"));\n    }\n\n    #[test]\n    fn test_parse_whitespace_args() {\n        assert_eq!(\n            Ok(ParsedCommand {\n                cmd: \"shellharden\",\n                args: vec![\"--transform\", \"\"]\n            }),\n            parse_command(\"shellharden --transform \")\n        );\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "use clap::{App, Arg, ArgMatches};\nmod config;\nmod format;\nuse format::FormatResult;\nmod tree;\nmod utils;\nuse futures::{stream::FuturesUnordered, StreamExt};\nuse std::process;\nuse termcolor::{ColorChoice, StandardStream};\n\n#[tokio::main]\nasync fn main() {\n    let (mut color_choice, clap_color_choice) = if atty::is(atty::Stream::Stdout) {\n        (ColorChoice::Auto, clap::ColorChoice::Auto)\n    } else {\n        (ColorChoice::Never, clap::ColorChoice::Never)\n    };\n\n    let mut app =\n        App::new(\"cbfmt\")\n            .version(\"0.2.0\")\n            .author(\"Lukas Reineke <lukas@reineke.jp>\")\n            .about(\"A tool to format codeblocks inside markdown, org, and restructuredtext documents.\\nIt iterates over all codeblocks, and formats them with the tool(s) specified for the language of the block.\")\n            .arg(\n                Arg::with_name(\"config\")\n                    .long(\"config\")\n                    .value_name(\"FILE\")\n                    .help(\"Sets a custom config file.\")\n                    .takes_value(true),\n            )\n            .arg(\n                Arg::with_name(\"check\")\n                    .short('c')\n                    .long(\"check\")\n                    .takes_value(false)\n                    .help(\"Check if the given files are formatted. Print the path to unformatted files and exit with exit code 1 if they are not.\")\n            )\n            .arg(\n                Arg::with_name(\"fail_fast\")\n                    .long(\"fail-fast\")\n                    .takes_value(false)\n                    .help(\"Exit as soon as one file is not formatted correctly.\")\n            )\n            .arg(\n                Arg::with_name(\"write\")\n                    .short('w')\n                    .long(\"write\")\n                    .takes_value(false)\n                    .help(\"Edit files in-place.\")\n            )\n            .arg(\n                Arg::with_name(\"best_effort\")\n                    .long(\"best-effort\")\n                    .takes_value(false)\n                    .help(\"Ignore formatting errors and continue with the next codeblock.\")\n            )\n            .arg(\n                Arg::with_name(\"parser\")\n                    .short('p')\n                    .long(\"parser\")\n                    .value_name(\"markdown|org|restructuredtext\")\n                    .help(\"Sets the parser to use.\")\n                    .takes_value(true),\n            )\n            .arg(\n                Arg::with_name(\"stdin_filepath\")\n                    .long(\"stdin-filepath\")\n                    .help(\"Path to the file to pretend that stdin comes from.\")\n                    .takes_value(true),\n            )\n            .arg(\n                Arg::with_name(\"color\")\n                    .long(\"color\")\n                    .value_name(\"never|auto|always\")\n                    .help(\"Use colored output.\")\n                    .default_value(\"auto\")\n                    .takes_value(true),\n            )\n            .arg(\n                Arg::with_name(\"files\")\n                    .value_name(\"file/dir/glob\")\n                    .help(\"List of files to process. If no files are given cbfmt will read from Stdin.\")\n                    .index(1)\n                    .multiple_values(true),\n            )\n            .color(clap_color_choice);\n\n    let matches = app.to_owned().get_matches();\n\n    if let Some(color) = matches.value_of(\"color\") {\n        if color == \"never\" {\n            color_choice = ColorChoice::Never;\n        } else if color == \"always\" {\n            color_choice = ColorChoice::Always;\n        }\n    }\n\n    if matches.values_of(\"files\").is_none() && atty::is(atty::Stream::Stdin) {\n        app.print_help().unwrap();\n        return;\n    }\n\n    let mut stderr = StandardStream::stderr(color_choice);\n\n    let config_path = match matches.value_of(\"config\") {\n        Some(p) => p.to_owned(),\n        None => match utils::find_closest_config() {\n            Some(p) => p,\n            None => {\n                utils::print_error(&mut stderr, \"Could not find config file.\");\n                process::exit(1);\n            }\n        },\n    };\n    let conf = match config::get(&config_path) {\n        Ok(c) => c,\n        Err(_) => {\n            utils::print_error(&mut stderr, \"Could not parse config file.\");\n            process::exit(1);\n        }\n    };\n\n    match matches.values_of(\"files\") {\n        Some(_) => use_files(matches, &conf, color_choice).await,\n        None => use_stdin(matches, &conf).await,\n    }\n}\n\nasync fn use_files(matches: ArgMatches, conf: &config::Conf, color_choice: ColorChoice) {\n    let mut stdout = StandardStream::stdout(color_choice);\n    let mut stderr = StandardStream::stderr(color_choice);\n\n    let check = matches.is_present(\"check\");\n    let write = matches.is_present(\"write\");\n    let best_effort = matches.is_present(\"best_effort\");\n    let fail_fast = matches.is_present(\"fail_fast\");\n    let files = matches.values_of(\"files\").unwrap();\n    let parser = matches.value_of(\"parser\");\n\n    let mut futures: FuturesUnordered<_> = FuturesUnordered::new();\n    let files = match utils::get_files(files) {\n        Ok(f) => f,\n        Err(e) => {\n            utils::print_error(&mut stderr, &e.to_string());\n            process::exit(1);\n        }\n    };\n    for filename in files {\n        futures.push(format::run_file(conf, filename, parser, write, best_effort));\n    }\n\n    let mut error_count = 0;\n    let mut unchanged_count = 0;\n    let mut changed_count = 0;\n\n    while let Some(result) = futures.next().await {\n        match result {\n            FormatResult::Unchanged(f) => {\n                unchanged_count += 1;\n                if check {\n                    continue;\n                }\n                if write {\n                    utils::print_unchanged(&mut stdout, &f);\n                } else {\n                    utils::print_ok(&mut stdout, &f);\n                }\n            }\n            FormatResult::Changed(f) => {\n                changed_count += 1;\n                if check {\n                    eprintln!(\"{f}\")\n                } else if write {\n                    utils::print_ok(&mut stdout, &f);\n                } else {\n                    utils::print_fail(&mut stderr, &f);\n                }\n                if !write && fail_fast {\n                    println!(\"Failed fast...\");\n                    break;\n                }\n            }\n            FormatResult::Err(e) => {\n                error_count += 1;\n                if check {\n                    let filename = match &e.filename {\n                        Some(f) => f,\n                        None => \"Unknown\",\n                    };\n                    eprintln!(\"{filename}\");\n                } else {\n                    utils::print_error(&mut stderr, &e.to_string());\n                }\n                if fail_fast {\n                    println!(\"Failed fast...\");\n                    break;\n                }\n            }\n        }\n    }\n\n    let total_count = unchanged_count + changed_count + error_count;\n    if write {\n        println!(\"\\n[{changed_count}/{total_count}] files were written.\");\n    }\n\n    if !write && !check {\n        println!(\"\\n[{unchanged_count}/{total_count}] files are formatted correctly.\");\n    }\n\n    if error_count > 0 || (changed_count > 0 && !write) {\n        process::exit(1);\n    }\n}\n\nasync fn use_stdin(matches: ArgMatches, conf: &config::Conf) {\n    let parser = matches.value_of(\"parser\");\n    let filename = matches.value_of(\"stdin_filepath\");\n    let best_effort = matches.is_present(\"best_effort\");\n\n    if let FormatResult::Err(e) = format::run_stdin(conf, filename, parser, best_effort).await {\n        eprintln!(\"{e}\");\n        process::exit(1);\n    }\n}\n"
  },
  {
    "path": "src/tree.rs",
    "content": "use tree_sitter::Parser;\n\npub fn get_tree(parser_lang: &str, text: &[u8]) -> Option<tree_sitter::Tree> {\n    let mut parser = Parser::new();\n\n    match parser_lang {\n        \"markdown\" => {\n            parser\n                .set_language(tree_sitter_md::language())\n                .expect(\"Could not load markdown grammar\");\n        }\n        \"org\" => {\n            parser\n                .set_language(tree_sitter_org::language())\n                .expect(\"Could not load org grammar\");\n        }\n        \"restructuredtext\" => {\n            parser\n                .set_language(tree_sitter_rst::language())\n                .expect(\"Could not load restructuredtext grammar\");\n        }\n        _ => {\n            return None;\n        }\n    }\n\n    Some(parser.parse(text, None).expect(\"Could not parse input\"))\n}\n\npub fn get_query(parser_lang: &str) -> Option<tree_sitter::Query> {\n    match parser_lang {\n        \"markdown\" => Some(\n            tree_sitter::Query::new(\n                tree_sitter_md::language(),\n                r#\"\n                    (fenced_code_block\n                        (info_string (language) @language)\n                        (code_fence_content) @content) @codeblock\n                \"#,\n            )\n            .expect(\"Could not load markdown query\"),\n        ),\n        \"org\" => Some(\n            tree_sitter::Query::new(\n                tree_sitter_org::language(),\n                r#\"\n                    (block\n                        name: (expr) @_name\n                        (#match? @_name \"(SRC|src)\")\n                        parameter: (expr) @language\n                        contents: (contents) @content) @codeblock\n                \"#,\n            )\n            .expect(\"Could not load org query\"),\n        ),\n        \"restructuredtext\" => Some(\n            tree_sitter::Query::new(\n                tree_sitter_rst::language(),\n                r#\"\n                    (directive\n                        name: (type) @_name\n                        (#match? @_name \"code\")\n                        body: (body\n                            (arguments) @language\n                            (content) @content\n                            (#offset! @content 0 0 1 0))) @codeblock\n                    \n                \"#,\n            )\n            .expect(\"Could not load restructuredtext query\"),\n        ),\n        _ => None,\n    }\n}\n\npub fn get_parser_lang_from_filename(filename: &str) -> Option<&str> {\n    let filename = filename.to_lowercase();\n    if filename.ends_with(\".md\") {\n        return Some(\"markdown\");\n    }\n    if filename.ends_with(\".org\") {\n        return Some(\"org\");\n    }\n    if filename.ends_with(\".rst\") {\n        return Some(\"restructuredtext\");\n    }\n    None\n}\n\npub fn handle_directive(\n    directive: &str,\n    range: &tree_sitter::Range,\n    args: &Vec<tree_sitter::QueryPredicateArg>,\n) -> Option<tree_sitter::Range> {\n    match directive {\n        \"offset!\" => {\n            let start_row_offset = match &args[1] {\n                tree_sitter::QueryPredicateArg::String(value) => value.parse::<usize>().unwrap(),\n                _ => panic!(\"Unexpected argument type for offset!\"),\n            };\n            let start_col_offset = match &args[2] {\n                tree_sitter::QueryPredicateArg::String(value) => value.parse::<usize>().unwrap(),\n                _ => panic!(\"Unexpected argument type for offset!\"),\n            };\n            let end_row_offset = match &args[3] {\n                tree_sitter::QueryPredicateArg::String(value) => value.parse::<usize>().unwrap(),\n                _ => panic!(\"Unexpected argument type for offset!\"),\n            };\n            let end_col_offset = match &args[4] {\n                tree_sitter::QueryPredicateArg::String(value) => value.parse::<usize>().unwrap(),\n                _ => panic!(\"Unexpected argument type for offset!\"),\n            };\n\n            let mut new_range = range.clone();\n            new_range.start_point.row = range.start_point.row + start_row_offset;\n            new_range.start_point.column = range.start_point.column + start_col_offset;\n            new_range.end_point.row = range.end_point.row + end_row_offset;\n            new_range.end_point.column = range.end_point.column + end_col_offset;\n            return Some(new_range);\n        }\n        &_ => {}\n    }\n    None\n}\n"
  },
  {
    "path": "src/utils.rs",
    "content": "use super::format::FormatError;\nuse super::tree;\nuse clap::Values;\nuse ignore::WalkBuilder;\nuse std::collections::hash_map::DefaultHasher;\nuse std::env;\nuse std::fs;\nuse std::hash::Hash;\nuse std::hash::Hasher;\nuse std::io;\nuse termcolor::{Color, ColorSpec, StandardStream, WriteColor};\n\npub fn get_start_whitespace(text: &str) -> String {\n    let mut result = String::new();\n\n    for ch in text.chars() {\n        if ch.is_whitespace() {\n            result.push(ch)\n        } else {\n            break;\n        }\n    }\n\n    result\n}\n\npub fn get_hash(text: &str) -> u64 {\n    let mut hasher = DefaultHasher::new();\n    text.hash(&mut hasher);\n    hasher.finish()\n}\n\npub fn get_files(files: Values) -> Result<Vec<String>, io::Error> {\n    let mut result = Vec::new();\n\n    for file in files {\n        let meta = match fs::metadata(file) {\n            Ok(m) => m,\n            Err(e) => {\n                return Err(io::Error::new(\n                    e.kind(),\n                    format!(\"{file}: {}\", &e.to_string()),\n                ))\n            }\n        };\n        if meta.is_file() {\n            result.push(file.to_string());\n        } else {\n            for entry in WalkBuilder::new(file)\n                .hidden(false)\n                .build()\n                .filter_map(|e| e.ok())\n            {\n                let path = entry.path().display().to_string();\n                let meta = fs::metadata(entry.path()).unwrap();\n                if meta.is_file() && tree::get_parser_lang_from_filename(&path).is_some() {\n                    result.push(path);\n                }\n            }\n        }\n    }\n    result.sort();\n    result.dedup();\n\n    Ok(result)\n}\n\npub fn get_parser(filename: Option<&str>, parser: Option<&str>) -> Result<String, FormatError> {\n    if let Some(p) = parser {\n        return Ok(p.to_owned());\n    }\n    if let Some(f) = filename {\n        if let Some(p) = tree::get_parser_lang_from_filename(f) {\n            return Ok(p.to_owned());\n        }\n    }\n    Err(FormatError {\n        msg: \"Could not infer parser.\".to_string(),\n        filename: filename.map(|f| f.to_owned()),\n        command: None,\n        language: None,\n        start: None,\n    })\n}\n\npub fn find_closest_config() -> Option<String> {\n    let name = \".cbfmt.toml\";\n    let mut current_dir = match env::current_dir() {\n        Ok(c) => c,\n        Err(_) => return None,\n    };\n    loop {\n        let path = current_dir.join(name);\n\n        if path.exists() {\n            return Some(path.to_str()?.to_string());\n        }\n        match current_dir.parent() {\n            Some(p) => current_dir = p.to_path_buf(),\n            None => return None,\n        }\n    }\n}\n\npub fn print_ok(stdout: &mut StandardStream, text: &str) {\n    let mut color_spec = ColorSpec::new();\n    print!(\"[\");\n    stdout\n        .set_color(color_spec.set_fg(Some(Color::Green)).set_bold(true))\n        .unwrap();\n    print!(\"Okay\");\n    color_spec.clear();\n    stdout.set_color(&color_spec).unwrap();\n    println!(\"]: {text}\");\n}\n\npub fn print_unchanged(stdout: &mut StandardStream, text: &str) {\n    let mut color_spec = ColorSpec::new();\n    print!(\"[\");\n    stdout\n        .set_color(color_spec.set_fg(Some(Color::Blue)).set_bold(true))\n        .unwrap();\n    print!(\"Same\");\n    color_spec.clear();\n    stdout.set_color(&color_spec).unwrap();\n    println!(\"]: {text}\");\n}\n\npub fn print_fail(stderr: &mut StandardStream, text: &str) {\n    let mut color_spec = ColorSpec::new();\n    eprint!(\"[\");\n    stderr\n        .set_color(color_spec.set_fg(Some(Color::Yellow)).set_bold(true))\n        .unwrap();\n    eprint!(\"Fail\");\n    color_spec.clear();\n    stderr.set_color(&color_spec).unwrap();\n    eprintln!(\"]: {text}\");\n}\n\npub fn print_error(stderr: &mut StandardStream, text: &str) {\n    let mut color_spec = ColorSpec::new();\n    eprint!(\"[\");\n    stderr\n        .set_color(color_spec.set_fg(Some(Color::Red)).set_bold(true))\n        .unwrap();\n    eprint!(\"Error\");\n    color_spec.clear();\n    stderr.set_color(&color_spec).unwrap();\n    eprintln!(\"]: {text}\");\n}\n"
  }
]