Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Test
.test/
57 changes: 50 additions & 7 deletions docs/dev/design/repo-rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,59 @@ Here's how the exact rule file is defined.
The repository rule file will be written to a TOML file, following the structure below:

```toml
# The article template to use when creating a new source file for an article.
article_template = """---
title: {{title}}
date: <发布时间>
author:
- fosscope-translation-team
- {{translator}}
- {{proofreader}}
banner: {{cover_image}}
cover: {{cover_image}}
categories:
- 翻译
- <类型>
tags:
- <标签>
authorInfo: |
via: {{via}}

作者:[{{author}}]({{author_link}})
选题:[{{selector}}](https://github.com/{{selector}})
译者:[{{translator}}](https://github.com/{{translator}})
校对:[{{proofreader}}](https://github.com/{{proofreader}})

本文由 [FOSScope翻译组](https://github.com/FOSScope/TranslateProject) 原创编译,[开源观察](https://fosscope.com/) 荣誉推出
---

<!-- 所有在被 `<>` 标记的地方都需要填写对应信息 -->

{{summary}}

<!-- more -->

{{content}}
"""


[[articles]]
# Each `[[articles]]` block defines a type of article available to contribute to.
type = "news" # The type of article.
description = "News Articles" # The description of the article type.
directory = "{step}/news" # The directory where the articles of this type are stored.
directory = "{{step}}/news" # The directory where the articles of this type are stored.
# `{step}` is the placeholder for the directory where the article will be moved from
# step to step (e.g. "source", "translated", "published", etc.)
# If needed, a specific article template can be defined for this article type.
# Otherwise, the default article template will be used.
# article_template = """
# """

# Multiple article types can be defined.
[[articles]]
type = "tech"
description = "Tech Articles"
directory = "{step}/tech"
directory = "{{step}}/tech"

# [[articles]]
# ...
Expand All @@ -41,15 +81,15 @@ directory = "{step}/tech"
# Each `[[actions]]` block defines an action that can be made in the contribution process.
action = "select" # The action name.
description = "Select an article to translate." # The description of the action.
command = "TOUCH source/{article}.md" # The command to execute when the action is made.
command = "TOUCH source/{{article_id}}.md" # The command to execute when the action is made.
# The command follows a *nix shell command syntax, but is defined, parsed, and executed by the core component of Toolkit software.
# In this case, {article} is the placeholder for the article name.

# Multiple actions can be defined.
[[actions]]
action = "translate"
description = "Translate the article."
command = "MV source/{article}.md translated/{article}.md"
command = "MV source/{{article_id}}.md translated/{{article_id}}.md"

# [[actions]]
# ...
Expand All @@ -58,9 +98,12 @@ command = "MV source/{article}.md translated/{article}.md"
# This section defines how git conventions applies in different steps.
# `{action}`, `{type}`, and `{article}` are placeholders for the action's name, article type, and article name respectively.
# Other placeholders can be used as well.
branch_naming = "{action}/{type}/{article}" # The branch naming rule.
commit_message = "[{action.desc}][{type.desc}]: {article.title}" # The commit message rule.
branch_naming = "{{action_name}}/{{type_name}}/{{article_id}}" # The branch naming rule.
commit_message = "[{{action_desc}}][{{type_desc}}]: {{article_title}}" # The commit message rule.
```

> [!NOTE]
> In general, placeholders like `{step}` can be used anywhere, and placerholders other than what's shown in the example above can be defined and used as well.
>
> In general, placeholders like `{{title}}` can be used anywhere, and the template engine will replace them with the actual value when generating the file.
>
> Place holder other than what's shown above may be defined and used.
66 changes: 66 additions & 0 deletions src-rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-rust/toolkit-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ octocrab = "0.38.0" # GitHub API
html2md = { workspace = true } # HTML to Markdown
libhtmlfilter = "0.1.2" # HTML Filter
openai_api_rust = "0.1.9" # OpenAI API
handlebars = "5.1.2" # Template Engine

[dev-dependencies]
wiremock = "0.6.0"
127 changes: 127 additions & 0 deletions src-rust/toolkit-core/src/models/action_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use std::collections::HashMap;
use handlebars::Handlebars;

#[derive(PartialEq, Eq, Debug)]
pub struct ActionCommand {
pub command: String,
pub args: Vec<String>,
}

impl<'de> serde::Deserialize<'de> for ActionCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let content = String::deserialize(deserializer)?;
let mut parts = content.split_whitespace();

let command = parts.next().unwrap().to_string();
let args = parts.map(|s| s.to_string()).collect();

Ok(Self {
command,
args,
})
}
}

impl ActionCommand {
pub fn new(command: String, args: Vec<String>) -> Self {
Self {
command,
args,
}
}

pub fn execute(&self, vars: Option<&HashMap<&str, &str>>) -> Result<(), &str> {
let mut args = self.args.clone();
if vars.is_some() {
let handlebars = Handlebars::new();
args = args.iter().map(
|arg| handlebars.render_template(&*arg, &vars).unwrap()
).collect();
}

match self.command.as_str() {
// Copy a file or a directory
"CP" => {
let src = args.get(0).unwrap();
let dest = args.get(1).unwrap();

let src_dir = std::path::Path::new(&src);
if src_dir.is_dir() {
let r = copy_dir_all(src, dest);
match r {
Ok(_) => Ok(()),
Err(_) => Err("Error copying directory"),
}
} else {
let dest_dir = std::path::Path::new(&dest);
if !dest_dir.exists() {
std::fs::create_dir_all(dest_dir.parent().unwrap()).unwrap();
}

std::fs::copy(src, dest).unwrap();
Ok(())
}
}
// Write a content to a file
"ECHO" => {
let path = args.get(0).unwrap();
let content = args.get(1).unwrap();
std::fs::write(path, content).unwrap();
Ok(())
}
// Create a directory
"MKDIR" => {
let path = args.get(0).unwrap();
std::fs::create_dir_all(path).unwrap();
Ok(())
}
// Move a file or a directory
"MV" => {
let src = args.get(0).unwrap();
let dest = args.get(1).unwrap();

let dest_dir = std::path::Path::new(dest);
if !dest_dir.exists() {
std::fs::create_dir_all(dest_dir.parent().unwrap()).unwrap();
}

std::fs::rename(src, dest).unwrap();
Ok(())
}
// Remove a file or a directory
"RM" => {
let path = args.get(0).unwrap();
if std::fs::metadata(path).unwrap().is_dir() {
std::fs::remove_dir_all(path).unwrap();
} else {
std::fs::remove_file(path).unwrap();
}
Ok(())
}
// Create a file
"TOUCH" => {
let path = args.get(0).unwrap();
std::fs::File::create(path).unwrap();
Ok(())
}
_ => Err("Unknown command"),
}
}
}

fn copy_dir_all(src: impl AsRef<std::path::Path>, dst: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::create_dir_all(&dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
1 change: 1 addition & 0 deletions src-rust/toolkit-core/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod github_repo;
pub mod repo_rule;
pub mod html_filter_rule;
pub mod action_command;
Loading