From ae2d022e2d4efffbbfb3d93f3bcc75fb53ba0fb8 Mon Sep 17 00:00:00 2001 From: ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:09:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:sparkles:=20=E6=AD=A3=E5=BC=8F?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E3=80=8C=E6=96=87=E4=BB=B6=E5=86=85=E8=81=94?= =?UTF-8?q?=E3=80=8D=E5=8A=9F=E8=83=BD=EF=BC=88=E7=9B=AE=E5=89=8D=E4=BB=85?= =?UTF-8?q?=E9=99=90Julia=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现在可以通过特殊注释「%inline-compiled」加「include(」或「include_notebook(」来引入指定路径的 文件内容/编译后笔记本内容 --- Project.toml | 2 +- README.md | 50 ++- src/IpynbCompile.ipynb | 698 ++++++++++++++++++++++++++++------------- src/IpynbCompile.jl | 257 +++++++++------ test/runtests.jl | 127 ++++++-- 5 files changed, 776 insertions(+), 358 deletions(-) diff --git a/Project.toml b/Project.toml index 3da3598..e655c8d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "IpynbCompile" uuid = "4eb781bf-a71e-403a-9d46-9d48649f04b2" authors = ["ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com>"] -version = "1.6.1" +version = "1.7.0" [deps] JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" diff --git a/README.md b/README.md index 35108dd..b0c4768 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具 [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) @@ -183,7 +183,7 @@ ##### 文件引入 -主要用途:结合「仅编译后可用」实现「外部代码内联」 +主要用途:结合「仅编译后可用」简单实现「外部代码内联」 - 如:集成某些**中小型映射表**,整合零散源码文件…… @@ -191,10 +191,54 @@ ```julia const square_map_dict = # 这里的等号可以另起一行 -# % include to_include.jl +# %include to_include.jl +# ↑ 上面一行会被替换成文件内容 +``` + +编译前@和笔记本**同目录**下的`to_include.jl`中: +↓文件末尾有换行符 + +```julia +# 这是一个要被引入的外部字典对象 +Dict([ + 1 => 1 + 2 => 4 + 3 => 9 + # ... +]) +``` + +编译后: + +```julia +const square_map_dict = # 这里的等号可以另起一行 +# 这是一个要被引入的外部字典对象 +Dict([ + 1 => 1 + 2 => 4 + 3 => 9 + # ... +]) # ↑ 上面一行会被替换成数据 ``` +📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白 + +##### 文件内联 + +主要用途:为「文件引入」提供一个快捷方式,并**支持「编译后内联笔记本」** + +- 如:编译并集成**其它笔记本**到该文件中 + +编译前@笔记本单元格: + +```julia +const square_map_dict = # 这里的等号可以另起一行 +#= %inline-compiled =# include("to_include.jl") +# ↑ 上面一行会被替换成文件内容 +# * 若为使用`include_notebook`引入的笔记本,则会被替换为编译后的笔记本内容 +``` + 编译前@和笔记本**同目录**下的`to_include.jl`中: ↓文件末尾有换行符 diff --git a/src/IpynbCompile.ipynb b/src/IpynbCompile.ipynb index e127f44..094693e 100644 --- a/src/IpynbCompile.ipynb +++ b/src/IpynbCompile.ipynb @@ -321,7 +321,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "主要用途:结合「仅编译后可用」实现「外部代码内联」\n", + "主要用途:结合「仅编译后可用」简单实现「外部代码内联」\n", "\n", "- 如:集成某些**中小型映射表**,整合零散源码文件……" ] @@ -334,10 +334,69 @@ "\n", "```julia\n", "const square_map_dict = # 这里的等号可以另起一行\n", - "# % include to_include.jl \n", + "# %include to_include.jl \n", + "# ↑ 上面一行会被替换成文件内容\n", + "```\n", + "\n", + "编译前@和笔记本**同目录**下的`to_include.jl`中:\n", + "↓文件末尾有换行符\n", + "\n", + "```julia\n", + "# 这是一个要被引入的外部字典对象\n", + "Dict([\n", + " 1 => 1\n", + " 2 => 4\n", + " 3 => 9\n", + " # ...\n", + "])\n", + "```\n", + "\n", + "编译后:\n", + "\n", + "```julia\n", + "const square_map_dict = # 这里的等号可以另起一行\n", + "# 这是一个要被引入的外部字典对象\n", + "Dict([\n", + " 1 => 1\n", + " 2 => 4\n", + " 3 => 9\n", + " # ...\n", + "])\n", "# ↑ 上面一行会被替换成数据\n", "```\n", "\n", + "📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 文件内联" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "主要用途:为「文件引入」提供一个快捷方式,并**支持「编译后内联笔记本」**\n", + "\n", + "- 如:编译并集成**其它笔记本**到该文件中" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "编译前@笔记本单元格:\n", + "\n", + "```julia\n", + "const square_map_dict = # 这里的等号可以另起一行\n", + "#= %inline-compiled =# include(\"to_include.jl\")\n", + "# ↑ 上面一行会被替换成文件内容\n", + "# * 若为使用`include_notebook`引入的笔记本,则会被替换为编译后的笔记本内容\n", + "```\n", + "\n", "编译前@和笔记本**同目录**下的`to_include.jl`中:\n", "↓文件末尾有换行符\n", "\n", @@ -560,8 +619,8 @@ " open(path, \"r\") do f\n", " read(f, String) |> JSON.parse\n", " end\n", - "\n", - "# ! ↓使用`# %ignore-line`让 编译器/解释器 忽略下一行\n", + "# %ignore-line\n", + "# ! ↕使用`# %ignore-line`让 编译器/解释器 忽略下一行\n", "# %ignore-line\n", "ROOT_PATH = any(contains(@__DIR__(), sub) for sub in [\"src\", \"test\"]) ? dirname(@__DIR__) : @__DIR__\n", "# %ignore-line\n", @@ -708,7 +767,7 @@ "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m \"file_extension\" => \".jl\"\n", "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m \"mimetype\" => \"application/julia\"\n", "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m \"name\" => \"julia\"\n", - "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m \"version\" => \"1.9.1\"\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m \"version\" => \"1.10.0\"\n", "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m metadata.kernelspec =\n", "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m Dict{String, Any} with 3 entries:\n", "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m \"name\" => \"julia-1.10\"\n", @@ -756,8 +815,8 @@ "output_type": "stream", "text": [ "\u001b[36m\u001b[1m┌ \u001b[22m\u001b[39m\u001b[36m\u001b[1mInfo: \u001b[22m\u001b[39mJSON转译结构化成功!\n", - "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m notebook_raw_cell = IpynbNotebook{Any}(Any[Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"(✨执行其中所有单元格,可自动构建、测试并生成相应`.jl`源码、测试文件与README!)\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)\\n\", \"[![Static Badge](https://img.shields.io/badge/julia-package?logo=julia&label=1.7%2B)](https://julialang.org/)\\n\", \"\\n\", \"[![CI status](https://github.com/ARCJ137442/IpynbCompile.jl/workflows/CI/badge.svg)](https://github.com/ARCJ137442/IpynbCompile.jl/actions/workflows/ci.yml)\\n\", \"\\n\", \"该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"## 主要功能\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"### 简介\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"📍主要功能:为 [***Jupyter***](https://jupyter.org/) 笔记本(`.ipynb`文件)提供一套特定的注释语法,以支持 **编译转换**&**解释执行** 功能,扩展其应用的可能性\\n\", \"\\n\", \"- 📌可【打开】并【解析】Jupyter笔记本:提供基本的「Jupyter笔记本」「Jupyter笔记本元数据」「Jupyter笔记本单元格」数据结构定义\\n\", \" - 笔记本 `IpynbNotebook{单元格类型}`\\n\", \" - 元数据 `IpynbNotebookMetadata`\\n\", \" - 单元格 `IpynbCell`\\n\", \"- 📌可将Jupyter笔记本(`.ipynb`文件)【转换】成可直接执行的 [***Julia***](https://julialang.org/) 代码\\n\", \" - 编译单元格 `compile_cell`\\n\", \" - 编译笔记本 `compile_notebook`\\n\", \" - 方法1:`compile_notebook(笔记本::IpynbNotebook)`\\n\" … \" - `nothing`(若为其它类型)\\n\", \" - 解析笔记本 `parse_notebook`\\n\", \" - 等效于「编译笔记本的**所有单元格**」\\n\", \" - 执行单元格 `eval_cell`\\n\", \" - 等效于「【解析】并【执行】单元格」\\n\", \" - 执行笔记本 `eval_notebook`\\n\", \" - 等效于「【解析】并【执行】笔记本」\\n\", \" - 逐单元格版本:`eval_notebook_by_cell`\\n\", \" - 引入笔记本 `include_notebook`\\n\", \" - 逐单元格版本:`include_notebook_by_cell`\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"✨创新点:**使用多样的「特殊注释」机制,让使用者能更灵活、更便捷地编译Jupyter笔记本,并能将其【交互式】优势用于库的编写之中**\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"### 重要机制:单元格「特殊注释」\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"简介:单元格的主要「特殊注释」及其作用(以`# 单行注释` `#= 块注释 =#`为例)\\n\", \"\\n\", \"- `# %ignore-line` 忽略下一行\\n\", \"- `# %ignore-below` 忽略下面所有行\\n\", \"- `# %ignore-cell` 忽略整个单元格\\n\", \"- `# %ignore-begin` 块忽略开始\\n\", \"- `# %ignore-end` 块忽略结束\\n\", \"- `#= %only-compiled` 仅编译后可用(头)\\n\", \"- `%only-compiled =#` 仅编译后可用(尾)\\n\", \"- `# %include <路径>` 引入指定路径的文件内容,替代一整行注释\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"✨**该笔记本自身**,就是一个好的用法参考来源\"], \"metadata\" => Dict{String, Any}()) … Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"## 自动构建\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"构建过程主要包括:\\n\", \"\\n\", \"- **自举**构建主模块,生成库文件\\n\", \"- 扫描`src`目录下基本所有Jupyter笔记本(`.ipynb`),编译生成`.jl`源码\\n\", \"- 提取该文件开头Markdown笔记,在**项目根目录**下**生成自述文件**(`README.md`)\\n\", \" - 因此`README.md`暂且只有一种语言(常更新的语言)\\n\", \"\\n\", \"⚠️不应该在编译后的库文件中看到任何代码\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 自举生成源码\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\e[93m\\e[1m✅Jupyter笔记本「主模块」自编译成功!\\e[22m\\e[39m\\n\", \"\\e[93m\\e[1m(共写入 50156 个字节)\\e[22m\\e[39m\\n\"])], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell # * 自举构建主模块\\n\", \"# * 自编译生成`.jl`源码\\n\", \"let OUT_LIB_FILE = \\\"IpynbCompile.jl\\\" # 直接作为库的主文件\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\", \" write_bytes = compile_notebook(SELF_PATH, joinpath(ROOT_PATH, \\\"src\\\", OUT_LIB_FILE))\\n\", \" printstyled(\\n\", \" \\\"✅Jupyter笔记本「主模块」自编译成功!\\\\n(共写入 \\$write_bytes 个字节)\\\\n\\\";\\n\", \" color=:light_yellow, bold=true\\n\", \" )\\n\", \"end\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 30), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 文件夹下其它笔记本的编译\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\e[92m\\e[1mCompiled: .\\\\compiler.ipynb => .\\\\compiler.jl\\e[22m\\e[39m\\n\"])], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell\\n\", \"# * 扫描`src`目录,自动构建主模块\\n\", \"# * - 📝Julia 文件夹遍历:`walkdir`迭代器\\n\", \"# * - 🔗参考:参考:https://stackoverflow.com/questions/58258101/how-to-loop-through-a-folder-of-sub-folders-in-julia\\n\", \"PATH_SRC = \\\".\\\"\\n\", \"let root_folder = PATH_SRC\\n\", \"\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\" … \" path => new_path # * 测试Pair\\n\", \" # ! 根目录后续会由`path`自行指定\\n\", \" )\\n\", \" # 输出编译结果\\n\", \" printstyled(\\n\", \" \\\"Compiled: \\$path => \\$new_path\\\\n\\\";\\n\", \" color=:light_green, bold=true\\n\", \" )\\n\", \" end\\n\", \"end\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 31), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 编译生成测试文件`runtests.jl`\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\e[92m\\e[1m✅测试文件编译成功!\\e[22m\\e[39m\\n\", \"\\e[92m\\e[1m(共写入 41068 个字节)\\e[22m\\e[39m\\n\"])], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell # * 自举构建主模块\\n\", \"if !contains(@__DIR__, \\\"test\\\") # 不能在测试代码中被重复调用\\n\", \" OUT_TEST_JL = joinpath(ROOT_PATH, \\\"test\\\", \\\"runtests.jl\\\") # 直接作为库的主文件\\n\", \" # 直接拼接所有代码单元格\\n\", \" code_tests = join((\\n\", \" join(cell.source)\\n\", \" for cell in notebook.cells\\n\", \" if cell.cell_type == \\\"code\\\"\\n\", \" ), \\\"\\\\n\\\\n\\\")\\n\", \" # 开头使用Test库,并添加测试上下文\\n\" … \" code_tests *= \\\"\\\"\\\"\\n\", \" end\\n\", \" \\\"\\\"\\\" =#\\n\", \" # 最终写入\\n\", \" write_bytes = write(OUT_TEST_JL, code_tests)\\n\", \" printstyled(\\n\", \" \\\"✅测试文件编译成功!\\\\n(共写入 \\$write_bytes 个字节)\\\\n\\\";\\n\", \" color=:light_green, bold=true\\n\", \" )\\n\", \"end\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 32), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 编译生成自述文件`README.md`(非库功能)\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\n\", \"# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具\\n\", \"\\n\", \"[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)\\n\", \"[![Static Badge](https://img.shields.io/badge/julia-package?logo=julia&label=1.7%2B)](https://julialang.org/)\\n\", \"\\n\", \"[![CI status](https://github.com/ARCJ137442/IpynbCompile.jl/workflows/CI/badge.svg)](https://github.com/ARCJ137442/IpynbCompile.jl/actions/workflows/ci.yml)\\n\", \"\\n\", \"该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。\\n\", \"\\n\" … \"# ↑ 上面一行会被替换成数据\\n\", \"```\\n\", \"\\n\", \"📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白\\n\", \"\\n\", \"## 参考\\n\", \"\\n\", \"- 本Julia库的灵感来源:[Promises.jl/src/notebook.jl](https://github.com/fonsp/Promises.jl/blob/main/src/notebook.jl)\\n\", \" - 源库使用了 [**Pluto.jl**](https://github.com/fonsp/Pluto.jl) 的「笔记本导出」功能\\n\", \"- **Jupyter Notebook** 文件格式(JSON):[🔗nbformat.readthedocs.io](https://nbformat.readthedocs.io/en/latest/format_description.html#notebook-file-format)\\n\"]), Dict{String, Any}(\"output_type\" => \"execute_result\", \"data\" => Dict{String, Any}(\"text/plain\" => Any[\"7861\"]), \"metadata\" => Dict{String, Any}(), \"execution_count\" => 33)], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell # * 扫描自身Markdown单元格,自动生成`README.md`\\n\", \"\\\"决定「单元格采集结束」的标识\\\" # ! 不包括结束处单元格\\n\", \"FLAG_END = \\\"\\n\", \"\\$README_markdown_TEXT\\\\\\n\", \"\\\"\\\"\\\"\\n\", \"print(README_markdown_TEXT)\\n\", \"\\n\", \"README_FILE = \\\"README.md\\\"\\n\", \"write(joinpath(ROOT_PATH, README_FILE), README_markdown_TEXT)\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 33)], IpynbNotebookMetadata(Dict{String, Any}(\"file_extension\" => \".jl\", \"mimetype\" => \"application/julia\", \"name\" => \"julia\", \"version\" => \"1.9.1\"), Dict{String, Any}(\"name\" => \"julia-1.10\", \"display_name\" => \"Julia 1.10.0\", \"language\" => \"julia\")), 4, 2)\n", - "\u001b[36m\u001b[1m└ \u001b[22m\u001b[39m notebook_metadata = IpynbNotebookMetadata(Dict{String, Any}(\"file_extension\" => \".jl\", \"mimetype\" => \"application/julia\", \"name\" => \"julia\", \"version\" => \"1.9.1\"), Dict{String, Any}(\"name\" => \"julia-1.10\", \"display_name\" => \"Julia 1.10.0\", \"language\" => \"julia\"))\n" + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m notebook_raw_cell = IpynbNotebook{Any}(Any[Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"(✨执行其中所有单元格,可自动构建、测试并生成相应`.jl`源码、测试文件与README!)\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)\\n\", \"[![Static Badge](https://img.shields.io/badge/julia-package?logo=julia&label=1.7%2B)](https://julialang.org/)\\n\", \"\\n\", \"[![CI status](https://github.com/ARCJ137442/IpynbCompile.jl/workflows/CI/badge.svg)](https://github.com/ARCJ137442/IpynbCompile.jl/actions/workflows/ci.yml)\\n\", \"\\n\", \"该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"## 主要功能\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"### 简介\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"📍主要功能:为 [***Jupyter***](https://jupyter.org/) 笔记本(`.ipynb`文件)提供一套特定的注释语法,以支持 **编译转换**&**解释执行** 功能,扩展其应用的可能性\\n\", \"\\n\", \"- 📌可【打开】并【解析】Jupyter笔记本:提供基本的「Jupyter笔记本」「Jupyter笔记本元数据」「Jupyter笔记本单元格」数据结构定义\\n\", \" - 笔记本 `IpynbNotebook{单元格类型}`\\n\", \" - 元数据 `IpynbNotebookMetadata`\\n\", \" - 单元格 `IpynbCell`\\n\", \"- 📌可将Jupyter笔记本(`.ipynb`文件)【转换】成可直接执行的 [***Julia***](https://julialang.org/) 代码\\n\", \" - 编译单元格 `compile_cell`\\n\", \" - 编译笔记本 `compile_notebook`\\n\", \" - 方法1:`compile_notebook(笔记本::IpynbNotebook)`\\n\" … \" - `nothing`(若为其它类型)\\n\", \" - 解析笔记本 `parse_notebook`\\n\", \" - 等效于「编译笔记本的**所有单元格**」\\n\", \" - 执行单元格 `eval_cell`\\n\", \" - 等效于「【解析】并【执行】单元格」\\n\", \" - 执行笔记本 `eval_notebook`\\n\", \" - 等效于「【解析】并【执行】笔记本」\\n\", \" - 逐单元格版本:`eval_notebook_by_cell`\\n\", \" - 引入笔记本 `include_notebook`\\n\", \" - 逐单元格版本:`include_notebook_by_cell`\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"✨创新点:**使用多样的「特殊注释」机制,让使用者能更灵活、更便捷地编译Jupyter笔记本,并能将其【交互式】优势用于库的编写之中**\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"### 重要机制:单元格「特殊注释」\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"简介:单元格的主要「特殊注释」及其作用(以`# 单行注释` `#= 块注释 =#`为例)\\n\", \"\\n\", \"- `# %ignore-line` 忽略下一行\\n\", \"- `# %ignore-below` 忽略下面所有行\\n\", \"- `# %ignore-cell` 忽略整个单元格\\n\", \"- `# %ignore-begin` 块忽略开始\\n\", \"- `# %ignore-end` 块忽略结束\\n\", \"- `#= %only-compiled` 仅编译后可用(头)\\n\", \"- `%only-compiled =#` 仅编译后可用(尾)\\n\", \"- `# %include <路径>` 引入指定路径的文件内容,替代一整行注释\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"✨**该笔记本自身**,就是一个好的用法参考来源\"], \"metadata\" => Dict{String, Any}()) … Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"## 自动构建\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"构建过程主要包括:\\n\", \"\\n\", \"- **自举**构建主模块,生成库文件\\n\", \"- 扫描`src`目录下基本所有Jupyter笔记本(`.ipynb`),编译生成`.jl`源码\\n\", \"- 提取该文件开头Markdown笔记,在**项目根目录**下**生成自述文件**(`README.md`)\\n\", \" - 因此`README.md`暂且只有一种语言(常更新的语言)\\n\", \"\\n\", \"\\n\", \"⚠️不应该在编译后的库文件中看到任何代码\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 自举生成源码\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\e[93m\\e[1m✅Jupyter笔记本「主模块」自编译成功!\\e[22m\\e[39m\\n\", \"\\e[93m\\e[1m(共写入 50832 个字节)\\e[22m\\e[39m\\n\"])], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell # * 自举构建主模块\\n\", \"# * 自编译生成`.jl`源码\\n\", \"let OUT_LIB_FILE = \\\"IpynbCompile.jl\\\" # 直接作为库的主文件\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\", \" write_bytes = compile_notebook(SELF_PATH, joinpath(ROOT_PATH, \\\"src\\\", OUT_LIB_FILE))\\n\", \" printstyled(\\n\", \" \\\"✅Jupyter笔记本「主模块」自编译成功!\\\\n(共写入 \\$write_bytes 个字节)\\\\n\\\";\\n\", \" color=:light_yellow, bold=true\\n\", \" )\\n\", \"end\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 63), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 文件夹下其它笔记本的编译\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\e[92m\\e[1mCompiled: .\\\\compiler.ipynb => .\\\\compiler.jl\\e[22m\\e[39m\\n\"])], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell\\n\", \"# * 扫描`src`目录,自动构建主模块\\n\", \"# * - 📝Julia 文件夹遍历:`walkdir`迭代器\\n\", \"# * - 🔗参考:参考:https://stackoverflow.com/questions/58258101/how-to-loop-through-a-folder-of-sub-folders-in-julia\\n\", \"PATH_SRC = \\\".\\\"\\n\", \"let root_folder = PATH_SRC\\n\", \"\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\" … \" path => new_path # * 测试Pair\\n\", \" # ! 根目录后续会由`path`自行指定\\n\", \" )\\n\", \" # 输出编译结果\\n\", \" printstyled(\\n\", \" \\\"Compiled: \\$path => \\$new_path\\\\n\\\";\\n\", \" color=:light_green, bold=true\\n\", \" )\\n\", \" end\\n\", \"end\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 64), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 编译生成测试文件`runtests.jl`\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\e[92m\\e[1m✅测试文件编译成功!\\e[22m\\e[39m\\n\", \"\\e[92m\\e[1m(共写入 41758 个字节)\\e[22m\\e[39m\\n\"])], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell # * 自举构建主模块\\n\", \"if !contains(@__DIR__, \\\"test\\\") # 不能在测试代码中被重复调用\\n\", \" OUT_TEST_JL = joinpath(ROOT_PATH, \\\"test\\\", \\\"runtests.jl\\\") # 直接作为库的主文件\\n\", \" # 直接拼接所有代码单元格\\n\", \" code_tests = join((\\n\", \" join(cell.source)\\n\", \" for cell in notebook.cells\\n\", \" if cell.cell_type == \\\"code\\\"\\n\", \" ), \\\"\\\\n\\\\n\\\")\\n\", \" # 开头使用Test库,并添加测试上下文\\n\" … \" code_tests *= \\\"\\\"\\\"\\n\", \" end\\n\", \" \\\"\\\"\\\" =#\\n\", \" # 最终写入\\n\", \" write_bytes = write(OUT_TEST_JL, code_tests)\\n\", \" printstyled(\\n\", \" \\\"✅测试文件编译成功!\\\\n(共写入 \\$write_bytes 个字节)\\\\n\\\";\\n\", \" color=:light_green, bold=true\\n\", \" )\\n\", \"end\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 65), Dict{String, Any}(\"cell_type\" => \"markdown\", \"source\" => Any[\"\\n\", \"### 编译生成自述文件`README.md`(非库功能)\"], \"metadata\" => Dict{String, Any}()), Dict{String, Any}(\"outputs\" => Any[Dict{String, Any}(\"name\" => \"stdout\", \"output_type\" => \"stream\", \"text\" => Any[\"\\n\", \"# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具\\n\", \"\\n\", \"[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)\\n\", \"[![Static Badge](https://img.shields.io/badge/julia-package?logo=julia&label=1.7%2B)](https://julialang.org/)\\n\", \"\\n\", \"[![CI status](https://github.com/ARCJ137442/IpynbCompile.jl/workflows/CI/badge.svg)](https://github.com/ARCJ137442/IpynbCompile.jl/actions/workflows/ci.yml)\\n\", \"\\n\", \"该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。\\n\", \"\\n\" … \"# ↑ 上面一行会被替换成数据\\n\", \"```\\n\", \"\\n\", \"📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白\\n\", \"\\n\", \"## 参考\\n\", \"\\n\", \"- 本Julia库的灵感来源:[Promises.jl/src/notebook.jl](https://github.com/fonsp/Promises.jl/blob/main/src/notebook.jl)\\n\", \" - 源库使用了 [**Pluto.jl**](https://github.com/fonsp/Pluto.jl) 的「笔记本导出」功能\\n\", \"- **Jupyter Notebook** 文件格式(JSON):[🔗nbformat.readthedocs.io](https://nbformat.readthedocs.io/en/latest/format_description.html#notebook-file-format)\\n\"]), Dict{String, Any}(\"output_type\" => \"execute_result\", \"data\" => Dict{String, Any}(\"text/plain\" => Any[\"7861\"]), \"metadata\" => Dict{String, Any}(), \"execution_count\" => 66)], \"cell_type\" => \"code\", \"source\" => Any[\"# %ignore-cell # * 扫描自身Markdown单元格,自动生成`README.md`\\n\", \"\\\"决定「单元格采集结束」的标识\\\" # ! 不包括结束处单元格\\n\", \"FLAG_END = \\\"\\n\", \"\\$README_markdown_TEXT\\\\\\n\", \"\\\"\\\"\\\"\\n\", \"print(README_markdown_TEXT)\\n\", \"\\n\", \"README_FILE = \\\"README.md\\\"\\n\", \"write(joinpath(ROOT_PATH, README_FILE), README_markdown_TEXT)\"], \"metadata\" => Dict{String, Any}(), \"execution_count\" => 66)], IpynbNotebookMetadata(Dict{String, Any}(\"file_extension\" => \".jl\", \"mimetype\" => \"application/julia\", \"name\" => \"julia\", \"version\" => \"1.10.0\"), Dict{String, Any}(\"name\" => \"julia-1.10\", \"display_name\" => \"Julia 1.10.0\", \"language\" => \"julia\")), 4, 2)\n", + "\u001b[36m\u001b[1m└ \u001b[22m\u001b[39m notebook_metadata = IpynbNotebookMetadata(Dict{String, Any}(\"file_extension\" => \".jl\", \"mimetype\" => \"application/julia\", \"name\" => \"julia\", \"version\" => \"1.10.0\"), Dict{String, Any}(\"name\" => \"julia-1.10\", \"display_name\" => \"Julia 1.10.0\", \"language\" => \"julia\"))\n" ] } ], @@ -827,9 +886,9 @@ " language_info=json[\"language_info\"],\n", " kernelspec=json[\"kernelspec\"],\n", ")\n", - "\n", - "# ! ↓使用`# %ignore-below`让 编译器/解释器 忽略后续内容\n", "# %ignore-below\n", + "# ! ↑使用`# %ignore-below`让 编译器/解释器 忽略后续内容\n", + "\n", "notebook_raw_cell = IpynbNotebook(notebook_json)\n", "notebook_metadata = notebook_raw_cell.metadata\n", "@info \"JSON转译结构化成功!\" notebook_raw_cell notebook_metadata" @@ -906,6 +965,7 @@ " :(read_notebook($path)) |> esc\n", "end\n", "# %ignore-below\n", + "\n", "@macroexpand notebook\"IpynbCompile.ipynb\"" ] }, @@ -974,8 +1034,7 @@ "identify_lang(language_text::AbstractString) =\n", " findfirst(LANG_IDENTIFY_DICT) do regex\n", " contains(language_text, regex)\n", - " end # ! 默认返回`nothing`\n", - "# %ignore-below # ! 测试代码在最下边" + " end # ! 默认返回`nothing`" ] }, { @@ -1050,8 +1109,8 @@ "\n", "\"【内部】生成块注释结尾 | ⚠️找不到⇒报错\"\n", "generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang]\n", - "\n", "# %ignore-below # ! 测试代码在最下边\n", + "\n", "@info \"\" LANG_COMMENT_DICT_INLINE LANG_COMMENT_DICT_MULTILINE_HEAD LANG_COMMENT_DICT_MULTILINE_TAIL" ] }, @@ -1105,8 +1164,8 @@ " LANG_EXTENSION_DICT, lang,\n", " string(lang)\n", ")\n", - "\n", "# %ignore-below # ! 测试代码在最下边\n", + "\n", "@info \"\" LANG_EXTENSION_DICT" ] }, @@ -1207,7 +1266,7 @@ "output_type": "stream", "text": [ "# %% Jupyter Notebook | Julia 1.10.0 @ julia | format 2~4\n", - "# % language_info: {\"file_extension\":\".jl\",\"mimetype\":\"application/julia\",\"name\":\"julia\",\"version\":\"1.9.1\"}\n", + "# % language_info: {\"file_extension\":\".jl\",\"mimetype\":\"application/julia\",\"name\":\"julia\",\"version\":\"1.10.0\"}\n", "# % kernelspec: {\"name\":\"julia-1.10\",\"display_name\":\"Julia 1.10.0\",\"language\":\"julia\"}\n", "# % nbformat: 4\n", "# % nbformat_minor: 2\n" @@ -1237,8 +1296,8 @@ "$(generate_comment_inline(lang)) % nbformat: $(notebook.nbformat)\n", "$(generate_comment_inline(lang)) % nbformat_minor: $(notebook.nbformat_minor)\n", "\"\"\"\n", - "\n", "# %ignore-below\n", + "\n", "# ! ↑使用`# %ignore-below`让 编译器/解释器 忽略后续内容 | 【2024-01-26 21:38:54】debug:笔记本可能在不同的电脑上运行\n", "let notebook_jl_head = compile_notebook_head(notebook_raw_cell; lang=:julia)\n", " @assert contains(\n", @@ -1399,8 +1458,8 @@ " )\n", " ) |> esc\n", "end\n", - "\n", "# %ignore-below\n", + "\n", "let a1 = split_to_cell(\"\"\"1\\n2\\n3\"\"\"), # 📌测试【末尾有无换行】的区别\n", " a2 = split_to_cell(\"\"\"1\\n2\\n3\\n\"\"\")\n", "\n", @@ -1435,7 +1494,7 @@ { "data": { "text/plain": [ - "114-element Vector{IpynbCell}:\n", + "118-element Vector{IpynbCell}:\n", " IpynbCell(\"markdown\", [\"# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"markdown\", [\"\\n\", \"(✨执行其中所有单元格,可自动构建、测试并生成相应`.jl`源码、测试文件与README!)\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"markdown\", [\"[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)\\n\", \"[![Static Badge](https://img.shields.io/badge/julia-package?logo=julia&label=1.7%2B)](https://julialang.org/)\\n\", \"\\n\", \"[![CI status](https://github.com/ARCJ137442/IpynbCompile.jl/workflows/CI/badge.svg)](https://github.com/ARCJ137442/IpynbCompile.jl/actions/workflows/ci.yml)\\n\", \"\\n\", \"该项目使用[语义化版本 2.0.0](https://semver.org/)进行版本号管理。\"], Dict{String, Any}(), nothing)\n", @@ -1453,7 +1512,7 @@ " IpynbCell(\"markdown\", [\"## 关闭模块上下文\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# ! ↓这后边注释的代码只有在编译后才会被执行\\n\", \"# ! 仍然使用多行注释语法,以便统一格式\\n\", \"#= %only-compiled\\n\", \"end # module\\n\", \"%only-compiled =#\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"markdown\", [\"\\n\", \"## 自动构建\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"markdown\", [\"\\n\", \"构建过程主要包括:\\n\", \"\\n\", \"- **自举**构建主模块,生成库文件\\n\", \"- 扫描`src`目录下基本所有Jupyter笔记本(`.ipynb`),编译生成`.jl`源码\\n\", \"- 提取该文件开头Markdown笔记,在**项目根目录**下**生成自述文件**(`README.md`)\\n\", \" - 因此`README.md`暂且只有一种语言(常更新的语言)\\n\", \"\\n\", \"⚠️不应该在编译后的库文件中看到任何代码\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"markdown\", [\"构建过程主要包括:\\n\", \"\\n\", \"- **自举**构建主模块,生成库文件\\n\", \"- 扫描`src`目录下基本所有Jupyter笔记本(`.ipynb`),编译生成`.jl`源码\\n\", \"- 提取该文件开头Markdown笔记,在**项目根目录**下**生成自述文件**(`README.md`)\\n\", \" - 因此`README.md`暂且只有一种语言(常更新的语言)\\n\", \"\\n\", \"\\n\", \"⚠️不应该在编译后的库文件中看到任何代码\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"markdown\", [\"\\n\", \"### 自举生成源码\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# %ignore-cell # * 自举构建主模块\\n\", \"# * 自编译生成`.jl`源码\\n\", \"let OUT_LIB_FILE = \\\"IpynbCompile.jl\\\" # 直接作为库的主文件\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\", \" write_bytes = compile_notebook(SELF_PATH, joinpath(ROOT_PATH, \\\"src\\\", OUT_LIB_FILE))\\n\", \" printstyled(\\n\", \" \\\"✅Jupyter笔记本「主模块」自编译成功!\\\\n(共写入 \\$write_bytes 个字节)\\\\n\\\";\\n\", \" color=:light_yellow, bold=true\\n\", \" )\\n\", \"end\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"markdown\", [\"\\n\", \"### 文件夹下其它笔记本的编译\"], Dict{String, Any}(), nothing)\n", @@ -1473,8 +1532,8 @@ "# ! 在此重定向,以便后续外部调用\n", "\"重定向「笔记本」的默认「单元格」类型\"\n", "IpynbNotebook(json) = IpynbNotebook{IpynbCell}(json)\n", - "\n", "# %ignore-below\n", + "\n", "notebook = IpynbNotebook{IpynbCell}(notebook_json)\n", "cells = notebook.cells" ] @@ -1579,8 +1638,8 @@ "$(#= 可选的行号 =# haskey(kwargs, :line_num) ? \"[$(kwargs[:line_num])] \" : \"\")\\\n", "$(cell.cell_type)\n", "\"\"\" # ! ←末尾附带换行符\n", - "\n", "# %ignore-below\n", + "\n", "@assert compile_cell_head(notebook.cells[1]; lang=:julia) == \"# %% markdown\\n\"\n", "@assert compile_cell_head(notebook.cells[1]; lang=:julia, line_num=1) == \"# %% [1] markdown\\n\"" ] @@ -1600,28 +1659,28 @@ { "data": { "text/plain": [ - "33-element Vector{IpynbCell}:\n", + "34-element Vector{IpynbCell}:\n", " IpynbCell(\"code\", [\"# ! ↓这后边注释的代码只有在编译后才会被执行\\n\", \"# ! 使用多行注释/块注释的语法,\\n\", \"# ! 以`#= %only-compiled`行*开头*\\n\", \"# ! 以`%only-compiled =#`行*结尾*\\n\", \"#= %only-compiled # * ←这个仅需作为前缀(⚠️这注释会被一并移除)\\n\", \"\\\"\\\"\\\"\\n\", \"IpynbCompile 主模块\\n\", \"\\\"\\\"\\\"\\n\", \"module IpynbCompile # 后续编译后会变为模块上下文\\n\", \"%only-compiled =# # * ←左边同理(⚠️这注释会被一并移除)\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"import JSON\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"\\\"JSON常用的字典\\\"\\n\", \"const JSONDict{ValueType} = Dict{String,ValueType} where ValueType\\n\", \"\\n\", \"\\\"默认解析出来的JSON字典(与`JSONDict`有本质不同,会影响到后续方法分派,并可能导致歧义)\\\"\\n\", \"const JSONDictAny = JSONDict{Any}\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"import Base: @kwdef # 兼容Julia 1.8⁻\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export read_ipynb_json\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"读取ipynb JSON文件\\n\", \"- @param path .ipynb文件路径\\n\", \"- @return .ipynb文件内容(JSON文本→Julia对象)\\n\", \"\\\"\\\"\\\"\\n\", \"read_ipynb_json(path) =\\n\" … \"\\n\", \"# ! ↓使用`# %ignore-line`让 编译器/解释器 忽略下一行\\n\", \"# %ignore-line\\n\", \"ROOT_PATH = any(contains(@__DIR__(), sub) for sub in [\\\"src\\\", \\\"test\\\"]) ? dirname(@__DIR__) : @__DIR__\\n\", \"# %ignore-line\\n\", \"SELF_FILE = \\\"IpynbCompile.ipynb\\\"\\n\", \"# %ignore-line # * 行尾可以附带其它注释\\n\", \"SELF_PATH = joinpath(ROOT_PATH, \\\"src\\\", SELF_FILE)\\n\", \"# %ignore-line # * 但每行都需要一个注释\\n\", \"notebook_json = read_ipynb_json(SELF_PATH)\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export read_ipynb_json\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"读取ipynb JSON文件\\n\", \"- @param path .ipynb文件路径\\n\", \"- @return .ipynb文件内容(JSON文本→Julia对象)\\n\", \"\\\"\\\"\\\"\\n\", \"read_ipynb_json(path) =\\n\" … \"# %ignore-line\\n\", \"# ! ↕使用`# %ignore-line`让 编译器/解释器 忽略下一行\\n\", \"# %ignore-line\\n\", \"ROOT_PATH = any(contains(@__DIR__(), sub) for sub in [\\\"src\\\", \\\"test\\\"]) ? dirname(@__DIR__) : @__DIR__\\n\", \"# %ignore-line\\n\", \"SELF_FILE = \\\"IpynbCompile.ipynb\\\"\\n\", \"# %ignore-line # * 行尾可以附带其它注释\\n\", \"SELF_PATH = joinpath(ROOT_PATH, \\\"src\\\", SELF_FILE)\\n\", \"# %ignore-line # * 但每行都需要一个注释\\n\", \"notebook_json = read_ipynb_json(SELF_PATH)\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# %ignore-cell # ! ←使用`# %ignore-line`让 编译器/解释器 忽略整个单元格\\n\", \"# * ↑建议放在第一行\\n\", \"# ! ⚠️该代码不能有其它冗余的【前缀】字符\\n\", \"\\n\", \"let metadata = notebook_json[\\\"metadata\\\"],\\n\", \" var\\\"metadata.language_info\\\" = metadata[\\\"language_info\\\"]\\n\", \"\\n\", \" var\\\"metadata.kernelspec\\\" = metadata[\\\"kernelspec\\\"]\\n\", \" @info \\\"notebook_json\\\" notebook_json\\n\", \" @info \\\"notebook_json.metadata\\\" metadata\\n\", \" @info \\\"metadata[...]\\\" var\\\"metadata.language_info\\\" var\\\"metadata.kernelspec\\\"\\n\", \"end\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export IpynbNotebook, IpynbNotebookMetadata\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"定义一个Jupyter Notebook的metadata结构\\n\", \"- 🎯规范化存储Jupyter Notebook的元数据\\n\", \" - 根据官方文档,仅存储【已经确定存在】的「语言信息」和「内核信息」\\n\", \"\\\"\\\"\\\"\\n\", \"@kwdef struct IpynbNotebookMetadata # !【2024-01-14 16:09:35】目前只发现这两种信息\\n\" … \"IpynbNotebookMetadata(json::JSONDict) = IpynbNotebookMetadata(;\\n\", \" language_info=json[\\\"language_info\\\"],\\n\", \" kernelspec=json[\\\"kernelspec\\\"],\\n\", \")\\n\", \"\\n\", \"# ! ↓使用`# %ignore-below`让 编译器/解释器 忽略后续内容\\n\", \"# %ignore-below\\n\", \"notebook_raw_cell = IpynbNotebook(notebook_json)\\n\", \"notebook_metadata = notebook_raw_cell.metadata\\n\", \"@info \\\"JSON转译结构化成功!\\\" notebook_raw_cell notebook_metadata\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export IpynbNotebook, IpynbNotebookMetadata\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"定义一个Jupyter Notebook的metadata结构\\n\", \"- 🎯规范化存储Jupyter Notebook的元数据\\n\", \" - 根据官方文档,仅存储【已经确定存在】的「语言信息」和「内核信息」\\n\", \"\\\"\\\"\\\"\\n\", \"@kwdef struct IpynbNotebookMetadata # !【2024-01-14 16:09:35】目前只发现这两种信息\\n\" … \"IpynbNotebookMetadata(json::JSONDict) = IpynbNotebookMetadata(;\\n\", \" language_info=json[\\\"language_info\\\"],\\n\", \" kernelspec=json[\\\"kernelspec\\\"],\\n\", \")\\n\", \"# %ignore-below\\n\", \"# ! ↑使用`# %ignore-below`让 编译器/解释器 忽略后续内容\\n\", \"\\n\", \"notebook_raw_cell = IpynbNotebook(notebook_json)\\n\", \"notebook_metadata = notebook_raw_cell.metadata\\n\", \"@info \\\"JSON转译结构化成功!\\\" notebook_raw_cell notebook_metadata\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export read_notebook\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"从路径读取Jupyter笔记本(`struct IpynbNotebook`)\\\"\\n\", \"read_notebook(path::AbstractString)::IpynbNotebook = IpynbNotebook(read_ipynb_json(path))\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export @notebook_str\\n\", \"%only-compiled =#\\n\", \"\\n\", \"macro notebook_str(path::AbstractString)\\n\", \" :(read_notebook(\\$path)) |> esc\\n\", \"end\\n\", \"# %ignore-below\\n\", \"@macroexpand notebook\\\"IpynbCompile.ipynb\\\"\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"\\\"【内部】编程语言⇒正则表达式 识别字典\\\"\\n\", \"const LANG_IDENTIFY_DICT = Dict{Symbol,Regex}(\\n\", \" lang => Regex(\\\"^(?:\\$regex_str)\\\\\\$\\\") # ! ←必须头尾精确匹配(不然就会把`JavaScript`认成`r`)\\n\", \" for (lang::Symbol, regex_str::String) in\\n\", \" # ! 以下「特殊注释」需要在行首\\n\", \" # * 下方内容是「执行时动态引入,编译时静态内联」\\n\", \"#= %inline-compiled =# include(\\\"./../src/datas/language_identify_dict.data.jl\\\")\\n\", \" # !【2024-01-27 00:48:32】为了兼容自动生成的测试文件`runtests.jl`,需要使用「相对绝对路径」`./../src/`\\n\", \")\\n\", \"\\n\" … \" # ! 默认返回空字串\\n\", \" \\\"\\\"\\n\", \" )\\n\", \" )\\n\", \")\\n\", \"identify_lang(language_text::AbstractString) =\\n\", \" findfirst(LANG_IDENTIFY_DICT) do regex\\n\", \" contains(language_text, regex)\\n\", \" end # ! 默认返回`nothing`\\n\", \"# %ignore-below # ! 测试代码在最下边\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"\\\"【内部】编程语言⇒单行注释\\\"\\n\", \"const LANG_COMMENT_DICT_INLINE = Dict{Symbol,String}()\\n\", \"\\n\", \"\\\"【内部】编程语言⇒多行注释开头\\\"\\n\", \"const LANG_COMMENT_DICT_MULTILINE_HEAD = Dict{Symbol,String}()\\n\", \"\\n\", \"\\\"【内部】编程语言⇒多行注释结尾\\\"\\n\", \"const LANG_COMMENT_DICT_MULTILINE_TAIL = Dict{Symbol,String}()\\n\", \"\\n\", \"# * 遍历表格,生成列表\\n\" … \"generate_comment_inline(lang::Symbol) = LANG_COMMENT_DICT_INLINE[lang]\\n\", \"\\n\", \"\\\"【内部】生成块注释开头 | ⚠️找不到⇒报错\\\"\\n\", \"generate_comment_multiline_head(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_HEAD[lang]\\n\", \"\\n\", \"\\\"【内部】生成块注释结尾 | ⚠️找不到⇒报错\\\"\\n\", \"generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang]\\n\", \"\\n\", \"# %ignore-below # ! 测试代码在最下边\\n\", \"@info \\\"\\\" LANG_COMMENT_DICT_INLINE LANG_COMMENT_DICT_MULTILINE_HEAD LANG_COMMENT_DICT_MULTILINE_TAIL\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"\\\"【内部】编程语言⇒常用扩展名(不带`.`)\\\"\\n\", \"const LANG_EXTENSION_DICT = Dict{Symbol,String}(\\n\", \" # ! 以下「特殊注释」需要在行首\\n\", \"#= %inline-compiled =# include(\\\"./../src/datas/language_extension_dict.data.jl\\\")\\n\", \" # !【2024-01-27 00:48:32】为了兼容自动生成的测试文件`runtests.jl`,需要使用「相对绝对路径」`./../src/`\\n\", \")\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"【内部】根据编程语言猜测扩展名\\n\", \"- @returns 特定语言的`Symbol` | 语言本身的字符串形式\\n\", \" - @default 如`:aaa => \\\"aaa\\\"`\\n\", \"\\\"\\\"\\\"\\n\", \"get_extension(lang::Symbol) = get(\\n\", \" LANG_EXTENSION_DICT, lang,\\n\", \" string(lang)\\n\", \")\\n\", \"\\n\", \"# %ignore-below # ! 测试代码在最下边\\n\", \"@info \\\"\\\" LANG_EXTENSION_DICT\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export @notebook_str\\n\", \"%only-compiled =#\\n\", \"\\n\", \"macro notebook_str(path::AbstractString)\\n\", \" :(read_notebook(\\$path)) |> esc\\n\", \"end\\n\", \"# %ignore-below\\n\", \"\\n\", \"@macroexpand notebook\\\"IpynbCompile.ipynb\\\"\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"\\\"【内部】编程语言⇒正则表达式 识别字典\\\"\\n\", \"const LANG_IDENTIFY_DICT = Dict{Symbol,Regex}(\\n\", \" lang => Regex(\\\"^(?:\\$regex_str)\\\\\\$\\\") # ! ←必须头尾精确匹配(不然就会把`JavaScript`认成`r`)\\n\", \" for (lang::Symbol, regex_str::String) in\\n\", \" # ! 以下「特殊注释」需要在行首\\n\", \" # * 下方内容是「执行时动态引入,编译时静态内联」\\n\", \"#= %inline-compiled =# include(\\\"./../src/datas/language_identify_dict.data.jl\\\")\\n\", \" # !【2024-01-27 00:48:32】为了兼容自动生成的测试文件`runtests.jl`,需要使用「相对绝对路径」`./../src/`\\n\", \")\\n\", \"\\n\" … \" notebook.metadata.language_info, \\\"name\\\",\\n\", \" # ! 默认返回空字串\\n\", \" \\\"\\\"\\n\", \" )\\n\", \" )\\n\", \")\\n\", \"identify_lang(language_text::AbstractString) =\\n\", \" findfirst(LANG_IDENTIFY_DICT) do regex\\n\", \" contains(language_text, regex)\\n\", \" end # ! 默认返回`nothing`\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"\\\"【内部】编程语言⇒单行注释\\\"\\n\", \"const LANG_COMMENT_DICT_INLINE = Dict{Symbol,String}()\\n\", \"\\n\", \"\\\"【内部】编程语言⇒多行注释开头\\\"\\n\", \"const LANG_COMMENT_DICT_MULTILINE_HEAD = Dict{Symbol,String}()\\n\", \"\\n\", \"\\\"【内部】编程语言⇒多行注释结尾\\\"\\n\", \"const LANG_COMMENT_DICT_MULTILINE_TAIL = Dict{Symbol,String}()\\n\", \"\\n\", \"# * 遍历表格,生成列表\\n\" … \"generate_comment_inline(lang::Symbol) = LANG_COMMENT_DICT_INLINE[lang]\\n\", \"\\n\", \"\\\"【内部】生成块注释开头 | ⚠️找不到⇒报错\\\"\\n\", \"generate_comment_multiline_head(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_HEAD[lang]\\n\", \"\\n\", \"\\\"【内部】生成块注释结尾 | ⚠️找不到⇒报错\\\"\\n\", \"generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang]\\n\", \"# %ignore-below # ! 测试代码在最下边\\n\", \"\\n\", \"@info \\\"\\\" LANG_COMMENT_DICT_INLINE LANG_COMMENT_DICT_MULTILINE_HEAD LANG_COMMENT_DICT_MULTILINE_TAIL\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"\\\"【内部】编程语言⇒常用扩展名(不带`.`)\\\"\\n\", \"const LANG_EXTENSION_DICT = Dict{Symbol,String}(\\n\", \" # ! 以下「特殊注释」需要在行首\\n\", \"#= %inline-compiled =# include(\\\"./../src/datas/language_extension_dict.data.jl\\\")\\n\", \" # !【2024-01-27 00:48:32】为了兼容自动生成的测试文件`runtests.jl`,需要使用「相对绝对路径」`./../src/`\\n\", \")\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"【内部】根据编程语言猜测扩展名\\n\", \"- @returns 特定语言的`Symbol` | 语言本身的字符串形式\\n\", \" - @default 如`:aaa => \\\"aaa\\\"`\\n\", \"\\\"\\\"\\\"\\n\", \"get_extension(lang::Symbol) = get(\\n\", \" LANG_EXTENSION_DICT, lang,\\n\", \" string(lang)\\n\", \")\\n\", \"# %ignore-below # ! 测试代码在最下边\\n\", \"\\n\", \"@info \\\"\\\" LANG_EXTENSION_DICT\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# %ignore-cell\\n\", \"let path_examples(path) = joinpath(ROOT_PATH, \\\"examples\\\", path),\\n\", \" notebooks = [\\n\", \" path_examples(\\\"c.ipynb\\\") # C\\n\", \" path_examples(\\\"java.ipynb\\\") # Java\\n\", \" SELF_PATH # Julia # * 直接使用自身\\n\", \" path_examples(\\\"python.ipynb\\\") # Python\\n\", \" path_examples(\\\"rust.ipynb\\\") # Rust\\n\", \" path_examples(\\\"typescript.ipynb\\\") # TypeScript\\n\", \" ] .|> read_ipynb_json .|> IpynbNotebook\\n\" … \" ])\\n\", \"\\n\", \" langs = identify_lang.(notebooks)\\n\", \" @info \\\"识别到的所有语言\\\" langs\\n\", \"\\n\", \" table_comments = [langs generate_comment_inline.(langs) generate_comment_multiline_head.(langs) generate_comment_multiline_tail.(langs)]\\n\", \" @info \\\"生成的所有注释 [语言 单行 多行开头 多行结尾]\\\" table_comments\\n\", \"\\n\", \" @info \\\"生成的常见扩展名 [语言 扩展名]\\\" [langs get_extension.(langs)]\\n\", \"end\"], Dict{String, Any}(), nothing)\n", " ⋮\n", - " IpynbCell(\"code\", [\"\\\"\\\"\\\"\\n\", \"对Markdown的编译\\n\", \"- 📌主要方法:转换成多个单行注释\\n\", \"- ✨不对Markdown单元格作过于特殊的处理\\n\", \" - 仅将其视作语言为 `markdown` 的源码\\n\", \" - 仅在编译后作为程序语言注释\\n\", \"\\n\", \"@example IpynbCell(\\\"markdown\\\", [\\\"# IpynbCompile.jl: 一个通用的Jupyter笔记本集成编译工具\\\"], Dict{String, Any}(), nothing)\\n\", \"(行号为1)将被转换为\\n\", \"```julia\\n\" … \" 全部被忽略!\\n\", \" \\\"\\\"\\\"markdown\\n\", \" local compiled::String = compile_cell(cell; lang=:julia, line_num=1)\\n\", \" @assert compiled == \\\"\\\"\\\"\\\\\\n\", \" # %% [1] markdown\\n\", \" # 这些内容不会被忽略\\n\", \" # \\n\", \" \\\"\\\"\\\"\\n\", \" compiled |> println\\n\", \"end\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export parse_cell, tryparse_cell, eval_cell\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"解析一个单元格\\n\", \"- 🎯将单元格解析成Julia表达式\\n\", \"- 📌使用`Meta.parseall`解析代码\\n\", \" - `Meta.parse`只能解析一个Julia表达式\\n\", \" - 可能会附加上不必要的「:toplevel」表达式\\n\" … \" contains(cell.source[end], \\\"const\\\")\\n\", \" end]\\n\", \" eval_cell(cell_const; lang=:julia)::Base.Docs.Binding\\n\", \"end\\n\", \"\\n\", \"# 尝试对每个单元格进行解析\\n\", \"[\\n\", \" tryparse_cell(cell; lang=:julia, line_num=i)\\n\", \" for (i, cell) in enumerate(codes)\\n\", \"]\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# %ignore-cell # * ↓ 不提供语言,会报错但静默失败\\n\", \"tryparse_cell(codes#= ; lang=:julia =#)\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export compile_notebook\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"编译整个笔记本\\n\", \"- 🎯编译整个笔记本对象,形成相应Julia代码\\n\", \"- 📌整体文本:头部注释+各单元格编译(逐个join(_, '\\\\\\\\n'))\\n\", \"- ⚠️末尾不会附加换行符\\n\", \"- @param notebook 要编译的笔记本对象\\n\" … \" # 根据语言自动追加扩展名,作为目标路径\\n\", \" \\\"\\$path.\\$(get_extension(identify_lang(notebook)))\\\";\\n\", \" # 其它附加参数 #\\n\", \" # 自动从`path`构造编译根目录\\n\", \" root_path=dirname(path),\\n\", \" )\\n\", \"end\\n\", \"\\n\", \"# %ignore-below\\n\", \"compile_notebook(notebook) |> print\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export parse_notebook, tryparse_notebook\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"解析笔记本\\n\", \"@param notebook 笔记本\\n\", \"@param parse_function 解析函数(替代原先`Meta.parseall`的位置)\\n\", \"@param kwargs 附加参数\\n\", \"@return 解析后的Julia表达式(可能含有错误的表达式`:error`)\\n\" … \" try\\n\", \" parse_notebook(args...; kwargs...)\\n\", \" catch e\\n\", \" @warn e\\n\", \" showerror(stderr, e, Base.stacktrace(Base.catch_backtrace()))\\n\", \" nothing\\n\", \" end\\n\", \"\\n\", \"# %ignore-below\\n\", \"@assert tryparse_notebook(notebook) isa Expr\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export compile_notebook\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"编译整个笔记本\\n\", \"- 🎯编译整个笔记本对象,形成相应Julia代码\\n\", \"- 📌整体文本:头部注释+各单元格编译(逐个join(_, '\\\\\\\\n'))\\n\", \"- ⚠️末尾不会附加换行符\\n\", \"- @param notebook 要编译的笔记本对象\\n\" … \" # 根据语言自动追加扩展名,作为目标路径\\n\", \" \\\"\\$path.\\$(get_extension(identify_lang(notebook)))\\\";\\n\", \" # 其它附加参数 #\\n\", \" # 自动从`path`构造编译根目录\\n\", \" root_path=dirname(path),\\n\", \" )\\n\", \"end\\n\", \"# %ignore-below\\n\", \"\\n\", \"compile_notebook(notebook) |> print\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export parse_notebook, tryparse_notebook\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"解析笔记本\\n\", \"@param notebook 笔记本\\n\", \"@param parse_function 解析函数(替代原先`Meta.parseall`的位置)\\n\", \"@param kwargs 附加参数\\n\", \"@return 解析后的Julia表达式(可能含有错误的表达式`:error`)\\n\" … \" try\\n\", \" parse_notebook(args...; kwargs...)\\n\", \" catch e\\n\", \" @warn e\\n\", \" showerror(stderr, e, Base.stacktrace(Base.catch_backtrace()))\\n\", \" nothing\\n\", \" end\\n\", \"# %ignore-below\\n\", \"\\n\", \"@assert tryparse_notebook(notebook) isa Expr\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export eval_notebook, eval_notebook_by_cell\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"【整个】解释并执行Jupyter笔记本\\n\", \"- 📌先解析整个笔记本,然后一次性执行所有代码\\n\", \" - 可以实现一些「编译后可用」的「上下文相关代码」\\n\", \" - 如「将全笔记本代码打包成一个模块」\\n\", \"\\\"\\\"\\\"\\n\" … \" local result::Any = nothing\\n\", \" # 逐个执行\\n\", \" for cell in notebook.cells\\n\", \" result = eval_cell(cell; kwargs...)\\n\", \" end\\n\", \" # 返回最后一个结果\\n\", \" return result\\n\", \"end\\n\", \"\\n\", \"# ! 测试代码放在最后边\"], Dict{String, Any}(), nothing)\n", - " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export include_notebook, include_notebook_by_cell\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"从【字符串】路径解析并【整个】编译执行整个笔记本的代码\\n\", \"- 📌「执行笔记本」已经有`eval_notebook`支持了\\n\", \"- 🎯直接解析并执行`.ipynb`文件\\n\", \"- 📌先加载并编译Jupyter笔记本,再【整个】执行其所有单元格\\n\", \"- 会像`include`一样返回「最后一个执行的单元格的返回值」\\n\" … \"@assert @isdefined IpynbCompile # ! 模块上下文生效:所有代码现在都在模块之中\\n\", \"printstyled(\\\"✅Jupyter笔记本文件引入完成,模块导入成功!\\\\n\\\"; color=:light_green, bold=true)\\n\", \"@show IpynbCompile\\n\", \"println()\\n\", \"\\n\", \"# * 打印导出的所有符号\\n\", \"printstyled(\\\"📜以下为IpynbCompile模块导出的所有\\$(length(names(IpynbCompile)))个符号:\\\\n\\\"; color=:light_blue, bold=true)\\n\", \"for name in names(IpynbCompile)\\n\", \" println(name)\\n\", \"end\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export include_notebook, include_notebook_by_cell\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"从【字符串】路径解析并【整个】编译执行整个笔记本的代码\\n\", \"- 📌「执行笔记本」已经有`eval_notebook`支持了\\n\", \"- 🎯直接解析并执行`.ipynb`文件\\n\", \"- 📌先加载并编译Jupyter笔记本,再【整个】执行其所有单元格\\n\", \"- 会像`include`一样返回「最后一个执行的单元格的返回值」\\n\" … \" source=[\\\"#= %inline-compiled =# include_notebook(\\$(repr(引入路径)))\\\"]\\n\", \" );\\n\", \" lang=:julia\\n\", \" )\\n\", \" @assert startswith(引入后内容, \\\"# %% Jupyter Notebook | Julia\\\")\\n\", \" @assert contains(引入后内容, \\\"println(\\\\\\\"这行会被引入\\\\\\\")\\\")\\n\", \" @assert !contains(引入后内容, \\\"println(\\\\\\\"这行不会被引入\\\\\\\")\\\")\\n\", \" @assert !contains(引入后内容, \\\"println(\\\\\\\"这单元格不会被引入\\\\\\\")\\\")\\n\", \" println(引入后内容)\\n\", \"end\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"# %ignore-cell\\n\", \"# * 递回执行自身代码(自举)\\n\", \"include_notebook(SELF_PATH)\\n\", \"\\n\", \"# 检验是否成功导入\\n\", \"@assert @isdefined IpynbCompile # ! 模块上下文生效:所有代码现在都在模块之中\\n\", \"printstyled(\\\"✅Jupyter笔记本文件引入完成,模块导入成功!\\\\n\\\"; color=:light_green, bold=true)\\n\", \"@show IpynbCompile\\n\", \"println()\\n\", \"\\n\", \"# * 打印导出的所有符号\\n\", \"printstyled(\\\"📜以下为IpynbCompile模块导出的所有\\$(length(names(IpynbCompile)))个符号:\\\\n\\\"; color=:light_blue, bold=true)\\n\", \"for name in names(IpynbCompile)\\n\", \" println(name)\\n\", \"end\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# ! ↓这后边注释的代码只有在编译后才会被执行\\n\", \"# ! 仍然使用多行注释语法,以便统一格式\\n\", \"#= %only-compiled\\n\", \"end # module\\n\", \"%only-compiled =#\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# %ignore-cell # * 自举构建主模块\\n\", \"# * 自编译生成`.jl`源码\\n\", \"let OUT_LIB_FILE = \\\"IpynbCompile.jl\\\" # 直接作为库的主文件\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\", \" write_bytes = compile_notebook(SELF_PATH, joinpath(ROOT_PATH, \\\"src\\\", OUT_LIB_FILE))\\n\", \" printstyled(\\n\", \" \\\"✅Jupyter笔记本「主模块」自编译成功!\\\\n(共写入 \\$write_bytes 个字节)\\\\n\\\";\\n\", \" color=:light_yellow, bold=true\\n\", \" )\\n\", \"end\"], Dict{String, Any}(), nothing)\n", " IpynbCell(\"code\", [\"# %ignore-cell\\n\", \"# * 扫描`src`目录,自动构建主模块\\n\", \"# * - 📝Julia 文件夹遍历:`walkdir`迭代器\\n\", \"# * - 🔗参考:参考:https://stackoverflow.com/questions/58258101/how-to-loop-through-a-folder-of-sub-folders-in-julia\\n\", \"PATH_SRC = \\\".\\\"\\n\", \"let root_folder = PATH_SRC\\n\", \"\\n\", \" # !不能在`runtests.jl`中运行\\n\", \" contains(@__DIR__, \\\"test\\\") && return\\n\", \"\\n\" … \" path => new_path # * 测试Pair\\n\", \" # ! 根目录后续会由`path`自行指定\\n\", \" )\\n\", \" # 输出编译结果\\n\", \" printstyled(\\n\", \" \\\"Compiled: \\$path => \\$new_path\\\\n\\\";\\n\", \" color=:light_green, bold=true\\n\", \" )\\n\", \" end\\n\", \"end\"], Dict{String, Any}(), nothing)\n", @@ -1697,8 +1756,6 @@ " read(f, String) |> JSON.parse\n", " end\n", "\n", - "# ! ↓使用`# %ignore-line`让 编译器/解释器 忽略下一行\n", - "\n", "\n", "\n", "# %% [7] code\n", @@ -1761,8 +1818,6 @@ " kernelspec=json[\"kernelspec\"],\n", ")\n", "\n", - "# ! ↓使用`# %ignore-below`让 编译器/解释器 忽略后续内容\n", - "\n", "\n", "# %% [8] code\n", "export read_notebook\n", @@ -1871,7 +1926,6 @@ " contains(language_text, regex)\n", " end # ! 默认返回`nothing`\n", "\n", - "\n", "# %% [11] code\n", "\"【内部】编程语言⇒单行注释\"\n", "const LANG_COMMENT_DICT_INLINE = Dict{Symbol,String}()\n", @@ -1960,7 +2014,6 @@ "generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang]\n", "\n", "\n", - "\n", "# %% [12] code\n", "\"【内部】编程语言⇒常用扩展名(不带`.`)\"\n", "const LANG_EXTENSION_DICT = Dict{Symbol,String}(\n", @@ -2037,7 +2090,6 @@ "\n", "\n", "\n", - "\n", "# %% [14] code\n", "\"\"\"\n", "【内部】从Notebook生成头部注释\n", @@ -2063,7 +2115,6 @@ "\"\"\"\n", "\n", "\n", - "\n", "# %% [15] code\n", "export IpynbCell\n", "\n", @@ -2138,14 +2189,12 @@ "end\n", "\n", "\n", - "\n", "# %% [17] code\n", "# ! 在此重定向,以便后续外部调用\n", "\"重定向「笔记本」的默认「单元格」类型\"\n", "IpynbNotebook(json) = IpynbNotebook{IpynbCell}(json)\n", "\n", "\n", - "\n", "# %% [18] code\n", "export compile_cell\n", "\n", @@ -2204,7 +2253,6 @@ "\n", "\n", "\n", - "\n", "# %% [21] code\n", "\"\"\"\n", "对代码的编译\n", @@ -2263,12 +2311,22 @@ " # * `%ignore-line` 忽略下一行 | 仅需为行前缀\n", " if startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-line\")\n", " current_line_i += 1 # ! 结合后续递增,跳过下面一行,不让本「特殊注释」行被编译\n", + "\n", " # * `%ignore-below` 忽略下面所有行 | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-below\")\n", " break # ! 结束循环,不再编译后续代码\n", + "\n", " # * `%ignore-cell` 忽略整个单元格 | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-cell\")\n", " return nothing # ! 返回「不编译单元格」的信号\n", + "\n", + " # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀\n", + " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-begin\")\n", + " # 只要后续没有以\"$(generate_comment_inline(lang)) %ignore-end\"开启的行,就不断跳过\n", + " while !startswith(lines[current_line_i], \"$(generate_comment_inline(lang)) %ignore-end\") && current_line_i <= len_lines\n", + " current_line_i += 1 # 忽略性递增\n", + " end # ! 让最终递增跳过\"# %ignore-end\"所在行\n", + "\n", " # * `%include` 读取其所指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %include\")\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", @@ -2276,33 +2334,43 @@ " # 读取内容\n", " local content::String = read(joinpath(root_path, relative_path), String)\n", " result *= content # ! 不会自动添加换行!\n", - " # * `#= %inline-compiled =# include(` 读取后边`include`指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀\n", - " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang)) include(\")\n", + "\n", + " # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀\n", + " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))\")\n", " # 直接作为Julia代码解析\n", " local expr::Expr = Meta.parse(current_line)\n", " #= # * 在Expr中提取相应字符串 | 参考:\n", + " ```\n", " julia> :(include(\"123\")) |> dump\n", " Expr\n", " head: Symbol call\n", " args: Array{Any}((2,))\n", " 1: Symbol include\n", " 2: String \"123\"\n", + " ```\n", + " * JuLISP语法:(call include \"123\")\n", " =#\n", - " if expr.head == :call && expr.args[1] == :include && length(expr.args) > 1\n", + " if expr.head == :call && length(expr.args) > 1\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", - " relative_path = expr.args[2]\n", - " # 读取内容 | if内不再要用local,和上级表达式重复\n", - " content = read(joinpath(root_path, relative_path), String)\n", + " relative_path = Main.eval(expr.args[2]) # * 在主模块上下文中加载计算路径\n", + " local file_path::String = joinpath(root_path, relative_path)\n", + " # * include⇒读取文件内容\n", + " if expr.args[1] == :include\n", + " content = read(file_path, String)\n", + " # * include_notebook⇒读取编译笔记本\n", + " elseif expr.args[1] == :include_notebook\n", + " content = compile_notebook(\n", + " IpynbNotebook(file_path); # 需要构造函数 # ! 直接使用字符串会将其编译为源码文件\n", + " root_path=dirname(file_path), # ! 使用文件自身的根目录\n", + " kwargs..., # 其它附加参数\n", + " )\n", + " end\n", + " # 追加内容\n", " result *= content # ! 不会自动添加换行!\n", " else # 若非`include(路径)`的形式⇒警告\n", " @warn \"非法表达式,内联失败!\" current_line expr\n", " end\n", - " # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀\n", - " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-begin\")\n", - " # 只要后续没有以\"$(generate_comment_inline(lang)) %ignore-end\"开启的行,就不断跳过\n", - " while !startswith(lines[current_line_i], \"$(generate_comment_inline(lang)) %ignore-end\") && current_line_i <= len_lines\n", - " current_line_i += 1 # 忽略性递增\n", - " end # ! 让最终递增跳过\"# %ignore-end\"所在行\n", + "\n", " # * `%only-compiled` 仅编译后可用(多行) | 仅需为行前缀\n", " elseif (\n", " startswith(current_line, \"$(generate_comment_multiline_head(lang)) %only-compiled\") ||\n", @@ -2310,6 +2378,7 @@ " )\n", " # ! 不做任何事情,跳过当前行\n", " # * 否则:直接将行追加到结果\n", + "\n", " else\n", " result *= current_line\n", " end\n", @@ -2323,7 +2392,6 @@ "end\n", "\n", "\n", - "\n", "# %% [22] code\n", "\"\"\"\n", "对Markdown的编译\n", @@ -2434,7 +2502,6 @@ "\n", "\n", "\n", - "\n", "# %% [25] code\n", "export compile_notebook\n", "\n", @@ -2515,7 +2582,6 @@ "end\n", "\n", "\n", - "\n", "# %% [26] code\n", "export parse_notebook, tryparse_notebook\n", "\n", @@ -2550,7 +2616,6 @@ " end\n", "\n", "\n", - "\n", "# %% [27] code\n", "export eval_notebook, eval_notebook_by_cell\n", "\n", @@ -2620,7 +2685,7 @@ "\n", "\n", "\n", - "# %% [29] code\n", + "# %% [30] code\n", "# ! ↓这后边注释的代码只有在编译后才会被执行\n", "# ! 仍然使用多行注释语法,以便统一格式\n", "end # module\n", @@ -2690,12 +2755,22 @@ " # * `%ignore-line` 忽略下一行 | 仅需为行前缀\n", " if startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-line\")\n", " current_line_i += 1 # ! 结合后续递增,跳过下面一行,不让本「特殊注释」行被编译\n", + "\n", " # * `%ignore-below` 忽略下面所有行 | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-below\")\n", " break # ! 结束循环,不再编译后续代码\n", + "\n", " # * `%ignore-cell` 忽略整个单元格 | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-cell\")\n", " return nothing # ! 返回「不编译单元格」的信号\n", + "\n", + " # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀\n", + " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-begin\")\n", + " # 只要后续没有以\"$(generate_comment_inline(lang)) %ignore-end\"开启的行,就不断跳过\n", + " while !startswith(lines[current_line_i], \"$(generate_comment_inline(lang)) %ignore-end\") && current_line_i <= len_lines\n", + " current_line_i += 1 # 忽略性递增\n", + " end # ! 让最终递增跳过\"# %ignore-end\"所在行\n", + "\n", " # * `%include` 读取其所指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %include\")\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", @@ -2703,33 +2778,43 @@ " # 读取内容\n", " local content::String = read(joinpath(root_path, relative_path), String)\n", " result *= content # ! 不会自动添加换行!\n", - " # * `#= %inline-compiled =# include(` 读取后边`include`指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀\n", - " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang)) include(\")\n", + "\n", + " # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀\n", + " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))\")\n", " # 直接作为Julia代码解析\n", " local expr::Expr = Meta.parse(current_line)\n", " #= # * 在Expr中提取相应字符串 | 参考:\n", + " ```\n", " julia> :(include(\"123\")) |> dump\n", " Expr\n", " head: Symbol call\n", " args: Array{Any}((2,))\n", " 1: Symbol include\n", " 2: String \"123\"\n", + " ```\n", + " * JuLISP语法:(call include \"123\")\n", " =#\n", - " if expr.head == :call && expr.args[1] == :include && length(expr.args) > 1\n", + " if expr.head == :call && length(expr.args) > 1\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", - " relative_path = expr.args[2]\n", - " # 读取内容 | if内不再要用local,和上级表达式重复\n", - " content = read(joinpath(root_path, relative_path), String)\n", + " relative_path = Main.eval(expr.args[2]) # * 在主模块上下文中加载计算路径\n", + " local file_path::String = joinpath(root_path, relative_path)\n", + " # * include⇒读取文件内容\n", + " if expr.args[1] == :include\n", + " content = read(file_path, String)\n", + " # * include_notebook⇒读取编译笔记本\n", + " elseif expr.args[1] == :include_notebook\n", + " content = compile_notebook(\n", + " IpynbNotebook(file_path); # 需要构造函数 # ! 直接使用字符串会将其编译为源码文件\n", + " root_path=dirname(file_path), # ! 使用文件自身的根目录\n", + " kwargs..., # 其它附加参数\n", + " )\n", + " end\n", + " # 追加内容\n", " result *= content # ! 不会自动添加换行!\n", " else # 若非`include(路径)`的形式⇒警告\n", " @warn \"非法表达式,内联失败!\" current_line expr\n", " end\n", - " # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀\n", - " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-begin\")\n", - " # 只要后续没有以\"$(generate_comment_inline(lang)) %ignore-end\"开启的行,就不断跳过\n", - " while !startswith(lines[current_line_i], \"$(generate_comment_inline(lang)) %ignore-end\") && current_line_i <= len_lines\n", - " current_line_i += 1 # 忽略性递增\n", - " end # ! 让最终递增跳过\"# %ignore-end\"所在行\n", + "\n", " # * `%only-compiled` 仅编译后可用(多行) | 仅需为行前缀\n", " elseif (\n", " startswith(current_line, \"$(generate_comment_multiline_head(lang)) %only-compiled\") ||\n", @@ -2737,6 +2822,7 @@ " )\n", " # ! 不做任何事情,跳过当前行\n", " # * 否则:直接将行追加到结果\n", + "\n", " else\n", " result *= current_line\n", " end\n", @@ -2748,9 +2834,9 @@ " # 最后返回所有行 # ! 「在最后一行和先前所有行的换行符一致」在行编译后方运行\n", " return result\n", "end\n", - "\n", "# %ignore-below\n", "\n", + "# 测试`%include`\n", "let 引入路径 = joinpath(ROOT_PATH, \"test\", \"%include.test.jl\")\n", " # 放置测试脚本\n", " 预期引入内容 = \"\"\"\\\n", @@ -2907,7 +2993,7 @@ { "data": { "text/plain": [ - "33-element Vector{Expr}:\n", + "34-element Vector{Expr}:\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:10 =#\u001b[39m), :($(Expr(:incomplete, Base.Meta.ParseError(\"ParseError:\\n# Error @ none:10:2\\nmodule IpynbCompile # 后续编译后会变为模块上下文\\n\\n#└ ── premature end of input\", Base.JuliaSyntax.ParseError(Base.JuliaSyntax.SourceFile(\"# %% [1] code\\n# ! ↓这后边注释的代码只有在编译后才会被执行\\n# ! 使用多行注释/块注释的语法,\\n# ! 以`#= %only-compiled`行*开头*\\n# ! 以`%only-compiled =#`行*结尾*\\n\\\"\\\"\\\"\\nIpynbCompile 主模块\\n\\\"\\\"\\\"\\nmodule IpynbCompile # 后续编译后会变为模块上下文\\n\\n\", 0, \"none\", 1, [1, 15, 80, 125, 167, 209, 213, 236, 240, 302, 303]), Base.JuliaSyntax.Diagnostic[Base.JuliaSyntax.Diagnostic(303, 302, :error, \"premature end of input\"), Base.JuliaSyntax.Diagnostic(303, 302, :error, \"Expected `end`\")], :block))))))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:2 =#\u001b[39m), :(import JSON))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:2 =#\u001b[39m), :(\u001b[90m#= none:2 =#\u001b[39m Core.@doc \"JSON常用的字典\" const JSONDict{ValueType} = (Dict{String, ValueType} where ValueType)), :(\u001b[90m#= none:5 =#\u001b[39m), :(\u001b[90m#= none:5 =#\u001b[39m Core.@doc \"默认解析出来的JSON字典(与`JSONDict`有本质不同,会影响到后续方法分派,并可能导致歧义)\" const JSONDictAny = JSONDict{Any}))))\n", @@ -3002,15 +3088,6 @@ " end))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", " ⋮\n", - " :($(Expr(:toplevel, :(\u001b[90m#= none:2 =#\u001b[39m), :(\u001b[90m#= none:2 =#\u001b[39m Core.@doc \"对Markdown的编译\\n- 📌主要方法:转换成多个单行注释\\n- ✨不对Markdown单元格作过于特殊的处理\\n - 仅将其视作语言为 `markdown` 的源码\\n - 仅在编译后作为程序语言注释\\n\\n@example IpynbCell(\\\"markdown\\\", [\\\"# IpynbCompile.jl: 一个通用的Jupyter笔记本集成编译工具\\\"], Dict{String, Any}(), nothing)\\n(行号为1)将被转换为\\n```julia\\n# %% [1] markdown\\n# # IpynbCompile.jl: 一个通用的Jupyter笔记本集成编译工具\\n```\\n# ↑末尾附带换行符\\n\" function compile_cell(::Val{:markdown}, cell::IpynbCell; lang::Symbol, kwargs...)\n", - " \u001b[90m#= none:17 =#\u001b[39m\n", - " \u001b[90m#= none:18 =#\u001b[39m\n", - " local code::Union{String, Nothing} = compile_code_lines(cell; lang = :markdown)\n", - " \u001b[90m#= none:23 =#\u001b[39m\n", - " isnothing(code) && return \"\"\n", - " \u001b[90m#= none:25 =#\u001b[39m\n", - " return \"$(compile_cell_head(cell; lang, kwargs...))$(generate_comment_inline(lang) * ' ' * replace(code, '\\n' => '\\n' * generate_comment_inline(lang) * ' '))\\n\"\n", - " end))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:2 =#\u001b[39m), :(export parse_cell, tryparse_cell, eval_cell), :(\u001b[90m#= none:4 =#\u001b[39m), :(\u001b[90m#= none:4 =#\u001b[39m Core.@doc \"解析一个单元格\\n- 🎯将单元格解析成Julia表达式\\n- 📌使用`Meta.parseall`解析代码\\n - `Meta.parse`只能解析一个Julia表达式\\n - 可能会附加上不必要的「:toplevel」表达式\\n@param cell 单元格\\n@param parse_function 解析函数(替代原先`Meta.parseall`的位置)\\n@param kwargs 附加参数\\n@return 解析后的Julia表达式 | nothing(不可执行)\\n\" function parse_cell(cell::IpynbCell; parse_function = Meta.parseall, kwargs...)\n", " \u001b[90m#= none:15 =#\u001b[39m\n", " \u001b[90m#= none:18 =#\u001b[39m\n", @@ -3104,7 +3181,8 @@ " \u001b[90m#= none:27 =#\u001b[39m\n", " eval_notebook_by_cell((path |> read_ipynb_json) |> IpynbNotebook{IpynbCell}; kwargs...)\n", " end))))\n", - " :($(Expr(:toplevel, :(\u001b[90m#= none:4 =#\u001b[39m), :($(Expr(:error, Base.Meta.ParseError(\"ParseError:\\n# Error @ none:4:1\\n# ! 仍然使用多行注释语法,以便统一格式\\nend # module\\n└─┘ ── invalid identifier\", Base.JuliaSyntax.ParseError(Base.JuliaSyntax.SourceFile(\"# %% [29] code\\n# ! ↓这后边注释的代码只有在编译后才会被执行\\n# ! 仍然使用多行注释语法,以便统一格式\\nend # module\\n\\n\", 0, \"none\", 1, [1, 16, 81, 137, 150, 151]), Base.JuliaSyntax.Diagnostic[Base.JuliaSyntax.Diagnostic(137, 139, :error, \"invalid identifier\")], :none))))))))\n", + " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", + " :($(Expr(:toplevel, :(\u001b[90m#= none:4 =#\u001b[39m), :($(Expr(:error, Base.Meta.ParseError(\"ParseError:\\n# Error @ none:4:1\\n# ! 仍然使用多行注释语法,以便统一格式\\nend # module\\n└─┘ ── invalid identifier\", Base.JuliaSyntax.ParseError(Base.JuliaSyntax.SourceFile(\"# %% [30] code\\n# ! ↓这后边注释的代码只有在编译后才会被执行\\n# ! 仍然使用多行注释语法,以便统一格式\\nend # module\\n\\n\", 0, \"none\", 1, [1, 16, 81, 137, 150, 151]), Base.JuliaSyntax.Diagnostic[Base.JuliaSyntax.Diagnostic(137, 139, :error, \"invalid identifier\")], :none))))))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", @@ -3190,7 +3268,6 @@ "eval_cell(code_or_codes; eval_function=Main.eval, kwargs...) = eval_function(\n", " parse_cell(code_or_codes; kwargs...)\n", ")\n", - "\n", "# %ignore-below\n", "\n", "# 执行其中一个代码单元格 # * 参考「预置语法糖」\n", @@ -3301,7 +3378,7 @@ "output_type": "stream", "text": [ "# %% Jupyter Notebook | Julia 1.10.0 @ julia | format 2~4\n", - "# % language_info: {\"file_extension\":\".jl\",\"mimetype\":\"application/julia\",\"name\":\"julia\",\"version\":\"1.9.1\"}\n", + "# % language_info: {\"file_extension\":\".jl\",\"mimetype\":\"application/julia\",\"name\":\"julia\",\"version\":\"1.10.0\"}\n", "# % kernelspec: {\"name\":\"julia-1.10\",\"display_name\":\"Julia 1.10.0\",\"language\":\"julia\"}\n", "# % nbformat: 4\n", "# % nbformat_minor: 2\n", @@ -3520,7 +3597,7 @@ "# ##### 文件引入\n", "\n", "# %% [27] markdown\n", - "# 主要用途:结合「仅编译后可用」实现「外部代码内联」\n", + "# 主要用途:结合「仅编译后可用」简单实现「外部代码内联」\n", "# \n", "# - 如:集成某些**中小型映射表**,整合零散源码文件……\n", "\n", @@ -3529,8 +3606,8 @@ "# \n", "# ```julia\n", "# const square_map_dict = # 这里的等号可以另起一行\n", - "# # % include to_include.jl \n", - "# # ↑ 上面一行会被替换成数据\n", + "# # %include to_include.jl \n", + "# # ↑ 上面一行会被替换成文件内容\n", "# ```\n", "# \n", "# 编译前@和笔记本**同目录**下的`to_include.jl`中:\n", @@ -3563,25 +3640,72 @@ "# 📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白\n", "\n", "# %% [29] markdown\n", - "# ## 参考\n", + "# ##### 文件内联\n", "\n", "# %% [30] markdown\n", + "# 主要用途:为「文件引入」提供一个快捷方式,并**支持「编译后内联笔记本」**\n", + "# \n", + "# - 如:编译并集成**其它笔记本**到该文件中\n", + "\n", + "# %% [31] markdown\n", + "# 编译前@笔记本单元格:\n", + "# \n", + "# ```julia\n", + "# const square_map_dict = # 这里的等号可以另起一行\n", + "# #= %inline-compiled =# include(\"to_include.jl\")\n", + "# # ↑ 上面一行会被替换成文件内容\n", + "# # * 若为使用`include_notebook`引入的笔记本,则会被替换为编译后的笔记本内容\n", + "# ```\n", + "# \n", + "# 编译前@和笔记本**同目录**下的`to_include.jl`中:\n", + "# ↓文件末尾有换行符\n", + "# \n", + "# ```julia\n", + "# # 这是一个要被引入的外部字典对象\n", + "# Dict([\n", + "# 1 => 1\n", + "# 2 => 4\n", + "# 3 => 9\n", + "# # ...\n", + "# ])\n", + "# ```\n", + "# \n", + "# 编译后:\n", + "# \n", + "# ```julia\n", + "# const square_map_dict = # 这里的等号可以另起一行\n", + "# # 这是一个要被引入的外部字典对象\n", + "# Dict([\n", + "# 1 => 1\n", + "# 2 => 4\n", + "# 3 => 9\n", + "# # ...\n", + "# ])\n", + "# # ↑ 上面一行会被替换成数据\n", + "# ```\n", + "# \n", + "# 📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白\n", + "\n", + "# %% [32] markdown\n", + "# ## 参考\n", + "\n", + "# %% [33] markdown\n", "# - 本Julia库的灵感来源:[Promises.jl/src/notebook.jl](https://github.com/fonsp/Promises.jl/blob/main/src/notebook.jl)\n", "# - 源库使用了 [**Pluto.jl**](https://github.com/fonsp/Pluto.jl) 的「笔记本导出」功能\n", "# - **Jupyter Notebook** 文件格式(JSON):[🔗nbformat.readthedocs.io](https://nbformat.readthedocs.io/en/latest/format_description.html#notebook-file-format)\n", "\n", - "# %% [31] markdown\n", + "# %% [34] markdown\n", "# \n", "# \n", "# ⚠️该单元格首行注释用于截止生成`README.md`(包括自身)\n", "\n", - "# %% [32] markdown\n", + "# %% [35] markdown\n", "# ## 建立模块上下文\n", "\n", - "# %% [33] markdown\n", + "# %% [36] markdown\n", "# 📌使用 `# %only-compiled` 控制 `module` 代码,生成模块上下文\n", "\n", - "# %% [34] code\n", + "# %% [37] code\n", "# ! ↓这后边注释的代码只有在编译后才会被执行\n", "# ! 使用多行注释/块注释的语法,\n", "# ! 以`#= %only-compiled`行*开头*\n", @@ -3592,32 +3716,32 @@ "module IpynbCompile # 后续编译后会变为模块上下文\n", "\n", "\n", - "# %% [35] markdown\n", + "# %% [38] markdown\n", "# ## 前置模块\n", "\n", - "# %% [36] markdown\n", + "# %% [39] markdown\n", "# ### 导入库\n", "\n", - "# %% [37] code\n", + "# %% [40] code\n", "import JSON\n", "\n", - "# %% [38] markdown\n", + "# %% [41] markdown\n", "# ### 预置语法糖\n", "\n", - "# %% [39] code\n", + "# %% [42] code\n", "\"JSON常用的字典\"\n", "const JSONDict{ValueType} = Dict{String,ValueType} where ValueType\n", "\n", "\"默认解析出来的JSON字典(与`JSONDict`有本质不同,会影响到后续方法分派,并可能导致歧义)\"\n", "const JSONDictAny = JSONDict{Any}\n", "\n", - "# %% [40] markdown\n", + "# %% [43] markdown\n", "# ### 兼容+注意事项\n", "\n", - "# %% [41] code\n", + "# %% [44] code\n", "import Base: @kwdef # 兼容Julia 1.8⁻\n", "\n", - "# %% [42] markdown\n", + "# %% [45] markdown\n", "# - ✅兼容 @ Julia **1.5⁺**:`include`自Julia **1.5**方可用\n", "# - 🔒锁定最低Julia版本为**1.5**\n", "# - ❌兼容 @ Julia **1.6⁻**:「多行字符串」自Julia **1.7**方可使用\"\\【换行】\"取消换行\n", @@ -3630,13 +3754,13 @@ "# - 📄错误信息:`LoadError: syntax: type declarations on global variables are not yet supported`\n", "# - ⚠️禁止在`const`定义的变量中标注类型(Julia运行时会自动推导)\n", "\n", - "# %% [43] markdown\n", + "# %% [46] markdown\n", "# ## 读取解析Jupyter笔记本(`.ipynb`文件)\n", "\n", - "# %% [44] markdown\n", + "# %% [47] markdown\n", "# ### 读取文件(JSON)\n", "\n", - "# %% [45] code\n", + "# %% [48] code\n", "export read_ipynb_json\n", "\n", "\"\"\"\n", @@ -3649,13 +3773,11 @@ " read(f, String) |> JSON.parse\n", " end\n", "\n", - "# ! ↓使用`# %ignore-line`让 编译器/解释器 忽略下一行\n", - "\n", "\n", - "# %% [46] markdown\n", + "# %% [49] markdown\n", "# ### 解析文件元信息\n", "\n", - "# %% [47] markdown\n", + "# %% [50] markdown\n", "# Jupyter Notebook元数据 格式参考\n", "# \n", "# ```yaml\n", @@ -3680,7 +3802,7 @@ "# }\n", "# ```\n", "\n", - "# %% [48] markdown\n", + "# %% [51] markdown\n", "# Jupyter Notebook Cell 格式参考\n", "# \n", "# 共有:\n", @@ -3724,7 +3846,7 @@ "# }\n", "# ```\n", "\n", - "# %% [49] markdown\n", + "# %% [52] markdown\n", "# 当前Julia笔记本 元数据:\n", "# \n", "# ```json\n", @@ -3746,13 +3868,13 @@ "# (截止至2024-01-16)\n", "\n", "\n", - "# %% [51] markdown\n", + "# %% [54] markdown\n", "# ## 解析Jupyter笔记本(Julia `struct`)\n", "\n", - "# %% [52] markdown\n", + "# %% [55] markdown\n", "# ### 定义「笔记本」结构\n", "\n", - "# %% [53] code\n", + "# %% [56] code\n", "export IpynbNotebook, IpynbNotebookMetadata\n", "\n", "\"\"\"\n", @@ -3812,25 +3934,23 @@ " kernelspec=json[\"kernelspec\"],\n", ")\n", "\n", - "# ! ↓使用`# %ignore-below`让 编译器/解释器 忽略后续内容\n", - "\n", "\n", - "# %% [54] markdown\n", + "# %% [57] markdown\n", "# ### 读取笔记本 总函数\n", "\n", - "# %% [55] markdown\n", + "# %% [58] markdown\n", "# 根据路径读取笔记本\n", "\n", - "# %% [56] code\n", + "# %% [59] code\n", "export read_notebook\n", "\n", "\"从路径读取Jupyter笔记本(`struct IpynbNotebook`)\"\n", "read_notebook(path::AbstractString)::IpynbNotebook = IpynbNotebook(read_ipynb_json(path))\n", "\n", - "# %% [57] markdown\n", + "# %% [60] markdown\n", "# 方便引入笔记本的字符串宏\n", "\n", - "# %% [58] code\n", + "# %% [61] code\n", "export @notebook_str\n", "\n", "macro notebook_str(path::AbstractString)\n", @@ -3838,13 +3958,13 @@ "end\n", "\n", "\n", - "# %% [59] markdown\n", + "# %% [62] markdown\n", "# ### 解析/生成 笔记本信息\n", "\n", - "# %% [60] markdown\n", + "# %% [63] markdown\n", "# #### 识别编程语言\n", "\n", - "# %% [61] code\n", + "# %% [64] code\n", "\"【内部】编程语言⇒正则表达式 识别字典\"\n", "const LANG_IDENTIFY_DICT = Dict{Symbol,Regex}(\n", " lang => Regex(\"^(?:$regex_str)\\$\") # ! ←必须头尾精确匹配(不然就会把`JavaScript`认成`r`)\n", @@ -3937,15 +4057,14 @@ " contains(language_text, regex)\n", " end # ! 默认返回`nothing`\n", "\n", - "\n", - "# %% [62] markdown\n", + "# %% [65] markdown\n", "# #### 根据编程语言生成注释\n", "# \n", "# - 生成的注释会用于「行开头」识别\n", "# - 如:`// %ignore-cell` (C系列)\n", "# - 如:`# %ignore-cell` (Python/Julia)\n", "\n", - "# %% [63] code\n", + "# %% [66] code\n", "\"【内部】编程语言⇒单行注释\"\n", "const LANG_COMMENT_DICT_INLINE = Dict{Symbol,String}()\n", "\n", @@ -4033,11 +4152,10 @@ "generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang]\n", "\n", "\n", - "\n", - "# %% [64] markdown\n", + "# %% [67] markdown\n", "# #### 生成常用扩展名\n", "\n", - "# %% [65] code\n", + "# %% [68] code\n", "\"【内部】编程语言⇒常用扩展名(不带`.`)\"\n", "const LANG_EXTENSION_DICT = Dict{Symbol,String}(\n", " # ! 以下「特殊注释」需要在行首\n", @@ -4112,18 +4230,17 @@ ")\n", "\n", "\n", - "\n", - "# %% [66] markdown\n", + "# %% [69] markdown\n", "# #### 解析/生成 测试\n", "\n", "\n", - "# %% [68] markdown\n", + "# %% [71] markdown\n", "# ### Notebook编译/头部注释\n", "# \n", "# - 🎯标注 版本信息\n", "# - 🎯标注 各类元数据\n", "\n", - "# %% [69] code\n", + "# %% [72] code\n", "\"\"\"\n", "【内部】从Notebook生成头部注释\n", "- ⚠️末尾有换行\n", @@ -4148,17 +4265,16 @@ "\"\"\"\n", "\n", "\n", - "\n", - "# %% [70] markdown\n", + "# %% [73] markdown\n", "# ## 解析处理单元格\n", "\n", - "# %% [71] markdown\n", + "# %% [74] markdown\n", "# ### 定义「单元格」\n", "\n", - "# %% [72] markdown\n", + "# %% [75] markdown\n", "# 定义结构类型\n", "\n", - "# %% [73] code\n", + "# %% [76] code\n", "export IpynbCell\n", "\n", "\"\"\"\n", @@ -4196,10 +4312,10 @@ " )...)\n", "end\n", "\n", - "# %% [74] markdown\n", + "# %% [77] markdown\n", "# 定义快捷字符串宏\n", "\n", - "# %% [75] code\n", + "# %% [78] code\n", "export @cell_str\n", "\n", "\"🎯将字符串拆分成单元格各行(区分末尾换行)\"\n", @@ -4235,24 +4351,22 @@ "end\n", "\n", "\n", - "\n", - "# %% [76] markdown\n", + "# %% [79] markdown\n", "# 结合笔记本,重定向&调用测试处理\n", "\n", - "# %% [77] code\n", + "# %% [80] code\n", "# ! 在此重定向,以便后续外部调用\n", "\"重定向「笔记本」的默认「单元格」类型\"\n", "IpynbNotebook(json) = IpynbNotebook{IpynbCell}(json)\n", "\n", "\n", - "\n", - "# %% [78] markdown\n", + "# %% [81] markdown\n", "# ## 编译单元格\n", "\n", - "# %% [79] markdown\n", + "# %% [82] markdown\n", "# ### 编译/入口\n", "\n", - "# %% [80] code\n", + "# %% [83] code\n", "export compile_cell\n", "\n", "\"\"\"\n", @@ -4287,10 +4401,10 @@ " for (line_num, cell) in enumerate(cells) # ! ←一定是顺序遍历\n", " ), '\\n')\n", "\n", - "# %% [81] markdown\n", + "# %% [84] markdown\n", "# ### 编译/单元格标头\n", "\n", - "# %% [82] code\n", + "# %% [85] code\n", "\"\"\"\n", "【内部】对整个单元格的「类型标头」编译\n", "- 🎯生成一行注释,标识单元格\n", @@ -4312,15 +4426,14 @@ "\"\"\" # ! ←末尾附带换行符\n", "\n", "\n", - "\n", - "# %% [83] markdown\n", + "# %% [86] markdown\n", "# ### 编译/代码\n", "\n", "\n", - "# %% [85] markdown\n", + "# %% [88] markdown\n", "# 主编译方法\n", "\n", - "# %% [86] code\n", + "# %% [89] code\n", "\"\"\"\n", "对代码的编译\n", "- @param cell 所需编译的单元格\n", @@ -4378,12 +4491,22 @@ " # * `%ignore-line` 忽略下一行 | 仅需为行前缀\n", " if startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-line\")\n", " current_line_i += 1 # ! 结合后续递增,跳过下面一行,不让本「特殊注释」行被编译\n", + "\n", " # * `%ignore-below` 忽略下面所有行 | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-below\")\n", " break # ! 结束循环,不再编译后续代码\n", + "\n", " # * `%ignore-cell` 忽略整个单元格 | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-cell\")\n", " return nothing # ! 返回「不编译单元格」的信号\n", + "\n", + " # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀\n", + " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-begin\")\n", + " # 只要后续没有以\"$(generate_comment_inline(lang)) %ignore-end\"开启的行,就不断跳过\n", + " while !startswith(lines[current_line_i], \"$(generate_comment_inline(lang)) %ignore-end\") && current_line_i <= len_lines\n", + " current_line_i += 1 # 忽略性递增\n", + " end # ! 让最终递增跳过\"# %ignore-end\"所在行\n", + "\n", " # * `%include` 读取其所指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %include\")\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", @@ -4391,33 +4514,43 @@ " # 读取内容\n", " local content::String = read(joinpath(root_path, relative_path), String)\n", " result *= content # ! 不会自动添加换行!\n", - " # * `#= %inline-compiled =# include(` 读取后边`include`指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀\n", - " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang)) include(\")\n", + "\n", + " # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀\n", + " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))\")\n", " # 直接作为Julia代码解析\n", " local expr::Expr = Meta.parse(current_line)\n", " #= # * 在Expr中提取相应字符串 | 参考:\n", + " ```\n", " julia> :(include(\"123\")) |> dump\n", " Expr\n", " head: Symbol call\n", " args: Array{Any}((2,))\n", " 1: Symbol include\n", " 2: String \"123\"\n", + " ```\n", + " * JuLISP语法:(call include \"123\")\n", " =#\n", - " if expr.head == :call && expr.args[1] == :include && length(expr.args) > 1\n", + " if expr.head == :call && length(expr.args) > 1\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", - " relative_path = expr.args[2]\n", - " # 读取内容 | if内不再要用local,和上级表达式重复\n", - " content = read(joinpath(root_path, relative_path), String)\n", + " relative_path = Main.eval(expr.args[2]) # * 在主模块上下文中加载计算路径\n", + " local file_path::String = joinpath(root_path, relative_path)\n", + " # * include⇒读取文件内容\n", + " if expr.args[1] == :include\n", + " content = read(file_path, String)\n", + " # * include_notebook⇒读取编译笔记本\n", + " elseif expr.args[1] == :include_notebook\n", + " content = compile_notebook(\n", + " IpynbNotebook(file_path); # 需要构造函数 # ! 直接使用字符串会将其编译为源码文件\n", + " root_path=dirname(file_path), # ! 使用文件自身的根目录\n", + " kwargs..., # 其它附加参数\n", + " )\n", + " end\n", + " # 追加内容\n", " result *= content # ! 不会自动添加换行!\n", " else # 若非`include(路径)`的形式⇒警告\n", " @warn \"非法表达式,内联失败!\" current_line expr\n", " end\n", - " # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀\n", - " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %ignore-begin\")\n", - " # 只要后续没有以\"$(generate_comment_inline(lang)) %ignore-end\"开启的行,就不断跳过\n", - " while !startswith(lines[current_line_i], \"$(generate_comment_inline(lang)) %ignore-end\") && current_line_i <= len_lines\n", - " current_line_i += 1 # 忽略性递增\n", - " end # ! 让最终递增跳过\"# %ignore-end\"所在行\n", + "\n", " # * `%only-compiled` 仅编译后可用(多行) | 仅需为行前缀\n", " elseif (\n", " startswith(current_line, \"$(generate_comment_multiline_head(lang)) %only-compiled\") ||\n", @@ -4425,6 +4558,7 @@ " )\n", " # ! 不做任何事情,跳过当前行\n", " # * 否则:直接将行追加到结果\n", + "\n", " else\n", " result *= current_line\n", " end\n", @@ -4438,11 +4572,10 @@ "end\n", "\n", "\n", - "\n", - "# %% [87] markdown\n", + "# %% [90] markdown\n", "# ### 编译/Markdown\n", "\n", - "# %% [88] code\n", + "# %% [91] code\n", "\"\"\"\n", "对Markdown的编译\n", "- 📌主要方法:转换成多个单行注释\n", @@ -4477,10 +4610,10 @@ "\n", "\n", "\n", - "# %% [89] markdown\n", + "# %% [92] markdown\n", "# ## 解析执行单元格\n", "\n", - "# %% [90] markdown\n", + "# %% [93] markdown\n", "# 🎯将单元格解析**编译**成Julia表达式,并可直接作为代码执行\n", "# - 【核心】解释:`parse_cell`\n", "# - 📌基本是`compile_cell` ∘ `Meta.parse`的复合\n", @@ -4492,7 +4625,7 @@ "# - 📌基本是`parse_cell` ∘ `eval`的复合\n", "# - ⚙️可任意指定其中的`eval`函数\n", "\n", - "# %% [91] code\n", + "# %% [94] code\n", "export parse_cell, tryparse_cell, eval_cell\n", "\n", "\"\"\"\n", @@ -4567,14 +4700,13 @@ "\n", "\n", "\n", - "\n", - "# %% [93] markdown\n", + "# %% [96] markdown\n", "# ## 编译解析笔记本\n", "\n", - "# %% [94] markdown\n", + "# %% [97] markdown\n", "# 编译笔记本\n", "\n", - "# %% [95] code\n", + "# %% [98] code\n", "export compile_notebook\n", "\n", "\"\"\"\n", @@ -4654,11 +4786,10 @@ "end\n", "\n", "\n", - "\n", - "# %% [96] markdown\n", + "# %% [99] markdown\n", "# 解析笔记本\n", "\n", - "# %% [97] code\n", + "# %% [100] code\n", "export parse_notebook, tryparse_notebook\n", "\n", "\"\"\"\n", @@ -4692,14 +4823,13 @@ " end\n", "\n", "\n", - "\n", - "# %% [98] markdown\n", + "# %% [101] markdown\n", "# ## 执行笔记本\n", "\n", - "# %% [99] markdown\n", + "# %% [102] markdown\n", "# 执行笔记本\n", "\n", - "# %% [100] code\n", + "# %% [103] code\n", "export eval_notebook, eval_notebook_by_cell\n", "\n", "\"\"\"\n", @@ -4732,10 +4862,10 @@ "\n", "# ! 测试代码放在最后边\n", "\n", - "# %% [101] markdown\n", + "# %% [104] markdown\n", "# 引入笔记本\n", "\n", - "# %% [102] code\n", + "# %% [105] code\n", "export include_notebook, include_notebook_by_cell\n", "\n", "\"\"\"\n", @@ -4771,10 +4901,10 @@ "\n", "\n", "\n", - "# %% [103] markdown\n", + "# %% [107] markdown\n", "# ## 关闭模块上下文\n", "\n", - "# %% [104] code\n", + "# %% [108] code\n", "# ! ↓这后边注释的代码只有在编译后才会被执行\n", "# ! 仍然使用多行注释语法,以便统一格式\n", "end # module\n", @@ -4873,8 +5003,8 @@ " root_path=dirname(path),\n", " )\n", "end\n", - "\n", "# %ignore-below\n", + "\n", "compile_notebook(notebook) |> print" ] }, @@ -4924,8 +5054,8 @@ " showerror(stderr, e, Base.stacktrace(Base.catch_backtrace()))\n", " nothing\n", " end\n", - "\n", "# %ignore-below\n", + "\n", "@assert tryparse_notebook(notebook) isa Expr" ] }, @@ -5011,29 +5141,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[92m\u001b[1m✅Jupyter笔记本文件引入完成,模块导入成功!\u001b[22m\u001b[39m\n", - "IpynbCompile = Main.IpynbCompile\n", + "# %% Jupyter Notebook | Julia 1.10.0 @ julia | format 2~4\n", + "# % language_info: {\"name\":\"julia\"}\n", + "# % kernelspec: {\"name\":\"julia-1.10\",\"display_name\":\"Julia 1.10.0\",\"language\":\"julia\"}\n", + "# % nbformat: 4\n", + "# % nbformat_minor: 2\n", "\n", - "\u001b[94m\u001b[1m📜以下为IpynbCompile模块导出的所有19个符号:\u001b[22m\u001b[39m\n", - "@cell_str\n", - "@notebook_str\n", - "IpynbCell\n", - "IpynbCompile\n", - "IpynbNotebook\n", - "IpynbNotebookMetadata\n", - "compile_cell\n", - "compile_notebook\n", - "eval_cell\n", - "eval_notebook\n", - "eval_notebook_by_cell\n", - "include_notebook\n", - "include_notebook_by_cell\n", - "parse_cell\n", - "parse_notebook\n", - "read_ipynb_json\n", - "read_notebook\n", - "tryparse_cell\n", - "tryparse_notebook\n" + "\n", + "# %% [2] code\n", + "println(\"这行会被引入\")\n", + "\n", + "\n" ] } ], @@ -5072,9 +5190,97 @@ " # 其它附加参数(如「编译根目录」)\n", " kwargs...\n", ")\n", - "\n", "# %ignore-below\n", "\n", + "# 测试`%inline-compiled`\n", + "let 引入路径 = joinpath(ROOT_PATH, \"test\", \"%inline-compiled.test.ipynb\")\n", + " # 放置测试脚本\n", + " ispath(引入路径) || write(引入路径, \"\"\"{\n", + " \"cells\": [\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"# %ignore-cell\",\n", + " \"print(\\\"这单元格不会引入\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"# %ignore-line\",\n", + " \"println(\\\"这行不会被引入\\\")\",\n", + " \"println(\\\"这行会被引入\\\")\"\n", + " ]\n", + " }\n", + " ],\n", + " \"metadata\": {\n", + " \"kernelspec\": {\n", + " \"display_name\": \"Julia 1.10.0\",\n", + " \"language\": \"julia\",\n", + " \"name\": \"julia-1.10\"\n", + " },\n", + " \"language_info\": {\n", + " \"name\": \"julia\"\n", + " }\n", + " },\n", + " \"nbformat\": 4,\n", + " \"nbformat_minor\": 2\n", + " }\"\"\")\n", + " # 现场编译\n", + " 引入后内容 = compile_code_lines(\n", + " IpynbCell(;\n", + " cell_type=\"code\",\n", + " source=[\"#= %inline-compiled =# include_notebook($(repr(引入路径)))\"]\n", + " );\n", + " lang=:julia\n", + " )\n", + " @assert startswith(引入后内容, \"# %% Jupyter Notebook | Julia\")\n", + " @assert contains(引入后内容, \"println(\\\"这行会被引入\\\")\")\n", + " @assert !contains(引入后内容, \"println(\\\"这行不会被引入\\\")\")\n", + " @assert !contains(引入后内容, \"println(\\\"这单元格不会被引入\\\")\")\n", + " println(引入后内容)\n", + "end" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92m\u001b[1m✅Jupyter笔记本文件引入完成,模块导入成功!\u001b[22m\u001b[39m\n", + "IpynbCompile = Main.IpynbCompile\n", + "\n", + "\u001b[94m\u001b[1m📜以下为IpynbCompile模块导出的所有19个符号:\u001b[22m\u001b[39m\n", + "@cell_str\n", + "@notebook_str\n", + "IpynbCell\n", + "IpynbCompile\n", + "IpynbNotebook\n", + "IpynbNotebookMetadata\n", + "compile_cell\n", + "compile_notebook\n", + "eval_cell\n", + "eval_notebook\n", + "eval_notebook_by_cell\n", + "include_notebook\n", + "include_notebook_by_cell\n", + "parse_cell\n", + "parse_notebook\n", + "read_ipynb_json\n", + "read_notebook\n", + "tryparse_cell\n", + "tryparse_notebook\n" + ] + } + ], + "source": [ + "# %ignore-cell\n", "# * 递回执行自身代码(自举)\n", "include_notebook(SELF_PATH)\n", "\n", @@ -5100,7 +5306,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ @@ -5123,7 +5329,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\n", "构建过程主要包括:\n", "\n", "- **自举**构建主模块,生成库文件\n", @@ -5131,6 +5336,7 @@ "- 提取该文件开头Markdown笔记,在**项目根目录**下**生成自述文件**(`README.md`)\n", " - 因此`README.md`暂且只有一种语言(常更新的语言)\n", "\n", + "\n", "⚠️不应该在编译后的库文件中看到任何代码" ] }, @@ -5144,7 +5350,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -5152,7 +5358,7 @@ "output_type": "stream", "text": [ "\u001b[93m\u001b[1m✅Jupyter笔记本「主模块」自编译成功!\u001b[22m\u001b[39m\n", - "\u001b[93m\u001b[1m(共写入 50165 个字节)\u001b[22m\u001b[39m\n" + "\u001b[93m\u001b[1m(共写入 51874 个字节)\u001b[22m\u001b[39m\n" ] } ], @@ -5181,7 +5387,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -5240,7 +5446,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 33, "metadata": {}, "outputs": [ { @@ -5248,7 +5454,7 @@ "output_type": "stream", "text": [ "\u001b[92m\u001b[1m✅测试文件编译成功!\u001b[22m\u001b[39m\n", - "\u001b[92m\u001b[1m(共写入 41092 个字节)\u001b[22m\u001b[39m\n" + "\u001b[92m\u001b[1m(共写入 43437 个字节)\u001b[22m\u001b[39m\n" ] } ], @@ -5300,14 +5506,14 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\n", + "\n", "# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具\n", "\n", "[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)\n", @@ -5492,7 +5698,7 @@ "\n", "##### 文件引入\n", "\n", - "主要用途:结合「仅编译后可用」实现「外部代码内联」\n", + "主要用途:结合「仅编译后可用」简单实现「外部代码内联」\n", "\n", "- 如:集成某些**中小型映射表**,整合零散源码文件……\n", "\n", @@ -5500,10 +5706,54 @@ "\n", "```julia\n", "const square_map_dict = # 这里的等号可以另起一行\n", - "# % include to_include.jl \n", + "# %include to_include.jl \n", + "# ↑ 上面一行会被替换成文件内容\n", + "```\n", + "\n", + "编译前@和笔记本**同目录**下的`to_include.jl`中:\n", + "↓文件末尾有换行符\n", + "\n", + "```julia\n", + "# 这是一个要被引入的外部字典对象\n", + "Dict([\n", + " 1 => 1\n", + " 2 => 4\n", + " 3 => 9\n", + " # ...\n", + "])\n", + "```\n", + "\n", + "编译后:\n", + "\n", + "```julia\n", + "const square_map_dict = # 这里的等号可以另起一行\n", + "# 这是一个要被引入的外部字典对象\n", + "Dict([\n", + " 1 => 1\n", + " 2 => 4\n", + " 3 => 9\n", + " # ...\n", + "])\n", "# ↑ 上面一行会被替换成数据\n", "```\n", "\n", + "📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白\n", + "\n", + "##### 文件内联\n", + "\n", + "主要用途:为「文件引入」提供一个快捷方式,并**支持「编译后内联笔记本」**\n", + "\n", + "- 如:编译并集成**其它笔记本**到该文件中\n", + "\n", + "编译前@笔记本单元格:\n", + "\n", + "```julia\n", + "const square_map_dict = # 这里的等号可以另起一行\n", + "#= %inline-compiled =# include(\"to_include.jl\")\n", + "# ↑ 上面一行会被替换成文件内容\n", + "# * 若为使用`include_notebook`引入的笔记本,则会被替换为编译后的笔记本内容\n", + "```\n", + "\n", "编译前@和笔记本**同目录**下的`to_include.jl`中:\n", "↓文件末尾有换行符\n", "\n", @@ -5543,10 +5793,10 @@ { "data": { "text/plain": [ - "7861" + "8870" ] }, - "execution_count": 33, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } diff --git a/src/IpynbCompile.jl b/src/IpynbCompile.jl index da0ec52..25a0a07 100644 --- a/src/IpynbCompile.jl +++ b/src/IpynbCompile.jl @@ -1,5 +1,5 @@ # %% Jupyter Notebook | Julia 1.10.0 @ julia | format 2~4 -# % language_info: {"file_extension":".jl","mimetype":"application/julia","name":"julia","version":"1.9.1"} +# % language_info: {"file_extension":".jl","mimetype":"application/julia","name":"julia","version":"1.10.0"} # % kernelspec: {"name":"julia-1.10","display_name":"Julia 1.10.0","language":"julia"} # % nbformat: 4 # % nbformat_minor: 2 @@ -218,7 +218,7 @@ # ##### 文件引入 # %% [27] markdown -# 主要用途:结合「仅编译后可用」实现「外部代码内联」 +# 主要用途:结合「仅编译后可用」简单实现「外部代码内联」 # # - 如:集成某些**中小型映射表**,整合零散源码文件…… @@ -227,8 +227,8 @@ # # ```julia # const square_map_dict = # 这里的等号可以另起一行 -# # % include to_include.jl -# # ↑ 上面一行会被替换成数据 +# # %include to_include.jl +# # ↑ 上面一行会被替换成文件内容 # ``` # # 编译前@和笔记本**同目录**下的`to_include.jl`中: @@ -261,25 +261,72 @@ # 📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白 # %% [29] markdown -# ## 参考 +# ##### 文件内联 # %% [30] markdown +# 主要用途:为「文件引入」提供一个快捷方式,并**支持「编译后内联笔记本」** +# +# - 如:编译并集成**其它笔记本**到该文件中 + +# %% [31] markdown +# 编译前@笔记本单元格: +# +# ```julia +# const square_map_dict = # 这里的等号可以另起一行 +# #= %inline-compiled =# include("to_include.jl") +# # ↑ 上面一行会被替换成文件内容 +# # * 若为使用`include_notebook`引入的笔记本,则会被替换为编译后的笔记本内容 +# ``` +# +# 编译前@和笔记本**同目录**下的`to_include.jl`中: +# ↓文件末尾有换行符 +# +# ```julia +# # 这是一个要被引入的外部字典对象 +# Dict([ +# 1 => 1 +# 2 => 4 +# 3 => 9 +# # ... +# ]) +# ``` +# +# 编译后: +# +# ```julia +# const square_map_dict = # 这里的等号可以另起一行 +# # 这是一个要被引入的外部字典对象 +# Dict([ +# 1 => 1 +# 2 => 4 +# 3 => 9 +# # ... +# ]) +# # ↑ 上面一行会被替换成数据 +# ``` +# +# 📝Julia的「空白符无关性」允许在等号后边大范围附带注释的空白 + +# %% [32] markdown +# ## 参考 + +# %% [33] markdown # - 本Julia库的灵感来源:[Promises.jl/src/notebook.jl](https://github.com/fonsp/Promises.jl/blob/main/src/notebook.jl) # - 源库使用了 [**Pluto.jl**](https://github.com/fonsp/Pluto.jl) 的「笔记本导出」功能 # - **Jupyter Notebook** 文件格式(JSON):[🔗nbformat.readthedocs.io](https://nbformat.readthedocs.io/en/latest/format_description.html#notebook-file-format) -# %% [31] markdown +# %% [34] markdown # # # ⚠️该单元格首行注释用于截止生成`README.md`(包括自身) -# %% [32] markdown +# %% [35] markdown # ## 建立模块上下文 -# %% [33] markdown +# %% [36] markdown # 📌使用 `# %only-compiled` 控制 `module` 代码,生成模块上下文 -# %% [34] code +# %% [37] code # ! ↓这后边注释的代码只有在编译后才会被执行 # ! 使用多行注释/块注释的语法, # ! 以`#= %only-compiled`行*开头* @@ -290,32 +337,32 @@ IpynbCompile 主模块 module IpynbCompile # 后续编译后会变为模块上下文 -# %% [35] markdown +# %% [38] markdown # ## 前置模块 -# %% [36] markdown +# %% [39] markdown # ### 导入库 -# %% [37] code +# %% [40] code import JSON -# %% [38] markdown +# %% [41] markdown # ### 预置语法糖 -# %% [39] code +# %% [42] code "JSON常用的字典" const JSONDict{ValueType} = Dict{String,ValueType} where ValueType "默认解析出来的JSON字典(与`JSONDict`有本质不同,会影响到后续方法分派,并可能导致歧义)" const JSONDictAny = JSONDict{Any} -# %% [40] markdown +# %% [43] markdown # ### 兼容+注意事项 -# %% [41] code +# %% [44] code import Base: @kwdef # 兼容Julia 1.8⁻ -# %% [42] markdown +# %% [45] markdown # - ✅兼容 @ Julia **1.5⁺**:`include`自Julia **1.5**方可用 # - 🔒锁定最低Julia版本为**1.5** # - ❌兼容 @ Julia **1.6⁻**:「多行字符串」自Julia **1.7**方可使用"\【换行】"取消换行 @@ -328,13 +375,13 @@ import Base: @kwdef # 兼容Julia 1.8⁻ # - 📄错误信息:`LoadError: syntax: type declarations on global variables are not yet supported` # - ⚠️禁止在`const`定义的变量中标注类型(Julia运行时会自动推导) -# %% [43] markdown +# %% [46] markdown # ## 读取解析Jupyter笔记本(`.ipynb`文件) -# %% [44] markdown +# %% [47] markdown # ### 读取文件(JSON) -# %% [45] code +# %% [48] code export read_ipynb_json """ @@ -347,13 +394,11 @@ read_ipynb_json(path) = read(f, String) |> JSON.parse end -# ! ↓使用`# %ignore-line`让 编译器/解释器 忽略下一行 - -# %% [46] markdown +# %% [49] markdown # ### 解析文件元信息 -# %% [47] markdown +# %% [50] markdown # Jupyter Notebook元数据 格式参考 # # ```yaml @@ -378,7 +423,7 @@ read_ipynb_json(path) = # } # ``` -# %% [48] markdown +# %% [51] markdown # Jupyter Notebook Cell 格式参考 # # 共有: @@ -422,7 +467,7 @@ read_ipynb_json(path) = # } # ``` -# %% [49] markdown +# %% [52] markdown # 当前Julia笔记本 元数据: # # ```json @@ -444,13 +489,13 @@ read_ipynb_json(path) = # (截止至2024-01-16) -# %% [51] markdown +# %% [54] markdown # ## 解析Jupyter笔记本(Julia `struct`) -# %% [52] markdown +# %% [55] markdown # ### 定义「笔记本」结构 -# %% [53] code +# %% [56] code export IpynbNotebook, IpynbNotebookMetadata """ @@ -510,25 +555,23 @@ IpynbNotebookMetadata(json::JSONDict) = IpynbNotebookMetadata(; kernelspec=json["kernelspec"], ) -# ! ↓使用`# %ignore-below`让 编译器/解释器 忽略后续内容 - -# %% [54] markdown +# %% [57] markdown # ### 读取笔记本 总函数 -# %% [55] markdown +# %% [58] markdown # 根据路径读取笔记本 -# %% [56] code +# %% [59] code export read_notebook "从路径读取Jupyter笔记本(`struct IpynbNotebook`)" read_notebook(path::AbstractString)::IpynbNotebook = IpynbNotebook(read_ipynb_json(path)) -# %% [57] markdown +# %% [60] markdown # 方便引入笔记本的字符串宏 -# %% [58] code +# %% [61] code export @notebook_str macro notebook_str(path::AbstractString) @@ -536,13 +579,13 @@ macro notebook_str(path::AbstractString) end -# %% [59] markdown +# %% [62] markdown # ### 解析/生成 笔记本信息 -# %% [60] markdown +# %% [63] markdown # #### 识别编程语言 -# %% [61] code +# %% [64] code "【内部】编程语言⇒正则表达式 识别字典" const LANG_IDENTIFY_DICT = Dict{Symbol,Regex}( lang => Regex("^(?:$regex_str)\$") # ! ←必须头尾精确匹配(不然就会把`JavaScript`认成`r`) @@ -635,15 +678,14 @@ identify_lang(language_text::AbstractString) = contains(language_text, regex) end # ! 默认返回`nothing` - -# %% [62] markdown +# %% [65] markdown # #### 根据编程语言生成注释 # # - 生成的注释会用于「行开头」识别 # - 如:`// %ignore-cell` (C系列) # - 如:`# %ignore-cell` (Python/Julia) -# %% [63] code +# %% [66] code "【内部】编程语言⇒单行注释" const LANG_COMMENT_DICT_INLINE = Dict{Symbol,String}() @@ -731,11 +773,10 @@ generate_comment_multiline_head(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_HEAD generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang] - -# %% [64] markdown +# %% [67] markdown # #### 生成常用扩展名 -# %% [65] code +# %% [68] code "【内部】编程语言⇒常用扩展名(不带`.`)" const LANG_EXTENSION_DICT = Dict{Symbol,String}( # ! 以下「特殊注释」需要在行首 @@ -810,18 +851,17 @@ get_extension(lang::Symbol) = get( ) - -# %% [66] markdown +# %% [69] markdown # #### 解析/生成 测试 -# %% [68] markdown +# %% [71] markdown # ### Notebook编译/头部注释 # # - 🎯标注 版本信息 # - 🎯标注 各类元数据 -# %% [69] code +# %% [72] code """ 【内部】从Notebook生成头部注释 - ⚠️末尾有换行 @@ -846,17 +886,16 @@ $(generate_comment_inline(lang)) % nbformat_minor: $(notebook.nbformat_minor) """ - -# %% [70] markdown +# %% [73] markdown # ## 解析处理单元格 -# %% [71] markdown +# %% [74] markdown # ### 定义「单元格」 -# %% [72] markdown +# %% [75] markdown # 定义结构类型 -# %% [73] code +# %% [76] code export IpynbCell """ @@ -894,10 +933,10 @@ struct IpynbCell )...) end -# %% [74] markdown +# %% [77] markdown # 定义快捷字符串宏 -# %% [75] code +# %% [78] code export @cell_str "🎯将字符串拆分成单元格各行(区分末尾换行)" @@ -933,24 +972,22 @@ macro cell_str(content::AbstractString, cell_type::String="code") end - -# %% [76] markdown +# %% [79] markdown # 结合笔记本,重定向&调用测试处理 -# %% [77] code +# %% [80] code # ! 在此重定向,以便后续外部调用 "重定向「笔记本」的默认「单元格」类型" IpynbNotebook(json) = IpynbNotebook{IpynbCell}(json) - -# %% [78] markdown +# %% [81] markdown # ## 编译单元格 -# %% [79] markdown +# %% [82] markdown # ### 编译/入口 -# %% [80] code +# %% [83] code export compile_cell """ @@ -985,10 +1022,10 @@ compile_cell(cells::Vector{IpynbCell}; kwargs...)::String = join(( for (line_num, cell) in enumerate(cells) # ! ←一定是顺序遍历 ), '\n') -# %% [81] markdown +# %% [84] markdown # ### 编译/单元格标头 -# %% [82] code +# %% [85] code """ 【内部】对整个单元格的「类型标头」编译 - 🎯生成一行注释,标识单元格 @@ -1010,15 +1047,14 @@ $(cell.cell_type) """ # ! ←末尾附带换行符 - -# %% [83] markdown +# %% [86] markdown # ### 编译/代码 -# %% [85] markdown +# %% [88] markdown # 主编译方法 -# %% [86] code +# %% [89] code """ 对代码的编译 - @param cell 所需编译的单元格 @@ -1076,12 +1112,22 @@ function compile_code_lines(cell::IpynbCell; # * `%ignore-line` 忽略下一行 | 仅需为行前缀 if startswith(current_line, "$(generate_comment_inline(lang)) %ignore-line") current_line_i += 1 # ! 结合后续递增,跳过下面一行,不让本「特殊注释」行被编译 + # * `%ignore-below` 忽略下面所有行 | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-below") break # ! 结束循环,不再编译后续代码 + # * `%ignore-cell` 忽略整个单元格 | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-cell") return nothing # ! 返回「不编译单元格」的信号 + + # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀 + elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-begin") + # 只要后续没有以"$(generate_comment_inline(lang)) %ignore-end"开启的行,就不断跳过 + while !startswith(lines[current_line_i], "$(generate_comment_inline(lang)) %ignore-end") && current_line_i <= len_lines + current_line_i += 1 # 忽略性递增 + end # ! 让最终递增跳过"# %ignore-end"所在行 + # * `%include` 读取其所指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_inline(lang)) %include") # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 @@ -1089,33 +1135,43 @@ function compile_code_lines(cell::IpynbCell; # 读取内容 local content::String = read(joinpath(root_path, relative_path), String) result *= content # ! 不会自动添加换行! - # * `#= %inline-compiled =# include(` 读取后边`include`指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀 - elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang)) include(") + + # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀 + elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))") # 直接作为Julia代码解析 local expr::Expr = Meta.parse(current_line) #= # * 在Expr中提取相应字符串 | 参考: + ``` julia> :(include("123")) |> dump Expr head: Symbol call args: Array{Any}((2,)) 1: Symbol include 2: String "123" + ``` + * JuLISP语法:(call include "123") =# - if expr.head == :call && expr.args[1] == :include && length(expr.args) > 1 + if expr.head == :call && length(expr.args) > 1 # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 - relative_path = expr.args[2] - # 读取内容 | if内不再要用local,和上级表达式重复 - content = read(joinpath(root_path, relative_path), String) + relative_path = Main.eval(expr.args[2]) # * 在主模块上下文中加载计算路径 + local file_path::String = joinpath(root_path, relative_path) + # * include⇒读取文件内容 + if expr.args[1] == :include + content = read(file_path, String) + # * include_notebook⇒读取编译笔记本 + elseif expr.args[1] == :include_notebook + content = compile_notebook( + IpynbNotebook(file_path); # 需要构造函数 # ! 直接使用字符串会将其编译为源码文件 + root_path=dirname(file_path), # ! 使用文件自身的根目录 + kwargs..., # 其它附加参数 + ) + end + # 追加内容 result *= content # ! 不会自动添加换行! else # 若非`include(路径)`的形式⇒警告 @warn "非法表达式,内联失败!" current_line expr end - # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀 - elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-begin") - # 只要后续没有以"$(generate_comment_inline(lang)) %ignore-end"开启的行,就不断跳过 - while !startswith(lines[current_line_i], "$(generate_comment_inline(lang)) %ignore-end") && current_line_i <= len_lines - current_line_i += 1 # 忽略性递增 - end # ! 让最终递增跳过"# %ignore-end"所在行 + # * `%only-compiled` 仅编译后可用(多行) | 仅需为行前缀 elseif ( startswith(current_line, "$(generate_comment_multiline_head(lang)) %only-compiled") || @@ -1123,6 +1179,7 @@ function compile_code_lines(cell::IpynbCell; ) # ! 不做任何事情,跳过当前行 # * 否则:直接将行追加到结果 + else result *= current_line end @@ -1136,11 +1193,10 @@ function compile_code_lines(cell::IpynbCell; end - -# %% [87] markdown +# %% [90] markdown # ### 编译/Markdown -# %% [88] code +# %% [91] code """ 对Markdown的编译 - 📌主要方法:转换成多个单行注释 @@ -1175,10 +1231,10 @@ end -# %% [89] markdown +# %% [92] markdown # ## 解析执行单元格 -# %% [90] markdown +# %% [93] markdown # 🎯将单元格解析**编译**成Julia表达式,并可直接作为代码执行 # - 【核心】解释:`parse_cell` # - 📌基本是`compile_cell` ∘ `Meta.parse`的复合 @@ -1190,7 +1246,7 @@ end # - 📌基本是`parse_cell` ∘ `eval`的复合 # - ⚙️可任意指定其中的`eval`函数 -# %% [91] code +# %% [94] code export parse_cell, tryparse_cell, eval_cell """ @@ -1265,14 +1321,13 @@ eval_cell(code_or_codes; eval_function=Main.eval, kwargs...) = eval_function( - -# %% [93] markdown +# %% [96] markdown # ## 编译解析笔记本 -# %% [94] markdown +# %% [97] markdown # 编译笔记本 -# %% [95] code +# %% [98] code export compile_notebook """ @@ -1352,11 +1407,10 @@ function compile_notebook(path::AbstractString; kwargs...) end - -# %% [96] markdown +# %% [99] markdown # 解析笔记本 -# %% [97] code +# %% [100] code export parse_notebook, tryparse_notebook """ @@ -1390,14 +1444,13 @@ tryparse_notebook(args...; kwargs...) = end - -# %% [98] markdown +# %% [101] markdown # ## 执行笔记本 -# %% [99] markdown +# %% [102] markdown # 执行笔记本 -# %% [100] code +# %% [103] code export eval_notebook, eval_notebook_by_cell """ @@ -1430,10 +1483,10 @@ end # ! 测试代码放在最后边 -# %% [101] markdown +# %% [104] markdown # 引入笔记本 -# %% [102] code +# %% [105] code export include_notebook, include_notebook_by_cell """ @@ -1469,10 +1522,10 @@ include_notebook_by_cell(path::AbstractString; kwargs...) = eval_notebook_by_cel -# %% [103] markdown +# %% [107] markdown # ## 关闭模块上下文 -# %% [104] code +# %% [108] code # ! ↓这后边注释的代码只有在编译后才会被执行 # ! 仍然使用多行注释语法,以便统一格式 end # module diff --git a/test/runtests.jl b/test/runtests.jl index 02a9f50..c4ba7c9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -35,8 +35,8 @@ read_ipynb_json(path) = open(path, "r") do f read(f, String) |> JSON.parse end - -# ! ↓使用`# %ignore-line`让 编译器/解释器 忽略下一行 +# %ignore-line +# ! ↕使用`# %ignore-line`让 编译器/解释器 忽略下一行 # %ignore-line ROOT_PATH = any(contains(@__DIR__(), sub) for sub in ["src", "test"]) ? dirname(@__DIR__) : @__DIR__ # %ignore-line @@ -124,9 +124,9 @@ IpynbNotebookMetadata(json::JSONDict) = IpynbNotebookMetadata(; language_info=json["language_info"], kernelspec=json["kernelspec"], ) - -# ! ↓使用`# %ignore-below`让 编译器/解释器 忽略后续内容 # %ignore-below +# ! ↑使用`# %ignore-below`让 编译器/解释器 忽略后续内容 + notebook_raw_cell = IpynbNotebook(notebook_json) notebook_metadata = notebook_raw_cell.metadata @info "JSON转译结构化成功!" notebook_raw_cell notebook_metadata @@ -146,6 +146,7 @@ macro notebook_str(path::AbstractString) :(read_notebook($path)) |> esc end # %ignore-below + @macroexpand notebook"IpynbCompile.ipynb" "【内部】编程语言⇒正则表达式 识别字典" @@ -183,7 +184,6 @@ identify_lang(language_text::AbstractString) = findfirst(LANG_IDENTIFY_DICT) do regex contains(language_text, regex) end # ! 默认返回`nothing` -# %ignore-below # ! 测试代码在最下边 "【内部】编程语言⇒单行注释" const LANG_COMMENT_DICT_INLINE = Dict{Symbol,String}() @@ -217,8 +217,8 @@ generate_comment_multiline_head(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_HEAD "【内部】生成块注释结尾 | ⚠️找不到⇒报错" generate_comment_multiline_tail(lang::Symbol) = LANG_COMMENT_DICT_MULTILINE_TAIL[lang] - # %ignore-below # ! 测试代码在最下边 + @info "" LANG_COMMENT_DICT_INLINE LANG_COMMENT_DICT_MULTILINE_HEAD LANG_COMMENT_DICT_MULTILINE_TAIL "【内部】编程语言⇒常用扩展名(不带`.`)" @@ -237,8 +237,8 @@ get_extension(lang::Symbol) = get( LANG_EXTENSION_DICT, lang, string(lang) ) - # %ignore-below # ! 测试代码在最下边 + @info "" LANG_EXTENSION_DICT # %ignore-cell @@ -292,8 +292,8 @@ $(generate_comment_inline(lang)) % kernelspec: $(JSON.json(notebook.metadata.ker $(generate_comment_inline(lang)) % nbformat: $(notebook.nbformat) $(generate_comment_inline(lang)) % nbformat_minor: $(notebook.nbformat_minor) """ - # %ignore-below + # ! ↑使用`# %ignore-below`让 编译器/解释器 忽略后续内容 | 【2024-01-26 21:38:54】debug:笔记本可能在不同的电脑上运行 let notebook_jl_head = compile_notebook_head(notebook_raw_cell; lang=:julia) @test contains( @@ -383,8 +383,8 @@ macro cell_str(content::AbstractString, cell_type::String="code") ) ) |> esc end - # %ignore-below + let a1 = split_to_cell("""1\n2\n3"""), # 📌测试【末尾有无换行】的区别 a2 = split_to_cell("""1\n2\n3\n""") @@ -406,8 +406,8 @@ end # ! 在此重定向,以便后续外部调用 "重定向「笔记本」的默认「单元格」类型" IpynbNotebook(json) = IpynbNotebook{IpynbCell}(json) - # %ignore-below + notebook = IpynbNotebook{IpynbCell}(notebook_json) cells = notebook.cells @@ -466,8 +466,8 @@ $(generate_comment_inline(lang)) %% \ $(#= 可选的行号 =# haskey(kwargs, :line_num) ? "[$(kwargs[:line_num])] " : "")\ $(cell.cell_type) """ # ! ←末尾附带换行符 - # %ignore-below + @test compile_cell_head(notebook.cells[1]; lang=:julia) == "# %% markdown\n" @test compile_cell_head(notebook.cells[1]; lang=:julia, line_num=1) == "# %% [1] markdown\n" @@ -533,12 +533,22 @@ function compile_code_lines(cell::IpynbCell; # * `%ignore-line` 忽略下一行 | 仅需为行前缀 if startswith(current_line, "$(generate_comment_inline(lang)) %ignore-line") current_line_i += 1 # ! 结合后续递增,跳过下面一行,不让本「特殊注释」行被编译 + # * `%ignore-below` 忽略下面所有行 | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-below") break # ! 结束循环,不再编译后续代码 + # * `%ignore-cell` 忽略整个单元格 | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-cell") return nothing # ! 返回「不编译单元格」的信号 + + # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀 + elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-begin") + # 只要后续没有以"$(generate_comment_inline(lang)) %ignore-end"开启的行,就不断跳过 + while !startswith(lines[current_line_i], "$(generate_comment_inline(lang)) %ignore-end") && current_line_i <= len_lines + current_line_i += 1 # 忽略性递增 + end # ! 让最终递增跳过"# %ignore-end"所在行 + # * `%include` 读取其所指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_inline(lang)) %include") # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 @@ -546,33 +556,43 @@ function compile_code_lines(cell::IpynbCell; # 读取内容 local content::String = read(joinpath(root_path, relative_path), String) result *= content # ! 不会自动添加换行! - # * `#= %inline-compiled =# include(` 读取后边`include`指定的路径,并将其内容作为「当前行」添加(不会自动添加换行!) | 仅需为行前缀 - elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang)) include(") + + # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀 + elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))") # 直接作为Julia代码解析 local expr::Expr = Meta.parse(current_line) #= # * 在Expr中提取相应字符串 | 参考: + ``` julia> :(include("123")) |> dump Expr head: Symbol call args: Array{Any}((2,)) 1: Symbol include 2: String "123" + ``` + * JuLISP语法:(call include "123") =# - if expr.head == :call && expr.args[1] == :include && length(expr.args) > 1 + if expr.head == :call && length(expr.args) > 1 # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 - relative_path = expr.args[2] - # 读取内容 | if内不再要用local,和上级表达式重复 - content = read(joinpath(root_path, relative_path), String) + relative_path = Main.eval(expr.args[2]) # * 在主模块上下文中加载计算路径 + local file_path::String = joinpath(root_path, relative_path) + # * include⇒读取文件内容 + if expr.args[1] == :include + content = read(file_path, String) + # * include_notebook⇒读取编译笔记本 + elseif expr.args[1] == :include_notebook + content = compile_notebook( + IpynbNotebook(file_path); # 需要构造函数 # ! 直接使用字符串会将其编译为源码文件 + root_path=dirname(file_path), # ! 使用文件自身的根目录 + kwargs..., # 其它附加参数 + ) + end + # 追加内容 result *= content # ! 不会自动添加换行! else # 若非`include(路径)`的形式⇒警告 @warn "非法表达式,内联失败!" current_line expr end - # * `%ignore-begin` 跳转到`%ignore-end`的下一行,并忽略中间所有行 | 仅需为行前缀 - elseif startswith(current_line, "$(generate_comment_inline(lang)) %ignore-begin") - # 只要后续没有以"$(generate_comment_inline(lang)) %ignore-end"开启的行,就不断跳过 - while !startswith(lines[current_line_i], "$(generate_comment_inline(lang)) %ignore-end") && current_line_i <= len_lines - current_line_i += 1 # 忽略性递增 - end # ! 让最终递增跳过"# %ignore-end"所在行 + # * `%only-compiled` 仅编译后可用(多行) | 仅需为行前缀 elseif ( startswith(current_line, "$(generate_comment_multiline_head(lang)) %only-compiled") || @@ -580,6 +600,7 @@ function compile_code_lines(cell::IpynbCell; ) # ! 不做任何事情,跳过当前行 # * 否则:直接将行追加到结果 + else result *= current_line end @@ -591,9 +612,9 @@ function compile_code_lines(cell::IpynbCell; # 最后返回所有行 # ! 「在最后一行和先前所有行的换行符一致」在行编译后方运行 return result end - # %ignore-below +# 测试`%include` let 引入路径 = joinpath(ROOT_PATH, "test", "%include.test.jl") # 放置测试脚本 预期引入内容 = """\ @@ -761,7 +782,6 @@ tryparse_cell(args...; kwargs...) = eval_cell(code_or_codes; eval_function=Main.eval, kwargs...) = eval_function( parse_cell(code_or_codes; kwargs...) ) - # %ignore-below # 执行其中一个代码单元格 # * 参考「预置语法糖」 @@ -860,8 +880,8 @@ function compile_notebook(path::AbstractString; kwargs...) root_path=dirname(path), ) end - # %ignore-below + compile_notebook(notebook) |> print #= %only-compiled # ! 模块上下文:导出元素 @@ -897,8 +917,8 @@ tryparse_notebook(args...; kwargs...) = showerror(stderr, e, Base.stacktrace(Base.catch_backtrace())) nothing end - # %ignore-below + @test tryparse_notebook(notebook) isa Expr #= %only-compiled # ! 模块上下文:导出元素 @@ -969,9 +989,60 @@ include_notebook_by_cell(path::AbstractString; kwargs...) = eval_notebook_by_cel # 其它附加参数(如「编译根目录」) kwargs... ) - # %ignore-below +# 测试`%inline-compiled` +let 引入路径 = joinpath(ROOT_PATH, "test", "%inline-compiled.test.ipynb") + # 放置测试脚本 + ispath(引入路径) || write(引入路径, """{ + "cells": [ + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# %ignore-cell", + "print(\"这单元格不会引入\")" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# %ignore-line", + "println(\"这行不会被引入\")", + "println(\"这行会被引入\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.10.0", + "language": "julia", + "name": "julia-1.10" + }, + "language_info": { + "name": "julia" + } + }, + "nbformat": 4, + "nbformat_minor": 2 + }""") + # 现场编译 + 引入后内容 = compile_code_lines( + IpynbCell(; + cell_type="code", + source=["#= %inline-compiled =# include_notebook($(repr(引入路径)))"] + ); + lang=:julia + ) + @test startswith(引入后内容, "# %% Jupyter Notebook | Julia") + @test contains(引入后内容, "println(\"这行会被引入\")") + @test !contains(引入后内容, "println(\"这行不会被引入\")") + @test !contains(引入后内容, "println(\"这单元格不会被引入\")") + println(引入后内容) +end + +# %ignore-cell # * 递回执行自身代码(自举) include_notebook(SELF_PATH)