diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5a7e2b50a27aa20f2ab6b427cc779a02c13fa996 --- /dev/null +++ b/.gitignore @@ -0,0 +1,403 @@ +.vscode +.idea +.DS_Store +__pycache__ +*.pyc +*.zip +*.out + + +# Created by https://www.toptal.com/developers/gitignore/api/csharp +# Edit at https://www.toptal.com/developers/gitignore?templates=csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +# nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml + +# End of https://www.toptal.com/developers/gitignore/api/csharp \ No newline at end of file diff --git a/README.md b/README.md index ba11353ad7780d1eeef092df0d5b62f6bda9f040..cac038a62046dbd0aa2e1674cf4662dfe2704c85 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,180 @@ # skill_tree_csharp -C# 技能树 \ No newline at end of file +C# 技能树 + +## 初始化技能树 + +技能树合成和id生成脚本目前用Python脚本统一处理 + +```bash +pip install -r requirement.txt +``` + + +## 目录结构说明 + +data目录下包含 难度节点/章节点/知识节点 3级目录结构 + +* 技能树`骨架文件`: + * 位置:`data/tree.json` + * 说明:该文件是执行 `python main.py` 生成的,请勿人工编辑 +* 技能树`根节点`配置文件: + * 位置:`data/config.json` + * 说明:可编辑配置关键词等字段,其中 `node_id` 字段是生成的,请勿编辑 +* 技能树`难度节点`: + * 位置:`data/xxx`,例如: `data/1.csharp初阶` + * 说明: + * 每个技能树有 3 个等级,目录前的序号是**必要**的,用来保持文件夹目录的顺序 + * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑 +* 技能树`章节点`: + * 位置:`data/xxx/xxx`,例如:`data/1.csharp初阶/1.预备知识` + * 说明: + * 每个技能树的每个难度等级有 n 个章节,目录前的序号是**必要**的,用来保持文件夹目录的顺序 + * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑 +* 技能树`知识节点`: + * 位置:`data/xxx/xxx`,例如:`data/1.csharp初阶/1.预备知识/1.csharp简介` + * 说明: + * 每个技能树的每章有 n 个知识节点,目录前的序号是必要的,用来保持文件夹目录的顺序 + * 每个目录下有一个 `config.json` + * 其中 `node_id` 字段是生成的,请勿编辑 + * 其中 `keywords` 可配置关键字字段 + * 其中 `children` 可配置该`知识节点`下的子树结构信息,参考后面描述 + * 其中 `export` 可配置该`知识节点`下的导出习题信息,参考后面描述 + + +## `知识节点` 子树信息结构 + +例如 `data/1.csharp初阶/1.预备知识/1.csharp简介/config.json` 里配置对该知识节点子树信息结构,用来增加技能树服务在该知识节点上的深度数据匹配: + +```json +{ + // ... + + "children": [ + { + "csharp的起源": { + "keywords": [ + "c#的起源", + "起源", + "c#" + ], + "children": [] + } + } + ], +} +``` + + +## `知识节点` 的导出习题编辑 + +例如 `data/1.csharp初阶/1.预备知识/1.csharp简介/config.json` 里配置对该知识节点导出的习题 + +```json +{ + // ... + "export": [ + "helloworld.json", + // ... + ] +} +``` + +在 `export` 字段中,我们列出习题定义的`json`文件列表 ,下面我们了解如何编写习题。 + +## `知识节点` 的导出习题选项配置编辑 + +目前我们支持使用 markdown 语法直接编辑习题和各选项。 + +如前文内容,我们在知识节点下增加习题 `helloworld`的定义文件,即在`data/1.csharp初阶/1.预备知识/1.csharp简介` 目录增加一个`helloworld.json`文件: + +```json +{ + "type": "code_options", + "author": "幻灰龙", + "source": "helloworld.md", + "notebook_enable": true +} +``` + +其中 + +* `type` 字段目前都固定是 `code_options`, +* `notebook_enable` 字段决定这个习题是否生成对应的 `notebook` +* `source` 字段代表习题编辑的 `markdwon` 文件。 + +现在我们新建一个 `helloworld.md` 并编辑为: + +````markdown +# Hello World + +编写一个输出 "Hello,World!" 的 C# 程序,以下错误的是? + +## 答案 + +```csharp +using System; +public class Program{ + public static void Main(){ + String str1 = "Hello,"; + String str2 = "World!"; + Console.WriteLine("str1"+"str2"); + } +} +``` + +## 选项 + +### 直接打印 + +```csharp +using System; +public class Program{ + public static void Main(){ + Console.WriteLine("Hello,World!"); + } +} +``` + +### 两个字符串拼接 + +```csharp +using System; +public class Program{ + public static void Main(){ + String str1 = "Hello,"; + String str2 = "World!"; + Console.WriteLine(str1+str2); + } +} +``` + +### 使用 var 关键字 + +```csharp +using System; +public class Program{ + public static void Main(){ + var str1 = "Hello,"; + var str2 = "World!"; + Console.WriteLine(str1+str2); + } +} +``` +```` + +这是一个最基本的习题结构,它包含标题、答案、选项,注意这几个一级和二级标题必须填写正确,解释器会读取这几个标题。而选项的标题会被直接忽略掉,在 +最终生成的习题中不包含选项的三级标题,所以这个标题可以用来标注一些编辑信息,例如“使用 var 关键字”,“两个字符串拼接”等等。 + +## 可选的习题源代码项目 + +编辑习题中,为了测试方便,可以直接在3级知识节点目录下创建对应的习题代码子目录 + + + +## 技能树合成 + +在根目录下执行 `python main.py` 会合成技能树文件,合成的技能树文件: `data/tree.json` + +* 合成过程中,会自动检查每个目录下 `config.json` 里的 `node_id` 是否存在,不存在则生成 +* 合成过程中,会自动检查每个知识点目录下 `config.json` 里的 `export` 里导出的习题配置,检查是否存在`exercise_id` 字段,如果不存在则生成 diff --git "a/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/config.json" "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..5a0b3a3afcb91b19dc1dbf10ed9e007150da0dd5 --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/config.json" @@ -0,0 +1,12 @@ +{ + "node_id": "csharp-37f9119660e445a48b065052b38c7687", + "keywords": [ + "c#的起源", + "起源", + "c#" + ], + "children": [], + "export": [ + "helloworld.json" + ] +} \ No newline at end of file diff --git "a/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/helloworld.json" "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/helloworld.json" new file mode 100644 index 0000000000000000000000000000000000000000..8bb392f99f3c9a42e20281f66781cffb45750e95 --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/helloworld.json" @@ -0,0 +1,6 @@ +{ + "type": "code_options", + "author": "幻灰龙", + "source": "helloworld.md", + "notebook_enable": true +} \ No newline at end of file diff --git "a/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/helloworld.md" "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/helloworld.md" new file mode 100644 index 0000000000000000000000000000000000000000..de9121787a221f40058f09c033c057860fe211ce --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/1.csharp\347\256\200\344\273\213/helloworld.md" @@ -0,0 +1,55 @@ +# Hello World + +编写一个输出 "Hello,World!" 的 C# 程序,以下错误的是? + +## 答案 + +```csharp +using System; +public class Program{ + public static void Main(){ + String str1 = "Hello,"; + String str2 = "World!"; + Console.WriteLine("str1"+"str2"); + } +} +``` + +## 选项 + +### 直接打印 + +```csharp +using System; +public class Program{ + public static void Main(){ + Console.WriteLine("Hello,World!"); + } +} +``` + +### 两个字符串拼接 + +```csharp +using System; +public class Program{ + public static void Main(){ + String str1 = "Hello,"; + String str2 = "World!"; + Console.WriteLine(str1+str2); + } +} +``` + +### 使用 var 关键字 + +```csharp +using System; +public class Program{ + public static void Main(){ + var str1 = "Hello,"; + var str2 = "World!"; + Console.WriteLine(str1+str2); + } +} +``` \ No newline at end of file diff --git "a/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/2.\345\256\211\350\243\205.NET/config.json" "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/2.\345\256\211\350\243\205.NET/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..a66934ba770591cffdd99d5a25b04461925a443a --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/2.\345\256\211\350\243\205.NET/config.json" @@ -0,0 +1,6 @@ +{ + "node_id": "csharp-ab27adf6911e4530bd1716526c12781c", + "keywords": [], + "children": [], + "export": [] +} \ No newline at end of file diff --git "a/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/config.json" "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..c765758c8b3b21b464075049fc817bf7dd0937b5 --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/1.\351\242\204\345\244\207\347\237\245\350\257\206/config.json" @@ -0,0 +1,4 @@ +{ + "node_id": "csharp-08a7251782494105ba037cc4f59ddf0c", + "keywords": [] +} \ No newline at end of file diff --git "a/data/1.csharp\345\210\235\351\230\266/2.\345\237\272\347\241\200\350\257\255\346\263\225/config.json" "b/data/1.csharp\345\210\235\351\230\266/2.\345\237\272\347\241\200\350\257\255\346\263\225/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..72666ed9d9ccfd4ca94a8f4137aaead83a25834d --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/2.\345\237\272\347\241\200\350\257\255\346\263\225/config.json" @@ -0,0 +1,4 @@ +{ + "node_id": "csharp-78ef60a83e3a45dca08ded3b6e9e806a", + "keywords": [] +} \ No newline at end of file diff --git "a/data/1.csharp\345\210\235\351\230\266/config.json" "b/data/1.csharp\345\210\235\351\230\266/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..c620655a77c9c5f548f784ef10bf14807acaa436 --- /dev/null +++ "b/data/1.csharp\345\210\235\351\230\266/config.json" @@ -0,0 +1,4 @@ +{ + "node_id": "csharp-0d59ee87a1c24dec86cd6174eda22d8e", + "keywords": [] +} \ No newline at end of file diff --git "a/data/2.csharp\344\270\255\351\230\266/config.json" "b/data/2.csharp\344\270\255\351\230\266/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..799e0b9cd32aee99a554a47f53bec2e4a648bb74 --- /dev/null +++ "b/data/2.csharp\344\270\255\351\230\266/config.json" @@ -0,0 +1,4 @@ +{ + "node_id": "csharp-e5ec4e73aa7842fda7fc5e2a3523373e", + "keywords": [] +} \ No newline at end of file diff --git "a/data/3.csharp\351\253\230\351\230\266/config.json" "b/data/3.csharp\351\253\230\351\230\266/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..6465aeea8c52acec9f3844f45cf2ad64c2bf5796 --- /dev/null +++ "b/data/3.csharp\351\253\230\351\230\266/config.json" @@ -0,0 +1,4 @@ +{ + "node_id": "csharp-7767ed91286e423fafc5c4637935199c", + "keywords": [] +} \ No newline at end of file diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000000000000000000000000000000000000..66ee44e71d9c1965f53c0c5a07ef15b9e101beb3 --- /dev/null +++ b/data/config.json @@ -0,0 +1,5 @@ +{ + "tree_name": "csharp", + "keywords": [], + "node_id": "csharp-0991d7a5f9a242e4983005d8ff53b086" +} \ No newline at end of file diff --git a/data/tree.json b/data/tree.json new file mode 100644 index 0000000000000000000000000000000000000000..078f00c9f52e363aaa521025cd00fbabe16ddd5e --- /dev/null +++ b/data/tree.json @@ -0,0 +1,59 @@ +{ + "csharp": { + "node_id": "csharp-0991d7a5f9a242e4983005d8ff53b086", + "keywords": [], + "children": [ + { + "csharp初阶": { + "node_id": "csharp-0d59ee87a1c24dec86cd6174eda22d8e", + "keywords": [], + "children": [ + { + "预备知识": { + "node_id": "csharp-08a7251782494105ba037cc4f59ddf0c", + "keywords": [], + "children": [ + { + "简介": { + "node_id": "csharp-37f9119660e445a48b065052b38c7687", + "keywords": [], + "children": [] + } + }, + { + "安装": { + "node_id": "csharp-ab27adf6911e4530bd1716526c12781c", + "keywords": [], + "children": [] + } + } + ] + } + }, + { + "基础语法": { + "node_id": "csharp-78ef60a83e3a45dca08ded3b6e9e806a", + "keywords": [], + "children": [] + } + } + ] + } + }, + { + "csharp中阶": { + "node_id": "csharp-e5ec4e73aa7842fda7fc5e2a3523373e", + "keywords": [], + "children": [] + } + }, + { + "csharp高阶": { + "node_id": "csharp-7767ed91286e423fafc5c4637935199c", + "keywords": [], + "children": [] + } + } + ] + } +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..d1e7e0e4cb65678325dacac2e7639386daa70937 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from src.tree import TreeWalker + +if __name__ == '__main__': + walker = TreeWalker("data", "csharp", "csharp") + walker.walk() diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000000000000000000000000000000000000..4336eaa0112dd5e9d95373c505d38d989e2ef667 --- /dev/null +++ b/requirement.txt @@ -0,0 +1 @@ +uuid==1.30 \ No newline at end of file diff --git a/src/tree.py b/src/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..95e1cbc5812810e30465eccc2632464caa5438e7 --- /dev/null +++ b/src/tree.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +import logging +from genericpath import exists +import json +import os +import uuid +import sys +import re + +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') +handler.setFormatter(formatter) +logger.addHandler(handler) + + +def load_json(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): + if os.path.exists(p): + if exist_ok: + if not override: + return + else: + logger.error(f"{p} already exist") + sys.exit(0) + + with open(p, 'w+', encoding='utf-8') 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": []} + dump_json(config_path, node, exist_ok=True, override=False) + return node + else: + return load_json(config_path) + + +def parse_no_name(d): + p = r'(\d+)\.(.*)' + m = re.search(p, d) + + try: + no = int(m.group(1)) + dir_name = m.group(2) + except: + sys.exit(0) + + return no, dir_name + + +def check_export(base, cfg): + flag = False + exports = [] + for export in cfg.get('export', []): + ecfg_path = os.path.join(base, export) + if os.path.exists(ecfg_path): + exports.append(export) + else: + flag = True + if flag: + cfg["export"] = exports + return flag + + +class TreeWalker: + def __init__(self, root, tree_name, title=None, log=None): + self.name = tree_name + self.root = root + self.title = tree_name if title is None else title + self.tree = {} + self.logger = logger if log is None else log + + def walk(self): + root = self.load_root() + root_node = { + "node_id": root["node_id"], + "keywords": root["keywords"], + "children": [] + } + self.tree[root["tree_name"]] = root_node + self.load_levels(root_node) + self.load_chapters(self.root, root_node) + for index, level in enumerate(root_node["children"]): + level_title = list(level.keys())[0] + level_node = list(level.values())[0] + level_path = os.path.join(self.root, f"{index+1}.{level_title}") + self.load_chapters(level_path, level_node) + 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}") + 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}.{section_title}") + if os.path.isdir(full_path): + self.ensure_exercises(full_path) + + tree_path = os.path.join(self.root, "tree.json") + dump_json(tree_path, self.tree, exist_ok=True, override=True) + return self.tree + + def load_levels(self, root_node): + levels = [] + for level in os.listdir(self.root): + if not os.path.isdir(level): + continue + level_path = os.path.join(self.root, level) + num, config = self.load_level_node(level_path) + levels.append((num, config)) + + levels = self.resort_children(self.root, levels) + root_node["children"] = [item[1] for item in levels] + return root_node + + def load_level_node(self, level_path): + config = self.ensure_level_config(level_path) + num, name = self.extract_node_env(level_path) + + result = { + name: { + "node_id": config["node_id"], + "keywords": config["keywords"], + "children": [], + } + } + + return num, result + + def load_chapters(self, base, level_node): + chapters = [] + for name in os.listdir(base): + full_name = os.path.join(base, name) + if os.path.isdir(full_name): + num, chapter = self.load_chapter_node(full_name) + chapters.append((num, chapter)) + + chapters = self.resort_children(base, chapters) + level_node["children"] = [item[1] for item in chapters] + return level_node + + def load_sections(self, base, chapter_node): + sections = [] + for name in os.listdir(base): + full_name = os.path.join(base, name) + if os.path.isdir(full_name): + num, section = self.load_section_node(full_name) + sections.append((num, section)) + + sections = self.resort_children(base, sections) + chapter_node["children"] = [item[1] for item in sections] + return chapter_node + + def resort_children(self, base, children): + children.sort(key=lambda item: item[0]) + for index, [number, element] in enumerate(children): + title = list(element.keys())[0] + origin = os.path.join(base, f"{number}.{title}") + posted = os.path.join(base, f"{index+1}.{title}") + if origin != posted: + self.logger.info(f"rename [{origin}] to [{posted}]") + os.rename(origin, posted) + return children + + def ensure_chapters(self): + for subdir in os.listdir(self.root): + self.ensure_level_config(subdir) + + def load_root(self): + config_path = os.path.join(self.root, "config.json") + if not os.path.exists(config_path): + config = { + "tree_name": self.name, + "keywords": [], + "node_id": self.gen_node_id(), + } + dump_json(config_path, config, exist_ok=True, override=True) + else: + config = load_json(config_path) + flag, result = self.ensure_node_id(config) + if flag: + dump_json(config_path, result, exist_ok=True, override=True) + + return config + + 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() + } + dump_json(config_path, config, exist_ok=True, override=True) + else: + config = load_json(config_path) + flag, result = self.ensure_node_id(config) + if flag: + dump_json(config_path, config, exist_ok=True, override=True) + return config + + def ensure_chapter_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(), + "keywords": [] + } + dump_json(config_path, config, exist_ok=True, override=True) + else: + config = load_json(config_path) + flag, result = self.ensure_node_id(config) + if flag: + dump_json(config_path, config, exist_ok=True, override=True) + return config + + def ensure_section_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(), + "keywords": [], + "children": [], + "export": [] + } + dump_json(config_path, config, exist_ok=True, override=True) + else: + config = load_json(config_path) + flag, result = self.ensure_node_id(config) + if flag: + dump_json(config_path, config, exist_ok=True, override=True) + return config + + def ensure_node_id(self, config): + if "node_id" not in config: + config["node_id"] = self.gen_node_id() + return True, config + else: + return False, config + + def gen_node_id(self): + return f"{self.name}-{uuid.uuid4().hex}" + + def extract_node_env(self, path): + try: + _, dir = os.path.split(path) + self.logger.info(path) + number, title = dir.split(".", 1) + return int(number), title + except Exception as error: + self.logger.error(f"目录 [{path}] 解析失败,结构不合法,可能是缺少序号") + sys.exit(1) + + def load_chapter_node(self, full_name): + config = self.ensure_chapter_config(full_name) + num, name = self.extract_node_env(full_name) + result = { + name: { + "node_id": config["node_id"], + "keywords": config["keywords"], + "children": [], + } + } + return num, result + + def load_section_node(self, full_name): + config = self.ensure_section_config(full_name) + num, name = self.extract_node_env(full_name) + result = { + name: { + "node_id": config["node_id"], + "keywords": config["keywords"], + "children": config.get("children", []) + } + } + # if "children" in config: + # result["children"] = config["children"] + return num, result + + def ensure_exercises(self, section_path): + config = self.ensure_section_config(section_path) + for e in config.get("export", []): + full_name = os.path.join(section_path, e) + exercise = load_json(full_name) + if "exercise_id" not in exercise: + exercise["exercise_id"] = uuid.uuid4().hex + dump_json(full_name, exercise)