プロジェクトと一体でコンテンツを提供するCLIツールを作った

2022/02/22に公開
xxxxx.md
---
title: タイトル
emoji: 🐒
type: tech
topics: []
published: false
---

コード↓

```rust:src/main.rs
1
```

例えばxxxxx.mdのファイル内容が上記のような場合で、projects/xxxxx/src/main.rsが以下の場合には...

main.rs
// 1
fn main() {
    println!("Hello, world!");
}
// -1

xxxxx.mdファイルが以下のように更新されます。

xxxxx.md
---
title: タイトル
emoji: 🐒
type: tech
topics: []
published: false
---

コード↓

```rust:src/main.rs
fn main() {
    println!("Hello, world!");
}
```

zenn-contentフォルダの構成

├─images
├─books
├─articles
│   └─xxxxx.md
└─projects
   └─xxxxx
      ├─src
      │  └─main.rs
      └─Cargo.toml

新しくprojectsというフォルダを作成し、この下に記事に使うソースコードを管理するプロジェクトを作成します。
この時作成するプロジェクトフォルダの名称は、記事と同じ名前にします。

コード

https://github.com/ogty/continuous-article-integration

main.rust
use std::env;

use regex::Regex;

mod modules;

fn main() {
    let args: Vec<String> = env::args().collect();
    let path: &str = &args[1];

    let article_path: &String = &format!("./articles/{}.md", path);
    let project_path: String = format!("./projects/{}", path);

    let re_code_snippet: Regex = Regex::new(r"```(?P<lang>\w+):(?P<path>.*)").unwrap();
    let re_number: Regex = Regex::new(r"^\d+$").unwrap();
    let mut article_line_count: usize = 0;
    let mut result: Vec<String> = Vec::new();

    let article_data: Vec<String> = modules::file::read_lines(&article_path);
    for line in &article_data {
        if re_number.is_match(line) {
            article_line_count += 1;
            continue;
        }

        result.push(line.clone());
        if let Some(captures) = re_code_snippet.captures(&line) {
            let path: &str = captures.name("path").unwrap().as_str();
            // let lang: &str = captures.name("lang").unwrap().as_str();
            let source_code_number: &String = &article_data[article_line_count + 1];
            let source_code_path: String = format!("{}/{}", project_path, path);
            let source_data: Vec<String> = modules::file::read_lines(&source_code_path);

            let mut source_code_count: usize = 0;
            for line in &source_data {
                if line == &format!("// {}", source_code_number) {
                    source_code_count += 1;
                    for mached_line in &source_data[source_code_count..] {
                        if mached_line == &format!("// -{}", source_code_number) {
                            break
                        }
                        result.push(mached_line.clone());
                    }
                }
                source_code_count += 1;
            }
        }
        article_line_count += 1;
    }
    modules::file::write(&article_path, result.join("\n"));
}
modules/file.rs
use std::fs::File;
use std::io::{prelude::*, self};
use std::path::{Path, Display};


pub fn write(path: &str, content: String) {
    let path: &Path = Path::new(&path);
    let display: Display = path.display();

    let mut file: File = match File::create(&path) {
        Err(why) => panic!("couldn't create {}: {}", display, why),
        Ok(file) => file,
    };

    match file.write_all(content.as_bytes()) {
        Err(why) => panic!("couldn't write to {}: {}", display, why),
        Ok(_) => (),
    }
}


pub fn read_lines(path: &str) -> Vec<String> {
    let mut result: Vec<String> = Vec::new();
    let file: File = File::open(path).unwrap();
    let tmp = io::BufReader::new(file).lines();
    for line in tmp {
        result.push(line.unwrap());
    }
    return result;
}
pub mod file;
Cargo.toml
[package]
name = "aci"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
+ regex = "1"

使い方

$ cargo build

target/debug/aci.exezenn-content直下に配置し、以下コマンドを実行。

$ aci <path>

Discussion