提交 2b0d213c 编写于 作者: F feilong

增加项目类型习题

上级 e677bd24
......@@ -9,13 +9,18 @@ import re
from parsec import BasicState, ParsecError
from .exercises.markdown import parse
from .exercises.init_exercises import emit_head, emit_answer, emit_options, simple_list_md_dump
from .exercises.init_exercises import (
emit_head,
emit_answer,
emit_options,
simple_list_md_dump,
)
id_set = set()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
......@@ -29,14 +34,12 @@ def search_author(author_dict, username):
def user_name(md_file, author_dict):
ret = subprocess.Popen([
"git", "log", md_file
], stdout=subprocess.PIPE)
ret = subprocess.Popen(["git", "log", md_file], stdout=subprocess.PIPE)
lines = list(map(lambda l: l.decode(), ret.stdout.readlines()))
author_lines = []
for line in lines:
if line.startswith('Author'):
author_lines.append(line.split(' ')[1])
if line.startswith("Author"):
author_lines.append(line.split(" ")[1])
if len(author_lines) == 0:
return None
author_nick_name = author_lines[-1]
......@@ -44,12 +47,11 @@ def user_name(md_file, author_dict):
def load_json(p):
with open(p, 'r', encoding="utf-8") as f:
with open(p, "r", encoding="utf-8") as f:
try:
return json.loads(f.read())
except UnicodeDecodeError:
logger.info(
"json 文件 [{p}] 编码错误,请确保其内容保存为 utf-8 或 base64 后的 ascii 格式。")
logger.info("json 文件 [{p}] 编码错误,请确保其内容保存为 utf-8 或 base64 后的 ascii 格式。")
def dump_json(p, j, exist_ok=False, override=False):
......@@ -61,18 +63,20 @@ def dump_json(p, j, exist_ok=False, override=False):
logger.error(f"{p} already exist")
sys.exit(0)
with open(p, 'w+', encoding="utf8") as f:
with open(p, "w+", encoding="utf8") as f:
f.write(json.dumps(j, indent=2, ensure_ascii=False))
def ensure_config(path):
config_path = os.path.join(path, "config.json")
if not os.path.exists(config_path):
node = {"keywords": [],
"keywords_must": [],
"keywords_forbid": [],
"group": 0,
"subtree": ""}
node = {
"keywords": [],
"keywords_must": [],
"keywords_forbid": [],
"group": 0,
"subtree": "",
}
dump_json(config_path, node, exist_ok=True, override=False)
return node
else:
......@@ -80,7 +84,7 @@ def ensure_config(path):
def parse_no_name(d):
p = r'(\d+)\.(.*)'
p = r"(\d+)\.(.*)"
m = re.search(p, d)
try:
......@@ -95,7 +99,7 @@ def parse_no_name(d):
def check_export(base, cfg):
flag = False
exports = []
for export in cfg.get('export', []):
for export in cfg.get("export", []):
ecfg_path = os.path.join(base, export)
if os.path.exists(ecfg_path):
exports.append(export)
......@@ -106,15 +110,53 @@ def check_export(base, cfg):
return flag
def read_project_markdown(file):
start_desc = False
start_project = False
desc = []
project = []
with open(file, "r") as f:
for line in f.readlines():
line = line.strip("\n")
if start_desc and line.strip() != "":
desc.append(line)
if start_project and line.strip() != "":
project.append(line)
if line == "# 项目说明":
start_desc = True
if line == "# 项目地址":
start_desc = False
start_project = True
print(desc)
print(project)
return "\n".join(desc), project[0].strip().replace("<", "").replace(">", "")
def walk_project_2_config(data_path):
for base, dirs, files in os.walk(data_path):
for file in files:
parts = file.split(".")
if parts[-1] == "md":
desc, project = read_project_markdown(os.path.join(base, file))
config_path = os.path.join(base, file.replace("md", "json"))
config = load_json(config_path)
config["project"] = project
config["desc"] = desc
dump_json(config_path, config, exist_ok=True, override=True)
class TreeWalker:
def __init__(
self, root,
tree_name,
title=None,
log=None,
authors=None,
enable_notebook=None,
ignore_keywords=False
self,
root,
tree_name,
title=None,
log=None,
authors=None,
enable_notebook=None,
ignore_keywords=False,
default_exercise_type="code_options",
):
self.ignore_keywords = ignore_keywords
self.authors = authors if authors else {}
......@@ -124,6 +166,7 @@ class TreeWalker:
self.title = tree_name if title is None else title
self.tree = {}
self.logger = logger if log is None else log
self.default_exercise_type = default_exercise_type
def walk(self):
root = self.load_root()
......@@ -134,7 +177,7 @@ class TreeWalker:
"keywords_must": root.get("keywords_must", []),
"keywords_forbid": root.get("keywords_forbid", []),
"group": root.get("group", 0),
"subtree": root.get("subtree", "")
"subtree": root.get("subtree", ""),
}
self.tree[root["tree_name"]] = root_node
self.load_levels(root_node)
......@@ -147,13 +190,13 @@ class TreeWalker:
for index, chapter in enumerate(level_node["children"]):
chapter_title = list(chapter.keys())[0]
chapter_node = list(chapter.values())[0]
chapter_path = os.path.join(
level_path, f"{index + 1}.{chapter_title}")
chapter_path = os.path.join(level_path, f"{index + 1}.{chapter_title}")
self.load_sections(chapter_path, chapter_node)
for index, section_node in enumerate(chapter_node["children"]):
section_title = list(section_node.keys())[0]
full_path = os.path.join(
chapter_path, f"{index + 1}.{section_title}")
chapter_path, f"{index + 1}.{section_title}"
)
if os.path.isdir(full_path):
self.check_section_keywords(full_path)
self.ensure_exercises(full_path)
......@@ -198,7 +241,7 @@ class TreeWalker:
"keywords_must": config.get("keywords_must", []),
"keywords_forbid": config.get("keywords_forbid", []),
"group": config.get("group", 0),
"subtree": config.get("subtree", "")
"subtree": config.get("subtree", ""),
}
}
......@@ -253,7 +296,7 @@ class TreeWalker:
"keywords_must": [],
"keywords_forbid": [],
"group": 0,
"subtree": ""
"subtree": "",
}
dump_json(config_path, config, exist_ok=True, override=True)
else:
......@@ -267,9 +310,7 @@ class TreeWalker:
def ensure_level_config(self, path):
config_path = os.path.join(path, "config.json")
if not os.path.exists(config_path):
config = {
"node_id": self.gen_node_id()
}
config = {"node_id": self.gen_node_id()}
dump_json(config_path, config, exist_ok=True, override=True)
else:
config = load_json(config_path)
......@@ -287,7 +328,7 @@ class TreeWalker:
"keywords_must": [],
"keywords_forbid": [],
"group": 0,
"subtree": ""
"subtree": "",
}
dump_json(config_path, config, exist_ok=True, override=True)
else:
......@@ -308,7 +349,7 @@ class TreeWalker:
"keywords_must": [],
"keywords_forbid": [],
"group": 0,
"subtree": ""
"subtree": "",
}
dump_json(config_path, config, exist_ok=True, override=True)
else:
......@@ -320,9 +361,11 @@ class TreeWalker:
def ensure_node_id(self, config):
flag = False
if "node_id" not in config or \
not config["node_id"].startswith(f"{self.name}-") or \
config["node_id"] in id_set:
if (
"node_id" not in config
or not config["node_id"].startswith(f"{self.name}-")
or config["node_id"] in id_set
):
new_id = self.gen_node_id()
id_set.add(new_id)
config["node_id"] = new_id
......@@ -360,7 +403,7 @@ class TreeWalker:
"keywords_must": config.get("keywords_must", []),
"keywords_forbid": config.get("keywords_forbid", []),
"group": config.get("group", 0),
"subtree": config.get("subtree", "")
"subtree": config.get("subtree", ""),
}
}
return num, result
......@@ -376,7 +419,7 @@ class TreeWalker:
"keywords_must": config.get("keywords_must", []),
"keywords_forbid": config.get("keywords_forbid", []),
"group": config.get("group", 0),
"subtree": config.get("subtree", "")
"subtree": config.get("subtree", ""),
}
}
# if "children" in config:
......@@ -409,7 +452,7 @@ class TreeWalker:
logger.error(f"习题 [{md_file}] 编码错误,请确保其保存为 utf-8 编码")
sys.exit(1)
if data.strip() == '':
if data.strip() == "":
md = []
emit_head(md)
emit_answer(md, None)
......@@ -426,16 +469,19 @@ class TreeWalker:
state = BasicState(data)
try:
doc = parse(state)
if meta["type"] == "code_options":
doc = parse(state)
else:
walk_project_2_config(self.root)
except ParsecError as err:
index = state.index
context = state.data[index - 15:index + 15]
context = state.data[index - 15 : index + 15]
logger.error(
f"习题 [{md_file}] 解析失败,在位置 {index} [{context}] 附近有格式: [{err}]")
f"习题 [{md_file}] 解析失败,在位置 {index} [{context}] 附近有格式: [{err}]"
)
if flag:
dump_json(os.path.join(section_path, "config.json"),
config, True, True)
dump_json(os.path.join(section_path, "config.json"), config, True, True)
for e in config.get("export", []):
full_name = os.path.join(section_path, e)
......@@ -463,18 +509,18 @@ class TreeWalker:
meta["source"] = source
if "author" not in meta:
meta["author"] = user_name(md_file, self.authors)
elif meta['author'] is None:
elif meta["author"] is None:
meta["author"] = user_name(md_file, self.authors)
if "type" not in meta:
meta["type"] = "code_options"
meta["type"] = self.default_exercise_type
if meta is None:
meta = {
"type": "code_options",
"type": self.default_exercise_type,
"author": user_name(md_file, self.authors),
"source": source,
"notebook_enable": self.default_notebook(),
"exercise_id": uuid.uuid4().hex
"exercise_id": uuid.uuid4().hex,
}
dump_json(meta_path, meta, True, True)
......@@ -499,11 +545,12 @@ class TreeWalker:
os.makedirs(data_root, exist_ok=True)
node_dirs = [
os.path.join(data_root, f'1.{self.title}初阶'),
os.path.join(data_root, f'2.{self.title}中阶'),
os.path.join(data_root, f'3.{self.title}高阶'),
os.path.join(data_root, f'1.{self.title}初阶',
f"1.{self.title}入门", f"1.HelloWorld"),
os.path.join(data_root, f"1.{self.title}初阶"),
os.path.join(data_root, f"2.{self.title}中阶"),
os.path.join(data_root, f"3.{self.title}高阶"),
os.path.join(
data_root, f"1.{self.title}初阶", f"1.{self.title}入门", f"1.HelloWorld"
),
]
for node_dir in node_dirs:
......@@ -513,187 +560,195 @@ class TreeWalker:
emit_head(md)
emit_answer(md, None)
emit_options(md, None)
simple_list_md_dump(os.path.join(
node_dirs[len(node_dirs) - 1], 'helloworld.md'), md)
simple_list_md_dump(
os.path.join(node_dirs[len(node_dirs) - 1], "helloworld.md"), md
)
self.walk()
self.init_readme()
with open('.gitignore', 'w', encoding='utf-8') as f:
f.write('\n'.join([
".vscode",
".idea",
".DS_Store",
"__pycache__",
"*.pyc",
"*.zip",
"*.out",
"bin/",
"debug/",
"release/",
]))
with open('requirements.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join([
"pre_commit",
"skill-tree-parser",
]))
with open(".gitignore", "w", encoding="utf-8") as f:
f.write(
"\n".join(
[
".vscode",
".idea",
".DS_Store",
"__pycache__",
"*.pyc",
"*.zip",
"*.out",
"bin/",
"debug/",
"release/",
]
)
)
with open("requirements.txt", "w", encoding="utf-8") as f:
f.write(
"\n".join(
[
"pre_commit",
"skill-tree-parser",
]
)
)
def init_readme(self):
md = [
f'# skill_tree_{self.name}',
f'',
f'`{self.title}技能树`是[技能森林](https://gitcode.net/csdn/skill_tree)的一部分。',
f'',
f'## 编辑环境初始化',
f'',
f'```',
f'pip install -r requirements.txt',
f'```',
f'',
f'## 目录结构说明',
f'技能树编辑仓库的 data 目录是主要的编辑目录,目录的结构是固定的',
f'',
f'* 技能树`骨架文件`:',
f' * 位置:`data/tree.json`',
f' * 说明:该文件是执行 `python main.py` 生成的,请勿人工编辑',
f'* 技能树`根节点`配置文件:',
f' * 位置:`data/config.json`',
f' * 说明:可编辑配置关键词等字段,其中 `node_id` 字段是生成的,请勿编辑',
f'* 技能树`难度节点`:',
f' * 位置:`data/xxx`,例如: `data/1.{self.title}初阶`',
f' * 说明:',
f' * 每个技能树有 3 个等级,目录前的序号是必要的,用来保持文件夹目录的顺序',
f' * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑',
f'* 技能树`章节点`:',
f' * 位置:`data/xxx/xxx`,例如:`data/1.{self.title}初阶/1.{self.title}简介`',
f' * 说明:',
f' * 每个技能树的每个难度等级有 n 个章节,目录前的序号是必要的,用来保持文件夹目录的顺序',
f' * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑',
f'* 技能树`知识节点`:',
f' * 位置:`data/xxx/xxx`,例如:`data/1.{self.title}初阶/1.{self.title}简介`',
f' * 说明:',
f' * 每个技能树的每章有 n 个知识节点,目录前的序号是必要的,用来保持文件夹目录的顺序',
f' * 每个目录下有一个 `config.json`',
f' * 其中 `node_id` 字段是生成的,请勿编辑',
f' * 其中 `keywords` 可配置关键字字段',
f' * 其中 `children` 可配置该`知识节点`下的子树结构信息,参考后面描述',
f' * 其中 `export` 可配置该`知识节点`下的导出习题信息,参考后面描述',
f'',
f'## `知识节点` 子树信息结构',
f'',
f'例如 `data/1.{self.title}初阶/1.{self.title}简介/1.HelloWorld/config.json` 里配置对该知识节点子树信息结构,这个配置是可选的:',
f'```json',
f'{{',
f' // ...',
f'',
f"# skill_tree_{self.name}",
f"",
f"`{self.title}技能树`是[技能森林](https://gitcode.net/csdn/skill_tree)的一部分。",
f"",
f"## 编辑环境初始化",
f"",
f"```",
f"pip install -r requirements.txt",
f"```",
f"",
f"## 目录结构说明",
f"技能树编辑仓库的 data 目录是主要的编辑目录,目录的结构是固定的",
f"",
f"* 技能树`骨架文件`:",
f" * 位置:`data/tree.json`",
f" * 说明:该文件是执行 `python main.py` 生成的,请勿人工编辑",
f"* 技能树`根节点`配置文件:",
f" * 位置:`data/config.json`",
f" * 说明:可编辑配置关键词等字段,其中 `node_id` 字段是生成的,请勿编辑",
f"* 技能树`难度节点`:",
f" * 位置:`data/xxx`,例如: `data/1.{self.title}初阶`",
f" * 说明:",
f" * 每个技能树有 3 个等级,目录前的序号是必要的,用来保持文件夹目录的顺序",
f" * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑",
f"* 技能树`章节点`:",
f" * 位置:`data/xxx/xxx`,例如:`data/1.{self.title}初阶/1.{self.title}简介`",
f" * 说明:",
f" * 每个技能树的每个难度等级有 n 个章节,目录前的序号是必要的,用来保持文件夹目录的顺序",
f" * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑",
f"* 技能树`知识节点`:",
f" * 位置:`data/xxx/xxx`,例如:`data/1.{self.title}初阶/1.{self.title}简介`",
f" * 说明:",
f" * 每个技能树的每章有 n 个知识节点,目录前的序号是必要的,用来保持文件夹目录的顺序",
f" * 每个目录下有一个 `config.json`",
f" * 其中 `node_id` 字段是生成的,请勿编辑",
f" * 其中 `keywords` 可配置关键字字段",
f" * 其中 `children` 可配置该`知识节点`下的子树结构信息,参考后面描述",
f" * 其中 `export` 可配置该`知识节点`下的导出习题信息,参考后面描述",
f"",
f"## `知识节点` 子树信息结构",
f"",
f"例如 `data/1.{self.title}初阶/1.{self.title}简介/1.HelloWorld/config.json` 里配置对该知识节点子树信息结构,这个配置是可选的:",
f"```json",
f"{{",
f" // ...",
f"",
f' "children": [',
f' {{',
f" {{",
f' "XX开发入门": {{',
f' "keywords": [',
f' "XX开发",',
f' ],',
f" ],",
f' "children": [],',
f' "keywords_must": [',
f' "XX"',
f' ],',
f" ],",
f' "keywords_forbid": []',
f' }}',
f' }}',
f' ],',
f'}}',
f'```',
f'',
f'## `知识节点` 的导出习题编辑',
f'',
f'例如 `data/1.{self.title}初阶/1.{self.title}简介/1.HelloWorld/config.json` 里配置对该知识节点导出的习题',
f'',
f'```json',
f'{{',
f' // ...',
f" }}",
f" }}",
f" ],",
f"}}",
f"```",
f"",
f"## `知识节点` 的导出习题编辑",
f"",
f"例如 `data/1.{self.title}初阶/1.{self.title}简介/1.HelloWorld/config.json` 里配置对该知识节点导出的习题",
f"",
f"```json",
f"{{",
f" // ...",
f' "export": [',
f' "helloworld.json"',
f' ]',
f'}}',
f'```',
f'',
f'helloworld.json 的格式如下:',
f'```bash',
f'{{',
f" ]",
f"}}",
f"```",
f"",
f"helloworld.json 的格式如下:",
f"```bash",
f"{{",
f' "type": "code_options",',
f' "author": "xxx",',
f' "source": "helloworld.md",',
f' "notebook_enable": false,',
f' "exercise_id": "xxx"',
f'}}',
f'```',
f'',
f'其中 ',
f"}}",
f"```",
f"",
f"其中 ",
f'* "type": "code_options" 表示是一个选择题',
f'* "author" 可以放作者的 CSDN id,',
f'* "source" 指向了习题 MarkDown文件',
f'* "notebook_enable" 目前都是false',
f'* "exercise_id" 是工具生成的,不填',
f'',
f'',
f'习题格式模版如下:',
f'',
f'````mardown',
f'# {{标题}}',
f'',
f'{{习题描述}}',
f'',
f'以下关于上述游戏代码说法[正确/错误]的是?',
f'',
f'## 答案',
f'',
f'{{目标选项}}',
f'',
f'## 选项',
f'',
f'### A',
f'',
f'{{混淆选项1}}',
f'',
f'### B',
f'',
f'{{混淆选项2}}',
f'',
f'### C',
f'',
f'{{混淆选项3}}',
f'',
f'````',
f'',
f'## 技能树合成',
f'',
f'在根目录下执行 `python main.py` 会合成技能树文件,合成的技能树文件: `data/tree.json`',
f'* 合成过程中,会自动检查每个目录下 `config.json` 里的 `node_id` 是否存在,不存在则生成',
f'* 合成过程中,会自动检查每个知识点目录下 `config.json` 里的 `export` 里导出的习题配置,检查是否存在`exercise_id` 字段,如果不存在则生成',
f'* 在 节 目录下根据需要,可以添加一些子目录用来测试代码。',
f'* 开始游戏入门技能树构建之旅,GoodLuck! ',
f'',
f'## FAQ',
f'',
f'**难度目录是固定的么?**',
f'',
f'1. data/xxx 目录下的子目录是固定的初/中/高三个难度等级目录',
f'',
f'**如何增加章目录?**',
f'',
f'1. 在VSCode里打开项目仓库',
f'2. 在对应的难度等级目录新建章目录,例如在 data/1.xxx初阶/ 下新建章文件夹,data/1.xxx初阶/1.yyy',
f'3. 在项目根目录下执行 python main.py 脚本,会自动生成章的配置文件 data/1.xxx初阶/1.yyy/config.json',
f'',
f'**如何增加节目录?**:',
f"",
f"",
f"习题格式模版如下:",
f"",
f"````mardown",
f"# {{标题}}",
f"",
f"{{习题描述}}",
f"",
f"以下关于上述游戏代码说法[正确/错误]的是?",
f"",
f"## 答案",
f"",
f"{{目标选项}}",
f"",
f"## 选项",
f"",
f"### A",
f"",
f"{{混淆选项1}}",
f"",
f"### B",
f"",
f"{{混淆选项2}}",
f"",
f"### C",
f"",
f"{{混淆选项3}}",
f"",
f"````",
f"",
f"## 技能树合成",
f"",
f"在根目录下执行 `python main.py` 会合成技能树文件,合成的技能树文件: `data/tree.json`",
f"* 合成过程中,会自动检查每个目录下 `config.json` 里的 `node_id` 是否存在,不存在则生成",
f"* 合成过程中,会自动检查每个知识点目录下 `config.json` 里的 `export` 里导出的习题配置,检查是否存在`exercise_id` 字段,如果不存在则生成",
f"* 在 节 目录下根据需要,可以添加一些子目录用来测试代码。",
f"* 开始游戏入门技能树构建之旅,GoodLuck! ",
f"",
f"## FAQ",
f"",
f"**难度目录是固定的么?**",
f"",
f"1. data/xxx 目录下的子目录是固定的初/中/高三个难度等级目录",
f"",
f"**如何增加章目录?**",
f"",
f"1. 在VSCode里打开项目仓库",
f"2. 在对应的难度等级目录新建章目录,例如在 data/1.xxx初阶/ 下新建章文件夹,data/1.xxx初阶/1.yyy",
f"3. 在项目根目录下执行 python main.py 脚本,会自动生成章的配置文件 data/1.xxx初阶/1.yyy/config.json",
f"",
f"**如何增加节目录?**:",
f'1. 直接在VSCode里创建文件夹,例如 "data/1.xxx初阶/1.yyy/2.zzz"',
f'2. 项目根目录下执行 python main.py 会自动为新增节创建配置文件 data/1.xxx初阶/1.yyy/2.zzz/config.json',
f'',
f'**如何在节下新增一个习题**:',
f"2. 项目根目录下执行 python main.py 会自动为新增节创建配置文件 data/1.xxx初阶/1.yyy/2.zzz/config.json",
f"",
f"**如何在节下新增一个习题**:",
f'3. 在"data/1.xxx初阶/1.yyy/2.zzz" 目录下添加一个 markdown 文件编辑,例如 yyy.md,按照习题markdown格式编辑习题。',
f'4. md编辑完后,可以再次执行 python main.py 会自动生成同名的 yyy.json,并将 yyy.json 添加到config.json 的export数组里。',
f'5. yyy.json里的author信息放作者 CSDN ID。',
f"4. md编辑完后,可以再次执行 python main.py 会自动生成同名的 yyy.json,并将 yyy.json 添加到config.json 的export数组里。",
f"5. yyy.json里的author信息放作者 CSDN ID。",
]
simple_list_md_dump('README.md', md)
simple_list_md_dump("README.md", md)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册