diff --git a/setup.py b/setup.py index 779748af5b52d5c25f5a608d1455f969b40fba0e..e9092c3037027ebc8873cc5273a809126e3f3872 100644 --- a/setup.py +++ b/setup.py @@ -15,10 +15,9 @@ setup(name="skill-tree-parser", author_email="liuxin@csdn.net", url="https://gitcode.net/csdn/skill_tree_parser", license="MIT", - packages=["csdn", "test"], + packages=["skill_tree"], package_dir={ - "skill_tree": "src/skill_tree", - "test": "src/tests" + "skill_tree": "src/skill_tree" }, install_requires=[ "pyparsec", @@ -33,5 +32,4 @@ setup(name="skill-tree-parser", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License" - ] - ) + ]) diff --git a/src/skill_tree/doc.py b/src/skill_tree/doc.py new file mode 100644 index 0000000000000000000000000000000000000000..18f7713777100f0b1b4cb2db0b7aa1a5cc12277b --- /dev/null +++ b/src/skill_tree/doc.py @@ -0,0 +1,63 @@ +import os +from sys import version +from .tree import load_json, dump_json + + +def simple_list_md_load(p): + with open(p, 'r', encoding='utf-8') as f: + lines = f.readlines() + result = [] + for line in lines: + item = line.strip('\n') + if item.startswith('* '): + item = item[2:] + result.append(item) + return result + + +class DocWalker(): + + def __init__(self, root) -> None: + self.root = root + + def walk(self): + root = self.root + root_config_path = os.path.join(root, 'config.json') + root_config = load_json(root_config_path) + doc_path = os.path.join(root, 'doc.json') + versions = [] + for version_dir in root_config['versions']: + version_full_dir = os.path.join(root, version_dir) + version_config_path = os.path.join(version_full_dir, 'config.json') + if os.path.exists(version_config_path): + version_config = load_json(version_config_path) + + for benchmark in version_config['benchmarks']: + username = benchmark['user_name'] + benchmark['askme'] = f'https://ask.csdn.net/new?expertName={username}' + + asserts_path = os.path.join( + version_full_dir, + version_config['asserts'] + ) + version_config['asserts'] = load_json(asserts_path) + + bug_fixes_path = os.path.join( + version_full_dir, + version_config['bugfixes'] + ) + version_config['bugfixes'] = simple_list_md_load( + bug_fixes_path) + + features_path = os.path.join( + version_full_dir, + version_config['features'] + ) + + parts = version_full_dir.split("/") + version_config['version'] = parts[len(parts)-1] + version_config['features'] = simple_list_md_load(features_path) + versions.append(version_config) + + root_config['versions'] = versions + dump_json(doc_path, root_config, True, True) diff --git a/src/skill_tree/excercises/markdown.py b/src/skill_tree/excercises/markdown.py index c8d568e8f620ff3e6ab3472eaa5510e8019ed848..162843b536277d3ed21adf11ef8bede855ac379d 100644 --- a/src/skill_tree/excercises/markdown.py +++ b/src/skill_tree/excercises/markdown.py @@ -321,6 +321,10 @@ def parse(state): maybe_spaces(state) except ParsecError as err: result = Exercise(t, answer, description, options) + if tmpl is None: + tmpl = template(state) + if aop is None: + aop = aop_parser(state) if tmpl is not None: result.template = tmpl if aop is not None: diff --git a/src/skill_tree/img.py b/src/skill_tree/img.py new file mode 100644 index 0000000000000000000000000000000000000000..e9b2f70c76fee85e0266c64a43417904f7d661bd --- /dev/null +++ b/src/skill_tree/img.py @@ -0,0 +1,41 @@ +import os +from sys import path, version +from types import new_class +from .tree import load_json, dump_json + + +def simple_list_md_load(p): + with open(p, 'r', encoding='utf-8') as f: + lines = f.readlines() + result = [] + for line in lines: + item = line.strip('\n') + result.append(item) + return result + + +def simple_list_md_dump(p, lines): + with open(p, 'w', encoding='utf-8') as f: + f.write('\n'.join(lines)) + + +class ImgWalker(): + + def __init__(self, root) -> None: + self.root = root + + def walk(self): + for base, dirs, files in os.walk(self.root): + for file in files: + if file[-3:] == '.md': + md_file = os.path.join(base, file) + md_lines = simple_list_md_load(md_file) + md_new = [] + for line in md_lines: + new_line = line.replace( + '![](./', f'![](https://gitcode.net/csdn/skill_tree_opencv/-/raw/master/{base}/') + md_new.append(new_line) + md_new.append('') + simple_list_md_dump(md_file, md_new) + # import sys + # sys.exit(0) diff --git a/test/__init__.py b/src/skill_tree/template/__init__.py similarity index 100% rename from test/__init__.py rename to src/skill_tree/template/__init__.py diff --git a/src/skill_tree/template/main.py b/src/skill_tree/template/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b67a9b16cd10ec15ebfd2d3721827926cc0e4fc7 --- /dev/null +++ b/src/skill_tree/template/main.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + + +tmpl = """ +from skill_tree.tree import TreeWalker +from skill_tree.doc import DocWalker +from skill_tree.img import ImgWalker + +# authors' format is { +# "name1": ["nickname1", "nickname2"], +# "name2": ["nickname3", "nickname4"], +# } + +if __name__ == '__main__': + walker = TreeWalker( + "data", "opencv", "OpenCV", + ignore_keywords=True, + enable_notebook=False, + authors=$authors + ) + walker.walk() + + doc = DocWalker('doc') + doc.walk() + + img = ImgWalker('data') + img.walk() +""" \ No newline at end of file diff --git a/src/skill_tree/tree.py b/src/skill_tree/tree.py index 28231141221751d6f8af4c95ecfd18c59791e6d7..29e598bcdc727cb9e8ed9fca1d8e6279ba834d2f 100644 --- a/src/skill_tree/tree.py +++ b/src/skill_tree/tree.py @@ -1,11 +1,10 @@ import json import logging import os -import re +import subprocess import sys import uuid - -import git +import re id_set = set() logger = logging.getLogger(__name__) @@ -14,20 +13,32 @@ handler = logging.StreamHandler(sys.stdout) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) -repo = git.Repo(".") -def user_name(): - return repo.config_reader().get_value("user", "name") +def search_author(author_dict, username): + for key in author_dict: + names = author_dict[key] + if username in names: + return key + return username -def read_text(filepath): - with open(filepath, 'r', encoding='utf-8') as f: - return f.read() +def user_name(md_file, author_dict): + 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]) + author_nick_name = author_lines[-1] + return search_author(author_dict, author_nick_name) def load_json(p): - return json.loads(read_text(p)) + with open(p, 'r', encoding="utf-8") as f: + return json.loads(f.read()) def dump_json(p, j, exist_ok=False, override=False): @@ -81,7 +92,18 @@ def check_export(base, cfg): class TreeWalker: - def __init__(self, root, tree_name, title=None, log=None): + def __init__( + self, root, + tree_name, + title=None, + log=None, + authors=None, + enable_notebook=None, + ignore_keywords=False + ): + self.ignore_keywords = ignore_keywords + self.authors = authors if authors else {} + self.enable_notebook = enable_notebook self.name = tree_name self.root = root self.title = tree_name if title is None else title @@ -93,7 +115,9 @@ class TreeWalker: root_node = { "node_id": root["node_id"], "keywords": root["keywords"], - "children": [] + "children": [], + "keywords_must": root["keywords_must"], + "keywords_forbid": root["keywords_forbid"] } self.tree[root["tree_name"]] = root_node self.load_levels(root_node) @@ -106,11 +130,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}") + full_path = os.path.join( + chapter_path, f"{index + 1}.{section_title}") if os.path.isdir(full_path): self.check_section_keywords(full_path) self.ensure_exercises(full_path) @@ -146,6 +172,8 @@ class TreeWalker: "node_id": config["node_id"], "keywords": config["keywords"], "children": [], + "keywords_must": config["keywords_must"], + "keywords_forbid": config["keywords_forbid"] } } @@ -183,7 +211,7 @@ class TreeWalker: posted = os.path.join(base, f"{index + 1}.{title}") if origin != posted: self.logger.info(f"rename [{origin}] to [{posted}]") - os.rename(origin, posted) + os.rename(origin, posted) return children def ensure_chapters(self): @@ -197,6 +225,8 @@ class TreeWalker: "tree_name": self.name, "keywords": [], "node_id": self.gen_node_id(), + "keywords_must": [], + "keywords_forbid": [] } dump_json(config_path, config, exist_ok=True, override=True) else: @@ -226,7 +256,9 @@ class TreeWalker: if not os.path.exists(config_path): config = { "node_id": self.gen_node_id(), - "keywords": [] + "keywords": [], + "keywords_must": [], + "keywords_forbid": [] } dump_json(config_path, config, exist_ok=True, override=True) else: @@ -292,6 +324,8 @@ class TreeWalker: "node_id": config["node_id"], "keywords": config["keywords"], "children": [], + "keywords_must": config["keywords_must"], + "keywords_forbid": config["keywords_forbid"] } } return num, result @@ -303,7 +337,9 @@ class TreeWalker: name: { "node_id": config["node_id"], "keywords": config["keywords"], - "children": config.get("children", []) + "children": config.get("children", []), + "keywords_must": config["keywords_must"], + "keywords_forbid": config["keywords_forbid"] } } # if "children" in config: @@ -320,7 +356,8 @@ class TreeWalker: continue mfile = base + ".json" meta_path = os.path.join(section_path, mfile) - self.ensure_exercises_meta(meta_path, source) + md_file = os.path.join(section_path, e) + self.ensure_exercises_meta(meta_path, source, md_file) export = config.get("export", []) if mfile not in export and self.name != "algorithm": export.append(mfile) @@ -328,23 +365,25 @@ class TreeWalker: config["export"] = export 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) exercise = load_json(full_name) - if not exercise.get("exercise_id") or exercise.get("exercise_id") in id_set: + if "exercise_id" not in exercise or exercise.get("exercise_id") in id_set: eid = uuid.uuid4().hex exercise["exercise_id"] = eid dump_json(full_name, exercise, True, True) else: id_set.add(exercise["exercise_id"]) - def ensure_exercises_meta(self, meta_path, source): + def ensure_exercises_meta(self, meta_path, source, md_file): _, mfile = os.path.split(meta_path) meta = None if os.path.exists(meta_path): - content = read_text(meta_path) + with open(meta_path) as f: + content = f.read() if content: meta = json.loads(content) if "exercise_id" not in meta: @@ -354,26 +393,31 @@ class TreeWalker: if "source" not in meta: meta["source"] = source if "author" not in meta: - meta["author"] = user_name() + meta["author"] = user_name(md_file, self.authors) if "type" not in meta: meta["type"] = "code_options" - if meta is None: - meta = { - "type": "code_options", - "author": user_name(), - "source": source, - "notebook_enable": self.default_notebook(), - "exercise_id": uuid.uuid4().hex - } + + if meta is None: + meta = { + "type": "code_options", + "author": user_name(md_file, self.authors), + "source": source, + "notebook_enable": self.default_notebook(), + "exercise_id": uuid.uuid4().hex + } dump_json(meta_path, meta, True, True) def default_notebook(self): + if self.enable_notebook is not None: + return self.enable_notebook if self.name in ["python", "java", "c"]: return True else: return False def check_section_keywords(self, full_path): + if self.ignore_keywords: + return config = self.ensure_section_config(full_path) if not config.get("keywords", []): self.logger.error(f"节点 [{full_path}] 的关键字为空,请修改配置文件写入关键字") diff --git a/test/exercises/__init__.py b/src/test/__init__.py similarity index 100% rename from test/exercises/__init__.py rename to src/test/__init__.py diff --git a/src/skill_tree/parser.py b/src/test/exercises/__init__.py similarity index 100% rename from src/skill_tree/parser.py rename to src/test/exercises/__init__.py diff --git a/src/test/exercises/markdown_test.py b/src/test/exercises/markdown_test.py new file mode 100644 index 0000000000000000000000000000000000000000..1352d12832de278ce5e69f99ed2ad2f3a4a3ec01 --- /dev/null +++ b/src/test/exercises/markdown_test.py @@ -0,0 +1,217 @@ +import unittest + +from parsec.state import BasicState + +from skill_tree.excercises.market_math import processor +import skill_tree.excercises.markdown as mk + + +def math_processor(context): + """ math(str)->str +对文本内容预处理,将公式标记为前端可展示的 html。 + """ + md = context + new_md = [] + math_ctx = { + "enter": False, + "chars": [] + } + + count = len(md) + i = 0 + while i < count: + c = md[i] + if c == '$': + if math_ctx['enter']: + j = 0 + chars = math_ctx['chars'] + length = len(chars) + while j < length: + cc = chars[j] + if cc == '_': + next_c = chars[j + 1] + if next_c == '{': + subs = [] + cursor = 2 + next_c = chars[j + cursor] + while next_c != '}': + subs.append(next_c) + cursor += 1 + next_c = chars[j + cursor] + + sub = ''.join(subs) + new_md.append(f'{sub}') + j += cursor + else: + new_md.append(f'{next_c}') + j += 1 + elif cc == '^': + next_c = chars[j + 1] + if next_c == '{': + subs = [] + cursor = 2 + next_c = chars[j + cursor] + while next_c != '}': + subs.append(next_c) + cursor += 1 + next_c = chars[j + cursor] + + sub = ''.join(subs) + new_md.append(f'{sub}') + j += cursor + else: + new_md.append(f'{next_c}') + j += 1 + else: + new_md.append(cc) + j += 1 + + math_ctx['enter'] = False + math_ctx['chars'] = [] + else: + math_ctx['enter'] = True + math_ctx['chars'] = [] + else: + if math_ctx['enter']: + math_ctx['chars'].append(c) + else: + new_md.append(c) + i += 1 + return "".join(new_md) + +data = """ +# Hello World + +以下 `Hello World` 程序中,能够正确输出内容的是: + +## 答案 + +```java +package app; + +public class App { + public static void main(String[] args){ + System.out.println("Hello World"); + } +} +``` + +## 选项 + +### B + +```java +package app; + +public class App { + public int main(){ + System.out.printf("Hello World"); + return 0; + } +} +``` + +### C + +```java +package app; + +public class App { + public static void main(String[] args){ + println("Hello World"); + } +} +``` + +### D + +```java +package app; +import stdout + +public class App { + public int main(){ + print("Hello World\n"); + return 0; + } +} + +``` + +""" + + +class MarkdownTestCase(unittest.TestCase): + def test_basic(self): + state = BasicState(data.strip()) + doc = mk.parse(state) + self.assertEqual(doc.title, "Hello World") + self.assertEqual(len(doc.options), 3) + self.assertEqual(doc.description[0].source.strip(), """以下 `Hello World` 程序中,能够正确输出内容的是:""") + self.assertEqual(doc.answer[0].language, "java") + self.assertEqual(doc.answer[0].source, """package app; + +public class App { + public static void main(String[] args){ + System.out.println("Hello World"); + } +}""") + self.assertEqual(doc.options[0].paras[0].language, "java") + self.assertEqual(doc.options[0].paras[0].source, """package app; + +public class App { + public int main(){ + System.out.printf("Hello World"); + return 0; + } +}""") + self.assertEqual(doc.options[1].paras[0].language, "java") + self.assertEqual(doc.options[1].paras[0].source, """package app; + +public class App { + public static void main(String[] args){ + println("Hello World"); + } +}""") + self.assertEqual(doc.options[2].paras[0].language, "java") + self.assertEqual(doc.options[2].paras[0].source, """package app; +import stdout + +public class App { + public int main(){ + print("Hello World\n"); + return 0; + } +} +""") + + +class MathTestCase(unittest.TestCase): + + def test_parse(self): + data = "$e^{pi}$" + result = processor(data) + self.assertEqual(result, math_processor(data)) + + def test_pack(self): + data = "ploy is $e^{pi}$ in plain text" + result = processor(data) + self.assertEqual(result, math_processor(data)) + + def test_simple(self): + data = "ploy is $x_0$ in plain text" + result = processor(data) + self.assertEqual(result, math_processor(data)) + + def test_left(self): + data = "$x_0$ at start of plain text" + result = processor(data) + self.assertEqual(result, math_processor(data)) + + def test_right(self): + data = "ploy is $x_0$" + result = processor(data) + self.assertEqual(result, math_processor(data)) + + + diff --git a/src/test/skill_tree/__init__.py b/src/test/skill_tree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/exercises/markdown.py b/test/exercises/markdown.py deleted file mode 100644 index 6addc0d467ea804f9e587fd00516b1f8632cb782..0000000000000000000000000000000000000000 --- a/test/exercises/markdown.py +++ /dev/null @@ -1,72 +0,0 @@ -def math_processor(context): - """ math(str)->str -对文本内容预处理,将公式标记为前端可展示的 html。 - """ - md = context - new_md = [] - math_ctx = { - "enter": False, - "chars": [] - } - - count = len(md) - i = 0 - while i < count: - c = md[i] - if c == '$': - if math_ctx['enter']: - j = 0 - chars = math_ctx['chars'] - length = len(chars) - while j < length: - cc = chars[j] - if cc == '_': - next_c = chars[j + 1] - if next_c == '{': - subs = [] - cursor = 2 - next_c = chars[j + cursor] - while next_c != '}': - subs.append(next_c) - cursor += 1 - next_c = chars[j + cursor] - - sub = ''.join(subs) - new_md.append(f'{sub}') - j += cursor - else: - new_md.append(f'{next_c}') - j += 1 - elif cc == '^': - next_c = chars[j + 1] - if next_c == '{': - subs = [] - cursor = 2 - next_c = chars[j + cursor] - while next_c != '}': - subs.append(next_c) - cursor += 1 - next_c = chars[j + cursor] - - sub = ''.join(subs) - new_md.append(f'{sub}') - j += cursor - else: - new_md.append(f'{next_c}') - j += 1 - else: - new_md.append(cc) - j += 1 - - math_ctx['enter'] = False - math_ctx['chars'] = [] - else: - math_ctx['enter'] = True - math_ctx['chars'] = [] - else: - if math_ctx['enter']: - math_ctx['chars'].append(c) - else: - new_md.append(c) - i += 1 - return "".join(new_md)