From afd058ee373cbafa76bc8010759e5f527a3a63d0 Mon Sep 17 00:00:00 2001 From: ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:50:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:sparkles:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=B0=86=E7=AC=94=E8=AE=B0=E6=9C=AC=E5=86=85=E8=81=94=E8=87=B3?= =?UTF-8?q?Markdown=E6=96=87=E4=BB=B6=EF=BC=9B=E5=A2=9E=E5=BC=BA=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=81=A5=E5=A3=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持将Jupyter笔记本单元格内联,并生成Markdown文件;附带相应的「内联器」`inliner.ipynb.jl`;现在引入/内联文件报错时,会静默失败并保留原始行 --- Project.toml | 2 +- README.md | 2 +- examples/rust.ipynb | 20 +- src/IpynbCompile.ipynb | 989 ++++++++++++++++++++++++++++++++++------- src/IpynbCompile.jl | 263 +++++++++-- src/compiler.ipynb | 2 +- src/compiler.jl | 2 +- src/inliner.ipynb | 324 ++++++++++++++ src/inliner.jl | 118 +++++ test/runtests.jl | 275 ++++++++++-- 10 files changed, 1758 insertions(+), 239 deletions(-) create mode 100644 src/inliner.ipynb create mode 100644 src/inliner.jl diff --git a/Project.toml b/Project.toml index b92a884..6c2e0a4 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.7.1" +version = "1.8.0" [deps] JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" diff --git a/README.md b/README.md index b0c4768..eb0959d 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) diff --git a/examples/rust.ipynb b/examples/rust.ipynb index dbf8a52..4226dab 100644 --- a/examples/rust.ipynb +++ b/examples/rust.ipynb @@ -34,11 +34,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "vscode": { - "languageId": "rust" - } - }, + "metadata": {}, "outputs": [], "source": [ "/**\n", @@ -65,11 +61,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "vscode": { - "languageId": "rust" - } - }, + "metadata": {}, "outputs": [], "source": [ "/**\n", @@ -100,11 +92,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "vscode": { - "languageId": "rust" - } - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -181,7 +169,7 @@ "codemirror_mode": "rust", "file_extension": ".rs", "mimetype": "text/rust", - "name": "Rust", + "name": "rust", "pygment_lexer": "rust", "version": "" } diff --git a/src/IpynbCompile.ipynb b/src/IpynbCompile.ipynb index f570990..2c4e30b 100644 --- a/src/IpynbCompile.ipynb +++ b/src/IpynbCompile.ipynb @@ -815,7 +815,7 @@ "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\", \"- 扫描`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(共写入 51874 个字节)\\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\" => 32), 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\" => 33), 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(共写入 43452 个字节)\\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\" => 34), 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[\"8870\"]), \"metadata\" => Dict{String, Any}(), \"execution_count\" => 35)], \"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\" => 35)], 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_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(共写入 58524 个字节)\\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\" => 32), 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\", \"\\e[92m\\e[1mCompiled: .\\\\inliner.ipynb => .\\\\inliner.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\" => 33), 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(共写入 50803 个字节)\\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\" => 34), 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[\"8870\"]), \"metadata\" => Dict{String, Any}(), \"execution_count\" => 35)], \"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\" => 35)], 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" ] } @@ -1494,7 +1494,7 @@ { "data": { "text/plain": [ - "118-element Vector{IpynbCell}:\n", + "122-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", @@ -1659,7 +1659,7 @@ { "data": { "text/plain": [ - "34-element Vector{IpynbCell}:\n", + "35-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", @@ -1674,12 +1674,12 @@ " 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\", [\"#= %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\", \"# %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\" … \" lang=:julia\\n\", \" )\\n\", \" @assert startswith(引入后内容, \\\"# %% Jupyter Notebook | Julia\\\")\\n\", \" @assert contains(引入后内容, \\\"println(\\\\\\\"这行会被引入\\\\\\\")\\\")\\n\", \" @assert !contains(引入后内容, \\\"println(\\\\\\\"这行不会被引入\\\\\\\")\\\")\\n\", \" @assert !contains(引入后内容, \\\"println(\\\\\\\"这单元格不会被引入\\\\\\\")\\\")\\n\", \" println(引入后内容)\\n\", \" # 清理现场\\n\", \" rm(引入路径)\\n\", \"end\"], Dict{String, Any}(), nothing)\n", + " IpynbCell(\"code\", [\"#= %only-compiled # ! 模块上下文:导出元素\\n\", \"export inline_notebook_to_markdown\\n\", \"%only-compiled =#\\n\", \"\\n\", \"\\\"\\\"\\\"\\n\", \"【内部】计算「为避免内部歧义所需涵盖的反引号数量」\\n\", \"- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之\\n\", \"- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712\\n\", \"\\\"\\\"\\\"\\n\", \"_quote_marks(raw_content) =\\n\" … \" inlined_content = read(FULL_MD_PATH, String)\\n\", \" @assert contains(inlined_content, \\\"```julia\\\") # 具有`julia`代码块\\n\", \"\\n\", \" # 删除文档,并打印其中内容\\n\", \" rm(FULL_MD_PATH)\\n\", \" printstyled(\\n\", \" \\\"ℹ️自内联测试文件已删除!以下是文件内容:\\\\n\\\";\\n\", \" color=:light_red, bold=true\\n\", \" ) |> print\\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", @@ -2296,7 +2296,7 @@ " # 所使用的编程语言\n", " lang::Symbol,\n", " # 根路径(默认为「执行编译的文件」所在目录)\n", - " root_path::AbstractString=@__DIR__,\n", + " root_path::AbstractString=@__DIR__(),\n", " # 其它参数\n", " kwargs...)::Union{String,Nothing}\n", "\n", @@ -2331,9 +2331,20 @@ " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %include\")\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", " local relative_path = current_line[nextind(current_line, 1, length(\"$(generate_comment_inline(lang)) %include \")):end] |> rstrip # ! ←注意`%include`后边有个空格\n", - " # 读取内容\n", - " local content::String = read(joinpath(root_path, relative_path), String)\n", - " result *= content # ! 不会自动添加换行!\n", + " try\n", + " # 读取内容\n", + " local content::String = read(joinpath(root_path, relative_path), String)\n", + " result *= content # ! 不会自动添加换行!\n", + " catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联\n", + " # 读取失败,显示警告\n", + " if e isa SystemError\n", + " @warn \"引入文件「$(relative_path)」失败!错误码:$(e.errnum)\" current_line current_line_i\n", + " else\n", + " @warn \"引入文件「$(relative_path)」失败!$e\" current_line current_line_i\n", + " end\n", + " # 保留原始行\n", + " result *= current_line\n", + " end\n", "\n", " # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))\")\n", @@ -2353,20 +2364,31 @@ " if expr.head == :call && length(expr.args) > 1\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\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", + " try\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", + " catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联\n", + " # 读取失败,显示警告\n", + " if e isa SystemError\n", + " @warn \"内联文件「$(relative_path)」失败!错误码:$(e.errnum)\" current_line current_line_i\n", + " else\n", + " @warn \"内联文件「$(relative_path)」失败!$e\" current_line current_line_i\n", + " end\n", + " # 保留原始行\n", + " result *= current_line\n", " end\n", - " # 追加内容\n", - " result *= content # ! 不会自动添加换行!\n", " else # 若非`include(路径)`的形式⇒警告\n", " @warn \"非法表达式,内联失败!\" current_line expr\n", " end\n", @@ -2527,6 +2549,7 @@ "以「配对」方式进行展开,允许同时编译多个笔记本\n", "- 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\n", "- 📌无论在此的「笔记本」「目标」路径还是其它的\n", + "- @param pairs 笔记本与目标的「配对」\n", "\"\"\"\n", "function compile_notebook(pairs::Vararg{Pair})\n", " for pair in pairs\n", @@ -2540,27 +2563,29 @@ "- @param path 要写入的路径\n", "- @return 写入结果\n", "\"\"\"\n", - "compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = write(\n", - " # 使用 `write`函数,自动写入编译结果\n", - " path,\n", - " # 传入前编译\n", - " compile_notebook(notebook; kwargs...)\n", - ")\n", + "compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) =\n", + " write(\n", + " # 使用 `write`函数,自动写入编译结果\n", + " path,\n", + " # 传入前编译\n", + " compile_notebook(notebook; kwargs...)\n", + " )\n", "\n", "\"\"\"\n", "编译指定路径的笔记本,并写入指定路径\n", "- @param path 要读取的路径\n", "- @return 写入结果\n", "\"\"\"\n", - "compile_notebook(path::AbstractString, destination; kwargs...) = compile_notebook(\n", - " # 直接使用构造函数加载笔记本\n", - " IpynbNotebook(path),\n", - " # 保存在目标路径\n", - " destination;\n", - " # 其它附加参数 #\n", - " # 自动从`path`构造编译根目录\n", - " root_path=dirname(path),\n", - ")\n", + "compile_notebook(path::AbstractString, destination; kwargs...) =\n", + " compile_notebook(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 保存在目标路径\n", + " destination;\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造编译根目录\n", + " root_path=dirname(path),\n", + " )\n", "\n", "\"\"\"\n", "编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\n", @@ -2684,8 +2709,170 @@ ")\n", "\n", "\n", + "# %% [29] code\n", + "export inline_notebook_to_markdown\n", + "\n", + "\"\"\"\n", + "【内部】计算「为避免内部歧义所需涵盖的反引号数量」\n", + "- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之\n", + "- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712\n", + "\"\"\"\n", + "_quote_marks(raw_content) =\n", + " '`' ^ (\n", + " maximum( # 取最大值\n", + " findall(r\"(`+)\", raw_content)\n", + " .|> length; # 批量求长\n", + " init=2 # 最小为2(保证最终值不小于3)\n", + " ) + 1 # 保证覆盖\n", + " )\n", + "\n", + "\"\"\"\n", + "【内部】内联一个单元格至Markdown\n", + "\"\"\"\n", + "function inline_cell_to_markdown(\n", + " cell::IpynbCell;\n", + " lang::Symbol, # ! 这是笔记本所用的语言\n", + " compile::Bool=true,\n", + " kwargs...\n", + ")::Union{String,Nothing}\n", + " # 先根据「是否编译」决定「原始码」\n", + " local raw_content::Union{String,Nothing} = (\n", + " compile ?\n", + " compile_code_lines(\n", + " cell;\n", + " lang=(\n", + " # ! 特别 对Markdown单元格做「语言特化」\n", + " cell.cell_type == \"markdown\" ?\n", + " :markdown :\n", + " :julia\n", + " ),\n", + " kwargs...\n", + " ) :\n", + " # ! ↑此处可能返回`nothing`\n", + " join(cell.source)\n", + " )\n", + " # 编译为空⇒返回空 #\n", + " isnothing(raw_content) && return nothing\n", + "\n", + " # 封装各单元格「原始码」为Markdown & 返回 #\n", + " # * Markdown单元格⇒返回自身\n", + " return if cell.cell_type == \"code\"\n", + " quote_marks = _quote_marks(raw_content)\n", + " \"\"\"\\\n", + " $(quote_marks)$lang\n", + " $(raw_content)\n", + " $(quote_marks)\\\n", + " \"\"\"\n", + " # * Markdown单元格⇒返回自身\n", + " elseif cell.cell_type == \"markdown\"\n", + " raw_content\n", + " else\n", + " @warn \"未支持的单元格类型:$(cell.cell_type)\"\n", + " # ! 仍然内联,但会放入「无语言代码块」中\n", + " quote_marks = _quote_marks(raw_content)\n", + " \"\"\"\\\n", + " $(quote_marks)\n", + " $(raw_content)\n", + " $(quote_marks)\\\n", + " \"\"\"\n", + " end\n", + "end\n", + "\n", + "\"\"\"\n", + "内联整个笔记本至Markdown\n", + "- 🎯编译/内联整个笔记本对象,形成相应**Markdown文档**(`.md`文件)\n", + " - 📌可通过`compile`关键字参数选择「是否编译单元格」\n", + " - 默认启用「编译」\n", + " - ✨由此可使用Jupyter写Markdown文档\n", + "- 📌整体文本:各单元格编译+代码块封装\n", + "- ⚠️末尾固定为一个换行符\n", + "- @param notebook 要内联的笔记本对象\n", + "- @return 内联后的文本\n", + "\"\"\"\n", + "function inline_notebook_to_markdown(\n", + " notebook::IpynbNotebook;\n", + " lang::Symbol=identify_lang(notebook),\n", + " compile::Bool=true,\n", + " kwargs...\n", + ")\n", + " # 内联所有单元格数据\n", + " local inlined_cells::Vector{String} = String[]\n", + " local inlined_cell::Union{String,Nothing}\n", + " for cell in notebook.cells\n", + " inlined_cell = inline_cell_to_markdown(\n", + " cell;\n", + " lang,\n", + " compile,\n", + " kwargs...\n", + " )\n", + " # 仅非空者加入 | 处理`%ignore-cell`的情况\n", + " isnothing(inlined_cell) || push!(inlined_cells, inlined_cell)\n", + " end\n", + " # 合并,固定末尾换行\n", + " (join(inlined_cells, \"\\n\\n\") |> rstrip) * '\\n'\n", + "end\n", "\n", - "# %% [30] code\n", + "\"\"\"\n", + "以「配对」方式进行展开,允许同时内联多个笔记本\n", + "- 🎯支持形如`inline_notebook_to_markdown(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\n", + "- 📌无论在此的「笔记本」「目标」路径还是其它的\n", + "- @param pairs 笔记本与目标的「配对」\n", + "\"\"\"\n", + "function inline_notebook_to_markdown(pairs::Vararg{Pair})\n", + " for pair in pairs\n", + " inline_notebook_to_markdown(first(pair), last(pair))\n", + " end\n", + "end\n", + "\n", + "\"\"\"\n", + "内联整个笔记本,并【写入】指定路径\n", + "- @param notebook 要内联的笔记本对象\n", + "- @param path 要写入的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(notebook::IpynbNotebook, path::AbstractString; kwargs...) =\n", + " write(\n", + " # 使用 `write`函数,自动写入内联结果\n", + " path,\n", + " # 传入前内联\n", + " inline_notebook_to_markdown(notebook; kwargs...)\n", + " )\n", + "\n", + "\"\"\"\n", + "内联指定路径的笔记本,并写入指定路径\n", + "- @param path 要读取的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(path::AbstractString, destination; kwargs...) =\n", + " inline_notebook_to_markdown(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 保存在目标路径\n", + " destination;\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造内联根目录\n", + " root_path=dirname(path),\n", + " )\n", + "\n", + "\"\"\"\n", + "内联指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\n", + "- @param path 要读取的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(path::AbstractString; kwargs...) =\n", + " inline_notebook_to_markdown(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 自动追加扩展名,作为目标路径\n", + " \"$path.md\";\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造编译根目录\n", + " root_path=dirname(path),\n", + " )\n", + "\n", + "\n", + "\n", + "# %% [31] code\n", "# ! ↓这后边注释的代码只有在编译后才会被执行\n", "# ! 仍然使用多行注释语法,以便统一格式\n", "end # module\n", @@ -2740,7 +2927,7 @@ " # 所使用的编程语言\n", " lang::Symbol,\n", " # 根路径(默认为「执行编译的文件」所在目录)\n", - " root_path::AbstractString=@__DIR__,\n", + " root_path::AbstractString=@__DIR__(),\n", " # 其它参数\n", " kwargs...)::Union{String,Nothing}\n", "\n", @@ -2775,9 +2962,20 @@ " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %include\")\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", " local relative_path = current_line[nextind(current_line, 1, length(\"$(generate_comment_inline(lang)) %include \")):end] |> rstrip # ! ←注意`%include`后边有个空格\n", - " # 读取内容\n", - " local content::String = read(joinpath(root_path, relative_path), String)\n", - " result *= content # ! 不会自动添加换行!\n", + " try\n", + " # 读取内容\n", + " local content::String = read(joinpath(root_path, relative_path), String)\n", + " result *= content # ! 不会自动添加换行!\n", + " catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联\n", + " # 读取失败,显示警告\n", + " if e isa SystemError\n", + " @warn \"引入文件「$(relative_path)」失败!错误码:$(e.errnum)\" current_line current_line_i\n", + " else\n", + " @warn \"引入文件「$(relative_path)」失败!$e\" current_line current_line_i\n", + " end\n", + " # 保留原始行\n", + " result *= current_line\n", + " end\n", "\n", " # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))\")\n", @@ -2797,20 +2995,31 @@ " if expr.head == :call && length(expr.args) > 1\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\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", + " try\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", + " catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联\n", + " # 读取失败,显示警告\n", + " if e isa SystemError\n", + " @warn \"内联文件「$(relative_path)」失败!错误码:$(e.errnum)\" current_line current_line_i\n", + " else\n", + " @warn \"内联文件「$(relative_path)」失败!$e\" current_line current_line_i\n", + " end\n", + " # 保留原始行\n", + " result *= current_line\n", " end\n", - " # 追加内容\n", - " result *= content # ! 不会自动添加换行!\n", " else # 若非`include(路径)`的形式⇒警告\n", " @warn \"非法表达式,内联失败!\" current_line expr\n", " end\n", @@ -2995,7 +3204,7 @@ { "data": { "text/plain": [ - "34-element Vector{Expr}:\n", + "35-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", @@ -3090,56 +3299,29 @@ " end))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", " ⋮\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", - " cell.cell_type == \"code\" && return parse_function(compile_cell(cell; kwargs...))\n", - " \u001b[90m#= none:23 =#\u001b[39m\n", - " return nothing\n", - " end), :(\u001b[90m#= none:26 =#\u001b[39m), :(\u001b[90m#= none:26 =#\u001b[39m Core.@doc \"解析一系列单元格\\n@param cells 单元格序列\\n@param parse_function 解析函数(替代原先`Meta.parseall`的位置)\\n@param kwargs 附加参数\\n@return 解析后的Julia表达式(可能含有错误的表达式`:error`)\\n\" function parse_cell(cells::Vector{IpynbCell}; parse_function = Meta.parseall, kwargs...)\n", - " \u001b[90m#= none:33 =#\u001b[39m\n", - " \u001b[90m#= none:34 =#\u001b[39m\n", - " return parse_function(join((compile_cell(cell; kwargs...) for cell = cells if cell.cell_type == \"code\")))\n", - " end), :(\u001b[90m#= none:45 =#\u001b[39m), :(\u001b[90m#= none:45 =#\u001b[39m Core.@doc \"尝试解析单元格\\n- 📌用法同`parse_cell`,但会在解析报错时返回`nothing`\\n - ⚠️此中「解析报错」≠「解析过程出现错误」\\n - 📝解析错误的代码会被`Meta.parseall`包裹进类似`Expr(错误)`的表达式中\\n - 例如:`Expr(:incomplete, \\\"incomplete: premature end of input\\\")`\\n\" tryparse_cell(args...; kwargs...) = begin\n", - " \u001b[90m#= none:52 =#\u001b[39m\n", - " try\n", - " \u001b[90m#= none:54 =#\u001b[39m\n", - " parse_cell(args...; kwargs...)\n", - " catch e\n", - " \u001b[90m#= none:56 =#\u001b[39m\n", - " \u001b[90m#= none:56 =#\u001b[39m @warn e\n", - " \u001b[90m#= none:57 =#\u001b[39m\n", - " showerror(stderr, e, Base.stacktrace(Base.catch_backtrace()))\n", - " \u001b[90m#= none:58 =#\u001b[39m\n", - " nothing\n", - " end\n", - " end), :(\u001b[90m#= none:61 =#\u001b[39m), :(\u001b[90m#= none:61 =#\u001b[39m Core.@doc \"执行单元格\\n- 🎯执行解析后的单元格(序列)\\n- @param code_or_codes 单元格 | 单元格序列\\n- @param eval_function 执行函数\\n - @default 默认为`Main.eval`,在全局主模块上下文执行\\n- @param kwargs 附加参数\\n- @return 执行后表达式的值\\n\" eval_cell(code_or_codes; eval_function = Main.eval, kwargs...) = begin\n", - " \u001b[90m#= none:70 =#\u001b[39m\n", - " eval_function(parse_cell(code_or_codes; kwargs...))\n", - " end))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:1 =#\u001b[39m))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:2 =#\u001b[39m), :(export compile_notebook), :(\u001b[90m#= none:4 =#\u001b[39m), :(\u001b[90m#= none:4 =#\u001b[39m Core.@doc \"编译整个笔记本\\n- 🎯编译整个笔记本对象,形成相应Julia代码\\n- 📌整体文本:头部注释+各单元格编译(逐个join(_, '\\\\n'))\\n- ⚠️末尾不会附加换行符\\n- @param notebook 要编译的笔记本对象\\n- @return 编译后的文本\\n\" compile_notebook(notebook::IpynbNotebook; lang = identify_lang(notebook), kwargs...) = begin\n", " \u001b[90m#= none:12 =#\u001b[39m\n", " \"$(compile_notebook_head(notebook; lang, kwargs...))\\n$(compile_cell(notebook.cells; lang, kwargs...))\\n\"\n", - " end), :(\u001b[90m#= none:22 =#\u001b[39m), :(\u001b[90m#= none:22 =#\u001b[39m Core.@doc \"以「配对」方式进行展开,允许同时编译多个笔记本\\n- 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\\n- 📌无论在此的「笔记本」「目标」路径还是其它的\\n\" function compile_notebook(pairs::Vararg{Pair})\n", - " \u001b[90m#= none:27 =#\u001b[39m\n", + " end), :(\u001b[90m#= none:22 =#\u001b[39m), :(\u001b[90m#= none:22 =#\u001b[39m Core.@doc \"以「配对」方式进行展开,允许同时编译多个笔记本\\n- 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\\n- 📌无论在此的「笔记本」「目标」路径还是其它的\\n- @param pairs 笔记本与目标的「配对」\\n\" function compile_notebook(pairs::Vararg{Pair})\n", " \u001b[90m#= none:28 =#\u001b[39m\n", + " \u001b[90m#= none:29 =#\u001b[39m\n", " for pair = pairs\n", - " \u001b[90m#= none:29 =#\u001b[39m\n", - " compile_notebook(first(pair), last(pair))\n", " \u001b[90m#= none:30 =#\u001b[39m\n", + " compile_notebook(first(pair), last(pair))\n", + " \u001b[90m#= none:31 =#\u001b[39m\n", " end\n", - " end), :(\u001b[90m#= none:33 =#\u001b[39m), :(\u001b[90m#= none:33 =#\u001b[39m Core.@doc \"编译整个笔记本,并【写入】指定路径\\n- @param notebook 要编译的笔记本对象\\n- @param path 要写入的路径\\n- @return 写入结果\\n\" compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = begin\n", - " \u001b[90m#= none:39 =#\u001b[39m\n", + " end), :(\u001b[90m#= none:34 =#\u001b[39m), :(\u001b[90m#= none:34 =#\u001b[39m Core.@doc \"编译整个笔记本,并【写入】指定路径\\n- @param notebook 要编译的笔记本对象\\n- @param path 要写入的路径\\n- @return 写入结果\\n\" compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = begin\n", + " \u001b[90m#= none:40 =#\u001b[39m\n", " write(path, compile_notebook(notebook; kwargs...))\n", - " end), :(\u001b[90m#= none:46 =#\u001b[39m), :(\u001b[90m#= none:46 =#\u001b[39m Core.@doc \"编译指定路径的笔记本,并写入指定路径\\n- @param path 要读取的路径\\n- @return 写入结果\\n\" compile_notebook(path::AbstractString, destination; kwargs...) = begin\n", - " \u001b[90m#= none:51 =#\u001b[39m\n", + " end), :(\u001b[90m#= none:48 =#\u001b[39m), :(\u001b[90m#= none:48 =#\u001b[39m Core.@doc \"编译指定路径的笔记本,并写入指定路径\\n- @param path 要读取的路径\\n- @return 写入结果\\n\" compile_notebook(path::AbstractString, destination; kwargs...) = begin\n", + " \u001b[90m#= none:53 =#\u001b[39m\n", " compile_notebook(IpynbNotebook(path), destination; root_path = dirname(path))\n", - " end), :(\u001b[90m#= none:61 =#\u001b[39m), :(\u001b[90m#= none:61 =#\u001b[39m Core.@doc \"编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\\n- @param path 要读取的路径\\n- @return 写入结果\\n\" function compile_notebook(path::AbstractString; kwargs...)\n", - " \u001b[90m#= none:66 =#\u001b[39m\n", - " \u001b[90m#= none:68 =#\u001b[39m\n", + " end), :(\u001b[90m#= none:64 =#\u001b[39m), :(\u001b[90m#= none:64 =#\u001b[39m Core.@doc \"编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\\n- @param path 要读取的路径\\n- @return 写入结果\\n\" function compile_notebook(path::AbstractString; kwargs...)\n", + " \u001b[90m#= none:69 =#\u001b[39m\n", + " \u001b[90m#= none:71 =#\u001b[39m\n", " local notebook::IpynbNotebook = IpynbNotebook(path)\n", - " \u001b[90m#= none:70 =#\u001b[39m\n", + " \u001b[90m#= none:73 =#\u001b[39m\n", " return compile_notebook(notebook, \"$(path).$(get_extension(identify_lang(notebook)))\"; root_path = dirname(path))\n", " end))))\n", " :($(Expr(:toplevel, :(\u001b[90m#= none:2 =#\u001b[39m), :(export parse_notebook, tryparse_notebook), :(\u001b[90m#= none:4 =#\u001b[39m), :(\u001b[90m#= none:4 =#\u001b[39m Core.@doc \"解析笔记本\\n@param notebook 笔记本\\n@param parse_function 解析函数(替代原先`Meta.parseall`的位置)\\n@param kwargs 附加参数\\n@return 解析后的Julia表达式(可能含有错误的表达式`:error`)\\n\" function parse_notebook(notebook::IpynbNotebook; parse_function = Meta.parseall, kwargs...)\n", @@ -3183,8 +3365,76 @@ " \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:2 =#\u001b[39m), :(export inline_notebook_to_markdown), :(\u001b[90m#= none:4 =#\u001b[39m), :(\u001b[90m#= none:4 =#\u001b[39m Core.@doc \"【内部】计算「为避免内部歧义所需涵盖的反引号数量」\\n- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之\\n- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712\\n\" _quote_marks(raw_content) = begin\n", + " \u001b[90m#= none:9 =#\u001b[39m\n", + " '`' ^ (maximum(findall(r\"(`+)\", raw_content) .|> length; init = 2) + 1)\n", + " end), :(\u001b[90m#= none:18 =#\u001b[39m), :(\u001b[90m#= none:18 =#\u001b[39m Core.@doc \"【内部】内联一个单元格至Markdown\\n\" function inline_cell_to_markdown(cell::IpynbCell; lang::Symbol, compile::Bool = true, kwargs...)::Union{String, Nothing}\n", + " \u001b[90m#= none:21 =#\u001b[39m\n", + " \u001b[90m#= none:28 =#\u001b[39m\n", + " local raw_content::Union{String, Nothing} = if compile\n", + " compile_code_lines(cell; lang = if cell.cell_type == \"markdown\"\n", + " :markdown\n", + " else\n", + " :julia\n", + " end, kwargs...)\n", + " else\n", + " join(cell.source)\n", + " end\n", + " \u001b[90m#= none:44 =#\u001b[39m\n", + " isnothing(raw_content) && return nothing\n", + " \u001b[90m#= none:48 =#\u001b[39m\n", + " return if cell.cell_type == \"code\"\n", + " \u001b[90m#= none:49 =#\u001b[39m\n", + " quote_marks = _quote_marks(raw_content)\n", + " \u001b[90m#= none:50 =#\u001b[39m\n", + " \"$(quote_marks)$(lang)\\n$(raw_content)\\n$(quote_marks)\"\n", + " elseif \u001b[90m#= none:56 =#\u001b[39m cell.cell_type == \"markdown\"\n", + " \u001b[90m#= none:57 =#\u001b[39m\n", + " raw_content\n", + " else\n", + " \u001b[90m#= none:59 =#\u001b[39m\n", + " \u001b[90m#= none:59 =#\u001b[39m @warn \"未支持的单元格类型:$(cell.cell_type)\"\n", + " \u001b[90m#= none:61 =#\u001b[39m\n", + " quote_marks = _quote_marks(raw_content)\n", + " \u001b[90m#= none:62 =#\u001b[39m\n", + " \"$(quote_marks)\\n$(raw_content)\\n$(quote_marks)\"\n", + " end\n", + " end), :(\u001b[90m#= none:70 =#\u001b[39m), :(\u001b[90m#= none:70 =#\u001b[39m Core.@doc \"内联整个笔记本至Markdown\\n- 🎯编译/内联整个笔记本对象,形成相应**Markdown文档**(`.md`文件)\\n - 📌可通过`compile`关键字参数选择「是否编译单元格」\\n - 默认启用「编译」\\n - ✨由此可使用Jupyter写Markdown文档\\n- 📌整体文本:各单元格编译+代码块封装\\n- ⚠️末尾固定为一个换行符\\n- @param notebook 要内联的笔记本对象\\n- @return 内联后的文本\\n\" function inline_notebook_to_markdown(notebook::IpynbNotebook; lang::Symbol = identify_lang(notebook), compile::Bool = true, kwargs...)\n", + " \u001b[90m#= none:81 =#\u001b[39m\n", + " \u001b[90m#= none:88 =#\u001b[39m\n", + " local inlined_cells::Vector{String} = String[]\n", + " \u001b[90m#= none:89 =#\u001b[39m\n", + " local inlined_cell::Union{String, Nothing}\n", + " \u001b[90m#= none:90 =#\u001b[39m\n", + " for cell = notebook.cells\n", + " \u001b[90m#= none:91 =#\u001b[39m\n", + " inlined_cell = inline_cell_to_markdown(cell; lang, compile, kwargs...)\n", + " \u001b[90m#= none:98 =#\u001b[39m\n", + " isnothing(inlined_cell) || push!(inlined_cells, inlined_cell)\n", + " \u001b[90m#= none:99 =#\u001b[39m\n", + " end\n", + " \u001b[90m#= none:101 =#\u001b[39m\n", + " (join(inlined_cells, \"\\n\\n\") |> rstrip) * '\\n'\n", + " end), :(\u001b[90m#= none:104 =#\u001b[39m), :(\u001b[90m#= none:104 =#\u001b[39m Core.@doc \"以「配对」方式进行展开,允许同时内联多个笔记本\\n- 🎯支持形如`inline_notebook_to_markdown(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\\n- 📌无论在此的「笔记本」「目标」路径还是其它的\\n- @param pairs 笔记本与目标的「配对」\\n\" function inline_notebook_to_markdown(pairs::Vararg{Pair})\n", + " \u001b[90m#= none:110 =#\u001b[39m\n", + " \u001b[90m#= none:111 =#\u001b[39m\n", + " for pair = pairs\n", + " \u001b[90m#= none:112 =#\u001b[39m\n", + " inline_notebook_to_markdown(first(pair), last(pair))\n", + " \u001b[90m#= none:113 =#\u001b[39m\n", + " end\n", + " end), :(\u001b[90m#= none:116 =#\u001b[39m), :(\u001b[90m#= none:116 =#\u001b[39m Core.@doc \"内联整个笔记本,并【写入】指定路径\\n- @param notebook 要内联的笔记本对象\\n- @param path 要写入的路径\\n- @return 写入结果\\n\" inline_notebook_to_markdown(notebook::IpynbNotebook, path::AbstractString; kwargs...) = begin\n", + " \u001b[90m#= none:122 =#\u001b[39m\n", + " write(path, inline_notebook_to_markdown(notebook; kwargs...))\n", + " end), :(\u001b[90m#= none:130 =#\u001b[39m), :(\u001b[90m#= none:130 =#\u001b[39m Core.@doc \"内联指定路径的笔记本,并写入指定路径\\n- @param path 要读取的路径\\n- @return 写入结果\\n\" inline_notebook_to_markdown(path::AbstractString, destination; kwargs...) = begin\n", + " \u001b[90m#= none:135 =#\u001b[39m\n", + " inline_notebook_to_markdown(IpynbNotebook(path), destination; root_path = dirname(path))\n", + " end), :(\u001b[90m#= none:146 =#\u001b[39m), :(\u001b[90m#= none:146 =#\u001b[39m Core.@doc \"内联指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\\n- @param path 要读取的路径\\n- @return 写入结果\\n\" inline_notebook_to_markdown(path::AbstractString; kwargs...) = begin\n", + " \u001b[90m#= none:151 =#\u001b[39m\n", + " inline_notebook_to_markdown(IpynbNotebook(path), \"$(path).md\"; root_path = dirname(path))\n", + " end))))\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: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(\"# %% [31] 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", @@ -4478,7 +4728,7 @@ " # 所使用的编程语言\n", " lang::Symbol,\n", " # 根路径(默认为「执行编译的文件」所在目录)\n", - " root_path::AbstractString=@__DIR__,\n", + " root_path::AbstractString=@__DIR__(),\n", " # 其它参数\n", " kwargs...)::Union{String,Nothing}\n", "\n", @@ -4513,9 +4763,20 @@ " elseif startswith(current_line, \"$(generate_comment_inline(lang)) %include\")\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\n", " local relative_path = current_line[nextind(current_line, 1, length(\"$(generate_comment_inline(lang)) %include \")):end] |> rstrip # ! ←注意`%include`后边有个空格\n", - " # 读取内容\n", - " local content::String = read(joinpath(root_path, relative_path), String)\n", - " result *= content # ! 不会自动添加换行!\n", + " try\n", + " # 读取内容\n", + " local content::String = read(joinpath(root_path, relative_path), String)\n", + " result *= content # ! 不会自动添加换行!\n", + " catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联\n", + " # 读取失败,显示警告\n", + " if e isa SystemError\n", + " @warn \"引入文件「$(relative_path)」失败!错误码:$(e.errnum)\" current_line current_line_i\n", + " else\n", + " @warn \"引入文件「$(relative_path)」失败!$e\" current_line current_line_i\n", + " end\n", + " # 保留原始行\n", + " result *= current_line\n", + " end\n", "\n", " # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀\n", " elseif startswith(current_line, \"$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))\")\n", @@ -4535,20 +4796,31 @@ " if expr.head == :call && length(expr.args) > 1\n", " # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联\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", + " try\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", + " catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联\n", + " # 读取失败,显示警告\n", + " if e isa SystemError\n", + " @warn \"内联文件「$(relative_path)」失败!错误码:$(e.errnum)\" current_line current_line_i\n", + " else\n", + " @warn \"内联文件「$(relative_path)」失败!$e\" current_line current_line_i\n", + " end\n", + " # 保留原始行\n", + " result *= current_line\n", " end\n", - " # 追加内容\n", - " result *= content # ! 不会自动添加换行!\n", " else # 若非`include(路径)`的形式⇒警告\n", " @warn \"非法表达式,内联失败!\" current_line expr\n", " end\n", @@ -4733,6 +5005,7 @@ "以「配对」方式进行展开,允许同时编译多个笔记本\n", "- 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\n", "- 📌无论在此的「笔记本」「目标」路径还是其它的\n", + "- @param pairs 笔记本与目标的「配对」\n", "\"\"\"\n", "function compile_notebook(pairs::Vararg{Pair})\n", " for pair in pairs\n", @@ -4746,27 +5019,29 @@ "- @param path 要写入的路径\n", "- @return 写入结果\n", "\"\"\"\n", - "compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = write(\n", - " # 使用 `write`函数,自动写入编译结果\n", - " path,\n", - " # 传入前编译\n", - " compile_notebook(notebook; kwargs...)\n", - ")\n", + "compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) =\n", + " write(\n", + " # 使用 `write`函数,自动写入编译结果\n", + " path,\n", + " # 传入前编译\n", + " compile_notebook(notebook; kwargs...)\n", + " )\n", "\n", "\"\"\"\n", "编译指定路径的笔记本,并写入指定路径\n", "- @param path 要读取的路径\n", "- @return 写入结果\n", "\"\"\"\n", - "compile_notebook(path::AbstractString, destination; kwargs...) = compile_notebook(\n", - " # 直接使用构造函数加载笔记本\n", - " IpynbNotebook(path),\n", - " # 保存在目标路径\n", - " destination;\n", - " # 其它附加参数 #\n", - " # 自动从`path`构造编译根目录\n", - " root_path=dirname(path),\n", - ")\n", + "compile_notebook(path::AbstractString, destination; kwargs...) =\n", + " compile_notebook(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 保存在目标路径\n", + " destination;\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造编译根目录\n", + " root_path=dirname(path),\n", + " )\n", "\n", "\"\"\"\n", "编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\n", @@ -4902,11 +5177,183 @@ ")\n", "\n", "\n", + "# %% [106] markdown\n", + "# ## 扩展功能\n", "\n", "# %% [107] markdown\n", - "# ## 关闭模块上下文\n", + "# ### 内联笔记本至Markdown\n", + "# \n", + "# - ✨可用于将Jupyter笔记本导出至**含代码块的Markdown文档**\n", + "# - 代码作为「代码块」内嵌于Markdown中,可保留所有单元格的内容\n", "\n", "# %% [108] code\n", + "export inline_notebook_to_markdown\n", + "\n", + "\"\"\"\n", + "【内部】计算「为避免内部歧义所需涵盖的反引号数量」\n", + "- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之\n", + "- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712\n", + "\"\"\"\n", + "_quote_marks(raw_content) =\n", + " '`' ^ (\n", + " maximum( # 取最大值\n", + " findall(r\"(`+)\", raw_content)\n", + " .|> length; # 批量求长\n", + " init=2 # 最小为2(保证最终值不小于3)\n", + " ) + 1 # 保证覆盖\n", + " )\n", + "\n", + "\"\"\"\n", + "【内部】内联一个单元格至Markdown\n", + "\"\"\"\n", + "function inline_cell_to_markdown(\n", + " cell::IpynbCell;\n", + " lang::Symbol, # ! 这是笔记本所用的语言\n", + " compile::Bool=true,\n", + " kwargs...\n", + ")::Union{String,Nothing}\n", + " # 先根据「是否编译」决定「原始码」\n", + " local raw_content::Union{String,Nothing} = (\n", + " compile ?\n", + " compile_code_lines(\n", + " cell;\n", + " lang=(\n", + " # ! 特别 对Markdown单元格做「语言特化」\n", + " cell.cell_type == \"markdown\" ?\n", + " :markdown :\n", + " :julia\n", + " ),\n", + " kwargs...\n", + " ) :\n", + " # ! ↑此处可能返回`nothing`\n", + " join(cell.source)\n", + " )\n", + " # 编译为空⇒返回空 #\n", + " isnothing(raw_content) && return nothing\n", + "\n", + " # 封装各单元格「原始码」为Markdown & 返回 #\n", + " # * Markdown单元格⇒返回自身\n", + " return if cell.cell_type == \"code\"\n", + " quote_marks = _quote_marks(raw_content)\n", + " \"\"\"\\\n", + " $(quote_marks)$lang\n", + " $(raw_content)\n", + " $(quote_marks)\\\n", + " \"\"\"\n", + " # * Markdown单元格⇒返回自身\n", + " elseif cell.cell_type == \"markdown\"\n", + " raw_content\n", + " else\n", + " @warn \"未支持的单元格类型:$(cell.cell_type)\"\n", + " # ! 仍然内联,但会放入「无语言代码块」中\n", + " quote_marks = _quote_marks(raw_content)\n", + " \"\"\"\\\n", + " $(quote_marks)\n", + " $(raw_content)\n", + " $(quote_marks)\\\n", + " \"\"\"\n", + " end\n", + "end\n", + "\n", + "\"\"\"\n", + "内联整个笔记本至Markdown\n", + "- 🎯编译/内联整个笔记本对象,形成相应**Markdown文档**(`.md`文件)\n", + " - 📌可通过`compile`关键字参数选择「是否编译单元格」\n", + " - 默认启用「编译」\n", + " - ✨由此可使用Jupyter写Markdown文档\n", + "- 📌整体文本:各单元格编译+代码块封装\n", + "- ⚠️末尾固定为一个换行符\n", + "- @param notebook 要内联的笔记本对象\n", + "- @return 内联后的文本\n", + "\"\"\"\n", + "function inline_notebook_to_markdown(\n", + " notebook::IpynbNotebook;\n", + " lang::Symbol=identify_lang(notebook),\n", + " compile::Bool=true,\n", + " kwargs...\n", + ")\n", + " # 内联所有单元格数据\n", + " local inlined_cells::Vector{String} = String[]\n", + " local inlined_cell::Union{String,Nothing}\n", + " for cell in notebook.cells\n", + " inlined_cell = inline_cell_to_markdown(\n", + " cell;\n", + " lang,\n", + " compile,\n", + " kwargs...\n", + " )\n", + " # 仅非空者加入 | 处理`%ignore-cell`的情况\n", + " isnothing(inlined_cell) || push!(inlined_cells, inlined_cell)\n", + " end\n", + " # 合并,固定末尾换行\n", + " (join(inlined_cells, \"\\n\\n\") |> rstrip) * '\\n'\n", + "end\n", + "\n", + "\"\"\"\n", + "以「配对」方式进行展开,允许同时内联多个笔记本\n", + "- 🎯支持形如`inline_notebook_to_markdown(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\n", + "- 📌无论在此的「笔记本」「目标」路径还是其它的\n", + "- @param pairs 笔记本与目标的「配对」\n", + "\"\"\"\n", + "function inline_notebook_to_markdown(pairs::Vararg{Pair})\n", + " for pair in pairs\n", + " inline_notebook_to_markdown(first(pair), last(pair))\n", + " end\n", + "end\n", + "\n", + "\"\"\"\n", + "内联整个笔记本,并【写入】指定路径\n", + "- @param notebook 要内联的笔记本对象\n", + "- @param path 要写入的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(notebook::IpynbNotebook, path::AbstractString; kwargs...) =\n", + " write(\n", + " # 使用 `write`函数,自动写入内联结果\n", + " path,\n", + " # 传入前内联\n", + " inline_notebook_to_markdown(notebook; kwargs...)\n", + " )\n", + "\n", + "\"\"\"\n", + "内联指定路径的笔记本,并写入指定路径\n", + "- @param path 要读取的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(path::AbstractString, destination; kwargs...) =\n", + " inline_notebook_to_markdown(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 保存在目标路径\n", + " destination;\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造内联根目录\n", + " root_path=dirname(path),\n", + " )\n", + "\n", + "\"\"\"\n", + "内联指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\n", + "- @param path 要读取的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(path::AbstractString; kwargs...) =\n", + " inline_notebook_to_markdown(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 自动追加扩展名,作为目标路径\n", + " \"$path.md\";\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造编译根目录\n", + " root_path=dirname(path),\n", + " )\n", + "\n", + "\n", + "\n", + "\n", + "# %% [111] markdown\n", + "# ## 关闭模块上下文\n", + "\n", + "# %% [112] code\n", "# ! ↓这后边注释的代码只有在编译后才会被执行\n", "# ! 仍然使用多行注释语法,以便统一格式\n", "end # module\n", @@ -4952,6 +5399,7 @@ "以「配对」方式进行展开,允许同时编译多个笔记本\n", "- 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\n", "- 📌无论在此的「笔记本」「目标」路径还是其它的\n", + "- @param pairs 笔记本与目标的「配对」\n", "\"\"\"\n", "function compile_notebook(pairs::Vararg{Pair})\n", " for pair in pairs\n", @@ -4965,27 +5413,29 @@ "- @param path 要写入的路径\n", "- @return 写入结果\n", "\"\"\"\n", - "compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = write(\n", - " # 使用 `write`函数,自动写入编译结果\n", - " path,\n", - " # 传入前编译\n", - " compile_notebook(notebook; kwargs...)\n", - ")\n", + "compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) =\n", + " write(\n", + " # 使用 `write`函数,自动写入编译结果\n", + " path,\n", + " # 传入前编译\n", + " compile_notebook(notebook; kwargs...)\n", + " )\n", "\n", "\"\"\"\n", "编译指定路径的笔记本,并写入指定路径\n", "- @param path 要读取的路径\n", "- @return 写入结果\n", "\"\"\"\n", - "compile_notebook(path::AbstractString, destination; kwargs...) = compile_notebook(\n", - " # 直接使用构造函数加载笔记本\n", - " IpynbNotebook(path),\n", - " # 保存在目标路径\n", - " destination;\n", - " # 其它附加参数 #\n", - " # 自动从`path`构造编译根目录\n", - " root_path=dirname(path),\n", - ")\n", + "compile_notebook(path::AbstractString, destination; kwargs...) =\n", + " compile_notebook(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 保存在目标路径\n", + " destination;\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造编译根目录\n", + " root_path=dirname(path),\n", + " )\n", "\n", "\"\"\"\n", "编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\n", @@ -5248,10 +5698,239 @@ "end" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 扩展功能" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 内联笔记本至Markdown\n", + "\n", + "- ✨可用于将Jupyter笔记本导出至**含代码块的Markdown文档**\n", + " - 代码作为「代码块」内嵌于Markdown中,可保留所有单元格的内容" + ] + }, { "cell_type": "code", "execution_count": 29, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93m\u001b[1m✅Jupyter笔记本「主模块」自内联测试成功!\u001b[22m\u001b[39m\n", + "\u001b[93m\u001b[1m(共写入 nothing 个字节)\u001b[22m\u001b[39m\n", + "\u001b[91m\u001b[1mℹ️自内联测试文件已删除!以下是文件内容:\u001b[22m\u001b[39m\n", + "nothing" + ] + } + ], + "source": [ + "#= %only-compiled # ! 模块上下文:导出元素\n", + "export inline_notebook_to_markdown\n", + "%only-compiled =#\n", + "\n", + "\"\"\"\n", + "【内部】计算「为避免内部歧义所需涵盖的反引号数量」\n", + "- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之\n", + "- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712\n", + "\"\"\"\n", + "_quote_marks(raw_content) =\n", + " '`' ^ (\n", + " maximum( # 取最大值\n", + " findall(r\"(`+)\", raw_content)\n", + " .|> length; # 批量求长\n", + " init=2 # 最小为2(保证最终值不小于3)\n", + " ) + 1 # 保证覆盖\n", + " )\n", + "\n", + "\"\"\"\n", + "【内部】内联一个单元格至Markdown\n", + "\"\"\"\n", + "function inline_cell_to_markdown(\n", + " cell::IpynbCell;\n", + " lang::Symbol, # ! 这是笔记本所用的语言\n", + " compile::Bool=true,\n", + " kwargs...\n", + ")::Union{String,Nothing}\n", + " # 先根据「是否编译」决定「原始码」\n", + " local raw_content::Union{String,Nothing} = (\n", + " compile ?\n", + " compile_code_lines(\n", + " cell;\n", + " lang=(\n", + " # ! 特别 对Markdown单元格做「语言特化」\n", + " cell.cell_type == \"markdown\" ?\n", + " :markdown :\n", + " :julia\n", + " ),\n", + " kwargs...\n", + " ) :\n", + " # ! ↑此处可能返回`nothing`\n", + " join(cell.source)\n", + " )\n", + " # 编译为空⇒返回空 #\n", + " isnothing(raw_content) && return nothing\n", + "\n", + " # 封装各单元格「原始码」为Markdown & 返回 #\n", + " # * Markdown单元格⇒返回自身\n", + " return if cell.cell_type == \"code\"\n", + " quote_marks = _quote_marks(raw_content)\n", + " \"\"\"\\\n", + " $(quote_marks)$lang\n", + " $(raw_content)\n", + " $(quote_marks)\\\n", + " \"\"\"\n", + " # * Markdown单元格⇒返回自身\n", + " elseif cell.cell_type == \"markdown\"\n", + " raw_content\n", + " else\n", + " @warn \"未支持的单元格类型:$(cell.cell_type)\"\n", + " # ! 仍然内联,但会放入「无语言代码块」中\n", + " quote_marks = _quote_marks(raw_content)\n", + " \"\"\"\\\n", + " $(quote_marks)\n", + " $(raw_content)\n", + " $(quote_marks)\\\n", + " \"\"\"\n", + " end\n", + "end\n", + "\n", + "\"\"\"\n", + "内联整个笔记本至Markdown\n", + "- 🎯编译/内联整个笔记本对象,形成相应**Markdown文档**(`.md`文件)\n", + " - 📌可通过`compile`关键字参数选择「是否编译单元格」\n", + " - 默认启用「编译」\n", + " - ✨由此可使用Jupyter写Markdown文档\n", + "- 📌整体文本:各单元格编译+代码块封装\n", + "- ⚠️末尾固定为一个换行符\n", + "- @param notebook 要内联的笔记本对象\n", + "- @return 内联后的文本\n", + "\"\"\"\n", + "function inline_notebook_to_markdown(\n", + " notebook::IpynbNotebook;\n", + " lang::Symbol=identify_lang(notebook),\n", + " compile::Bool=true,\n", + " kwargs...\n", + ")\n", + " # 内联所有单元格数据\n", + " local inlined_cells::Vector{String} = String[]\n", + " local inlined_cell::Union{String,Nothing}\n", + " for cell in notebook.cells\n", + " inlined_cell = inline_cell_to_markdown(\n", + " cell;\n", + " lang,\n", + " compile,\n", + " kwargs...\n", + " )\n", + " # 仅非空者加入 | 处理`%ignore-cell`的情况\n", + " isnothing(inlined_cell) || push!(inlined_cells, inlined_cell)\n", + " end\n", + " # 合并,固定末尾换行\n", + " (join(inlined_cells, \"\\n\\n\") |> rstrip) * '\\n'\n", + "end\n", + "\n", + "\"\"\"\n", + "以「配对」方式进行展开,允许同时内联多个笔记本\n", + "- 🎯支持形如`inline_notebook_to_markdown(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法\n", + "- 📌无论在此的「笔记本」「目标」路径还是其它的\n", + "- @param pairs 笔记本与目标的「配对」\n", + "\"\"\"\n", + "function inline_notebook_to_markdown(pairs::Vararg{Pair})\n", + " for pair in pairs\n", + " inline_notebook_to_markdown(first(pair), last(pair))\n", + " end\n", + "end\n", + "\n", + "\"\"\"\n", + "内联整个笔记本,并【写入】指定路径\n", + "- @param notebook 要内联的笔记本对象\n", + "- @param path 要写入的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(notebook::IpynbNotebook, path::AbstractString; kwargs...) =\n", + " write(\n", + " # 使用 `write`函数,自动写入内联结果\n", + " path,\n", + " # 传入前内联\n", + " inline_notebook_to_markdown(notebook; kwargs...)\n", + " )\n", + "\n", + "\"\"\"\n", + "内联指定路径的笔记本,并写入指定路径\n", + "- @param path 要读取的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(path::AbstractString, destination; kwargs...) =\n", + " inline_notebook_to_markdown(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 保存在目标路径\n", + " destination;\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造内联根目录\n", + " root_path=dirname(path),\n", + " )\n", + "\n", + "\"\"\"\n", + "内联指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】\n", + "- @param path 要读取的路径\n", + "- @return 写入结果\n", + "\"\"\"\n", + "inline_notebook_to_markdown(path::AbstractString; kwargs...) =\n", + " inline_notebook_to_markdown(\n", + " # 直接使用构造函数加载笔记本\n", + " IpynbNotebook(path),\n", + " # 自动追加扩展名,作为目标路径\n", + " \"$path.md\";\n", + " # 其它附加参数 #\n", + " # 自动从`path`构造编译根目录\n", + " root_path=dirname(path),\n", + " )\n", + "# %ignore-below\n", + "\n", + "# * 单元测试:自内联生成Markdown文档\n", + "let OUT_MD_FILE = \"IpynbCompile.md\" # 直接作为库的主文件\n", + "\n", + " # 临时创建文档\n", + " FULL_MD_PATH = joinpath(ROOT_PATH, \"src\", OUT_MD_FILE)\n", + " write_bytes = inline_notebook_to_markdown(SELF_PATH => FULL_MD_PATH)\n", + " printstyled(\n", + " \"✅Jupyter笔记本「主模块」自内联测试成功!\\n(共写入 $write_bytes 个字节)\\n\";\n", + " color=:light_yellow, bold=true\n", + " )\n", + "\n", + " # 读取&测试 文档内容\n", + " inlined_content = read(FULL_MD_PATH, String)\n", + " @assert contains(inlined_content, \"```julia\") # 具有`julia`代码块\n", + "\n", + " # 删除文档,并打印其中内容\n", + " rm(FULL_MD_PATH)\n", + " printstyled(\n", + " \"ℹ️自内联测试文件已删除!以下是文件内容:\\n\";\n", + " color=:light_red, bold=true\n", + " ) |> print\n", + "end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 整体测试" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -5260,7 +5939,7 @@ "\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", + "\u001b[94m\u001b[1m📜以下为IpynbCompile模块导出的所有20个符号:\u001b[22m\u001b[39m\n", "@cell_str\n", "@notebook_str\n", "IpynbCell\n", @@ -5274,6 +5953,7 @@ "eval_notebook_by_cell\n", "include_notebook\n", "include_notebook_by_cell\n", + "inline_notebook_to_markdown\n", "parse_cell\n", "parse_notebook\n", "read_ipynb_json\n", @@ -5310,7 +5990,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ @@ -5354,7 +6034,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -5362,7 +6042,7 @@ "output_type": "stream", "text": [ "\u001b[93m\u001b[1m✅Jupyter笔记本「主模块」自编译成功!\u001b[22m\u001b[39m\n", - "\u001b[93m\u001b[1m(共写入 51874 个字节)\u001b[22m\u001b[39m\n" + "\u001b[93m\u001b[1m(共写入 58552 个字节)\u001b[22m\u001b[39m\n" ] } ], @@ -5391,14 +6071,15 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[92m\u001b[1mCompiled: .\\compiler.ipynb => .\\compiler.jl\u001b[22m\u001b[39m\n" + "\u001b[92m\u001b[1mCompiled: .\\compiler.ipynb => .\\compiler.jl\u001b[22m\u001b[39m\n", + "\u001b[92m\u001b[1mCompiled: .\\inliner.ipynb => .\\inliner.jl\u001b[22m\u001b[39m\n" ] } ], @@ -5450,7 +6131,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -5458,7 +6139,7 @@ "output_type": "stream", "text": [ "\u001b[92m\u001b[1m✅测试文件编译成功!\u001b[22m\u001b[39m\n", - "\u001b[92m\u001b[1m(共写入 43532 个字节)\u001b[22m\u001b[39m\n" + "\u001b[92m\u001b[1m(共写入 50831 个字节)\u001b[22m\u001b[39m\n" ] } ], @@ -5510,14 +6191,14 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 35, "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", @@ -5800,7 +6481,7 @@ "8870" ] }, - "execution_count": 34, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } diff --git a/src/IpynbCompile.jl b/src/IpynbCompile.jl index 25a0a07..369ac70 100644 --- a/src/IpynbCompile.jl +++ b/src/IpynbCompile.jl @@ -1097,7 +1097,7 @@ function compile_code_lines(cell::IpynbCell; # 所使用的编程语言 lang::Symbol, # 根路径(默认为「执行编译的文件」所在目录) - root_path::AbstractString=@__DIR__, + root_path::AbstractString=@__DIR__(), # 其它参数 kwargs...)::Union{String,Nothing} @@ -1132,9 +1132,20 @@ function compile_code_lines(cell::IpynbCell; elseif startswith(current_line, "$(generate_comment_inline(lang)) %include") # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 local relative_path = current_line[nextind(current_line, 1, length("$(generate_comment_inline(lang)) %include ")):end] |> rstrip # ! ←注意`%include`后边有个空格 - # 读取内容 - local content::String = read(joinpath(root_path, relative_path), String) - result *= content # ! 不会自动添加换行! + try + # 读取内容 + local content::String = read(joinpath(root_path, relative_path), String) + result *= content # ! 不会自动添加换行! + catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联 + # 读取失败,显示警告 + if e isa SystemError + @warn "引入文件「$(relative_path)」失败!错误码:$(e.errnum)" current_line current_line_i + else + @warn "引入文件「$(relative_path)」失败!$e" current_line current_line_i + end + # 保留原始行 + result *= current_line + end # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))") @@ -1154,20 +1165,31 @@ function compile_code_lines(cell::IpynbCell; if expr.head == :call && length(expr.args) > 1 # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 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..., # 其它附加参数 - ) + try + 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 # ! 不会自动添加换行! + catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联 + # 读取失败,显示警告 + if e isa SystemError + @warn "内联文件「$(relative_path)」失败!错误码:$(e.errnum)" current_line current_line_i + else + @warn "内联文件「$(relative_path)」失败!$e" current_line current_line_i + end + # 保留原始行 + result *= current_line end - # 追加内容 - result *= content # ! 不会自动添加换行! else # 若非`include(路径)`的形式⇒警告 @warn "非法表达式,内联失败!" current_line expr end @@ -1352,6 +1374,7 @@ $(compile_cell(notebook.cells; lang, kwargs...)) 以「配对」方式进行展开,允许同时编译多个笔记本 - 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法 - 📌无论在此的「笔记本」「目标」路径还是其它的 +- @param pairs 笔记本与目标的「配对」 """ function compile_notebook(pairs::Vararg{Pair}) for pair in pairs @@ -1365,27 +1388,29 @@ end - @param path 要写入的路径 - @return 写入结果 """ -compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = write( - # 使用 `write`函数,自动写入编译结果 - path, - # 传入前编译 - compile_notebook(notebook; kwargs...) -) +compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = + write( + # 使用 `write`函数,自动写入编译结果 + path, + # 传入前编译 + compile_notebook(notebook; kwargs...) + ) """ 编译指定路径的笔记本,并写入指定路径 - @param path 要读取的路径 - @return 写入结果 """ -compile_notebook(path::AbstractString, destination; kwargs...) = compile_notebook( - # 直接使用构造函数加载笔记本 - IpynbNotebook(path), - # 保存在目标路径 - destination; - # 其它附加参数 # - # 自动从`path`构造编译根目录 - root_path=dirname(path), -) +compile_notebook(path::AbstractString, destination; kwargs...) = + compile_notebook( + # 直接使用构造函数加载笔记本 + IpynbNotebook(path), + # 保存在目标路径 + destination; + # 其它附加参数 # + # 自动从`path`构造编译根目录 + root_path=dirname(path), + ) """ 编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】 @@ -1521,11 +1546,183 @@ include_notebook_by_cell(path::AbstractString; kwargs...) = eval_notebook_by_cel ) +# %% [106] markdown +# ## 扩展功能 # %% [107] markdown -# ## 关闭模块上下文 +# ### 内联笔记本至Markdown +# +# - ✨可用于将Jupyter笔记本导出至**含代码块的Markdown文档** +# - 代码作为「代码块」内嵌于Markdown中,可保留所有单元格的内容 # %% [108] code +export inline_notebook_to_markdown + +""" +【内部】计算「为避免内部歧义所需涵盖的反引号数量」 +- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之 +- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712 +""" +_quote_marks(raw_content) = + '`' ^ ( + maximum( # 取最大值 + findall(r"(`+)", raw_content) + .|> length; # 批量求长 + init=2 # 最小为2(保证最终值不小于3) + ) + 1 # 保证覆盖 + ) + +""" +【内部】内联一个单元格至Markdown +""" +function inline_cell_to_markdown( + cell::IpynbCell; + lang::Symbol, # ! 这是笔记本所用的语言 + compile::Bool=true, + kwargs... +)::Union{String,Nothing} + # 先根据「是否编译」决定「原始码」 + local raw_content::Union{String,Nothing} = ( + compile ? + compile_code_lines( + cell; + lang=( + # ! 特别 对Markdown单元格做「语言特化」 + cell.cell_type == "markdown" ? + :markdown : + :julia + ), + kwargs... + ) : + # ! ↑此处可能返回`nothing` + join(cell.source) + ) + # 编译为空⇒返回空 # + isnothing(raw_content) && return nothing + + # 封装各单元格「原始码」为Markdown & 返回 # + # * Markdown单元格⇒返回自身 + return if cell.cell_type == "code" + quote_marks = _quote_marks(raw_content) + """\ + $(quote_marks)$lang + $(raw_content) + $(quote_marks)\ + """ + # * Markdown单元格⇒返回自身 + elseif cell.cell_type == "markdown" + raw_content + else + @warn "未支持的单元格类型:$(cell.cell_type)" + # ! 仍然内联,但会放入「无语言代码块」中 + quote_marks = _quote_marks(raw_content) + """\ + $(quote_marks) + $(raw_content) + $(quote_marks)\ + """ + end +end + +""" +内联整个笔记本至Markdown +- 🎯编译/内联整个笔记本对象,形成相应**Markdown文档**(`.md`文件) + - 📌可通过`compile`关键字参数选择「是否编译单元格」 + - 默认启用「编译」 + - ✨由此可使用Jupyter写Markdown文档 +- 📌整体文本:各单元格编译+代码块封装 +- ⚠️末尾固定为一个换行符 +- @param notebook 要内联的笔记本对象 +- @return 内联后的文本 +""" +function inline_notebook_to_markdown( + notebook::IpynbNotebook; + lang::Symbol=identify_lang(notebook), + compile::Bool=true, + kwargs... +) + # 内联所有单元格数据 + local inlined_cells::Vector{String} = String[] + local inlined_cell::Union{String,Nothing} + for cell in notebook.cells + inlined_cell = inline_cell_to_markdown( + cell; + lang, + compile, + kwargs... + ) + # 仅非空者加入 | 处理`%ignore-cell`的情况 + isnothing(inlined_cell) || push!(inlined_cells, inlined_cell) + end + # 合并,固定末尾换行 + (join(inlined_cells, "\n\n") |> rstrip) * '\n' +end + +""" +以「配对」方式进行展开,允许同时内联多个笔记本 +- 🎯支持形如`inline_notebook_to_markdown(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法 +- 📌无论在此的「笔记本」「目标」路径还是其它的 +- @param pairs 笔记本与目标的「配对」 +""" +function inline_notebook_to_markdown(pairs::Vararg{Pair}) + for pair in pairs + inline_notebook_to_markdown(first(pair), last(pair)) + end +end + +""" +内联整个笔记本,并【写入】指定路径 +- @param notebook 要内联的笔记本对象 +- @param path 要写入的路径 +- @return 写入结果 +""" +inline_notebook_to_markdown(notebook::IpynbNotebook, path::AbstractString; kwargs...) = + write( + # 使用 `write`函数,自动写入内联结果 + path, + # 传入前内联 + inline_notebook_to_markdown(notebook; kwargs...) + ) + +""" +内联指定路径的笔记本,并写入指定路径 +- @param path 要读取的路径 +- @return 写入结果 +""" +inline_notebook_to_markdown(path::AbstractString, destination; kwargs...) = + inline_notebook_to_markdown( + # 直接使用构造函数加载笔记本 + IpynbNotebook(path), + # 保存在目标路径 + destination; + # 其它附加参数 # + # 自动从`path`构造内联根目录 + root_path=dirname(path), + ) + +""" +内联指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】 +- @param path 要读取的路径 +- @return 写入结果 +""" +inline_notebook_to_markdown(path::AbstractString; kwargs...) = + inline_notebook_to_markdown( + # 直接使用构造函数加载笔记本 + IpynbNotebook(path), + # 自动追加扩展名,作为目标路径 + "$path.md"; + # 其它附加参数 # + # 自动从`path`构造编译根目录 + root_path=dirname(path), + ) + + + + +# %% [111] markdown +# ## 关闭模块上下文 + +# %% [112] code # ! ↓这后边注释的代码只有在编译后才会被执行 # ! 仍然使用多行注释语法,以便统一格式 end # module diff --git a/src/compiler.ipynb b/src/compiler.ipynb index d4830bc..95c902c 100644 --- a/src/compiler.ipynb +++ b/src/compiler.ipynb @@ -22,7 +22,7 @@ "- 可通过cmd命令行调用\n", " - 直接编译 语法:`compiler.ipynb.jl 文件名.ipynb`\n", "- 可直接打开并进入「交互模式」\n", - " - 直接键入路径,自动解析、编译并生成`.jl`文件" + " - 直接键入路径,自动解析、编译并生成源码文件" ] }, { diff --git a/src/compiler.jl b/src/compiler.jl index 3b283cb..657e1e6 100644 --- a/src/compiler.jl +++ b/src/compiler.jl @@ -15,7 +15,7 @@ # - 可通过cmd命令行调用 # - 直接编译 语法:`compiler.ipynb.jl 文件名.ipynb` # - 可直接打开并进入「交互模式」 -# - 直接键入路径,自动解析、编译并生成`.jl`文件 +# - 直接键入路径,自动解析、编译并生成源码文件 # %% [4] markdown # ## 引入模块 diff --git a/src/inliner.ipynb b/src/inliner.ipynb new file mode 100644 index 0000000..59b4eb7 --- /dev/null +++ b/src/inliner.ipynb @@ -0,0 +1,324 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# IpynbCompile.jl 交互式命令行 | 内联" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**✨执行其中所有单元格,可自动构建、测试并生成相应`.jl`源码**!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "主要用途:为 ***IpynbCompile.jl*** 提供交互访问接口\n", + "- 可通过cmd命令行调用\n", + " - 直接编译 语法:`inliner.ipynb.jl 文件名.ipynb`\n", + "- 可直接打开并进入「交互模式」\n", + " - 直接键入路径,自动解析、内联并生成`.md`文档" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 引入模块" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: replacing module IpynbCompile.\n" + ] + }, + { + "data": { + "text/plain": [ + "Main.IpynbCompile" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 编译后脚本中【直接引入】即可\n", + "LIB_IPYNB_PATH = \"IpynbCompile.ipynb\"\n", + "LIB_JL_PATH = \"IpynbCompile.jl\"\n", + "include(LIB_JL_PATH)\n", + "\n", + "# %ignore-below\n", + "# ! 在Jupyter笔记本中执行时,将尝试编译\"IpynbCompile.ipynb\"并再度引入\n", + "IpynbCompile.compile_notebook(LIB_IPYNB_PATH, LIB_JL_PATH) # 编译\n", + "include(LIB_JL_PATH) # 再次引入" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3318" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# %ignore-cell # ! 尝试编译自身\n", + "SELF_FILE = \"inliner.ipynb\"\n", + "OUT_LIB_FILE = \"inliner.jl\"\n", + "IpynbCompile.compile_notebook(SELF_FILE, OUT_LIB_FILE)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 预置函数" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "try_inline_notebook (generic function with 1 method)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "try_inline_notebook(path, destination) = try\n", + " printstyled(\"Inlining \\\"$path\\\" => \\\"$destination\\\"...\\n\", color=:white)\n", + " local num_bytes = IpynbCompile.inline_notebook_to_markdown(path, destination)\n", + " # 编译结果\n", + " printstyled(\"[√] Inlining succeed with $num_bytes bytes!\\n\", color=:light_green)\n", + "catch e\n", + " printstyled(\"[×] Inlining failed!\\n\", color=:light_red)\n", + " @error e\n", + " showerror(e)\n", + "end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 处理传入的命令行参数" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "处理方法:将其中所有参数视作**Jupyter笔记本路径**,直接【编译】成`.jl`文件" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "inline_with_ARGS (generic function with 2 methods)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# ! 📝Julia对「命令行参数」的存储:【不包括】自身路径\n", + "function inline_with_ARGS(ARGS=ARGS)\n", + " for path in ARGS\n", + " try_inline_notebook(path)\n", + " end\n", + "end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 交互模式" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "interactive_mode" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"\n", + "交互模式\n", + "- 📌不断请求输入路径\n", + "\"\"\"\n", + "function interactive_mode()\n", + " local path::AbstractString, out_path::AbstractString\n", + " # 开始循环\n", + " while true\n", + " # 请求输入路径 | 空值⇒退出\n", + " printstyled(\"IpynbCompile> path=\"; color=:light_cyan, bold=true)\n", + " path = readline()\n", + " # 空路径⇒退出\n", + " if isempty(path)\n", + " printstyled(\"Compiler exit!\\n\", color=:light_blue)\n", + " return\n", + " end\n", + " # 先读取笔记本\n", + " printstyled(\"Reading \\\"$path\\\"...\\n\", color=:white)\n", + " local notebook = IpynbCompile.read_notebook(path)\n", + " local lang = IpynbCompile.identify_lang(notebook)\n", + " local default_out = \"$path.md\" # ! 默认只会编译成`.md`文件\n", + " # 请求输出路径 | 默认为`输入路径.jl`\n", + " printstyled(\" > out_path(default \\\"$default_out\\\")=\"; color=:light_cyan, bold=true)\n", + " out_path = readline()\n", + " isempty(out_path) && (out_path = default_out) # 无⇒自动附加扩展名\n", + " try_inline_notebook(path, out_path)\n", + " end\n", + "end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 主程序" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "定义" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "main" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"主程序\"\n", + "function main()\n", + " if isempty(ARGS)\n", + " # 【无】附加参数时,进入交互模式\n", + " interactive_mode()\n", + " else\n", + " # 有附加参数时⇒编译所有文件并自动退出\n", + " compile_with_ARGS(ARGS)\n", + " end\n", + "end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "执行" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96m\u001b[1mIpynbCompile> path=\u001b[22m\u001b[39m\u001b[94mCompiler exit!\u001b[39m\n" + ] + } + ], + "source": [ + "#= %only-compiled # ! 编译后是直接执行的\n", + "main()\n", + "%only-compiled =#\n", + "# %ignore-below\n", + "#= # * 📝Jupyter笔记本中,ARGS传入的参数只有一个,并且内容类似「上下文路径」\n", + "{\n", + " \"key\": \"4a8bb47f-277d-47b5-b620-bcb1442ea48c\",\n", + " \"signature_scheme\": \"hmac-sha256\",\n", + " \"transport\": \"tcp\",\n", + " \"ip\": \"127.0.0.1\",\n", + " \"hb_port\": 9010,\n", + " \"control_port\": 9011,\n", + " \"shell_port\": 9014,\n", + " \"stdin_port\": 9015,\n", + " \"iopub_port\": 9016,\n", + " \"kernel_name\": \"julia-1.9\"\n", + "}\n", + "=#\n", + "# * Jupyter笔记本中:直接进入交互模式\n", + "interactive_mode()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.10.0", + "language": "julia", + "name": "julia-1.10" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/inliner.jl b/src/inliner.jl new file mode 100644 index 0000000..0257899 --- /dev/null +++ b/src/inliner.jl @@ -0,0 +1,118 @@ +# %% Jupyter Notebook | Julia 1.10.0 @ julia | format 2~4 +# % 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 + +# %% [1] markdown +# # IpynbCompile.jl 交互式命令行 | 内联 + +# %% [2] markdown +# **✨执行其中所有单元格,可自动构建、测试并生成相应`.jl`源码**! + +# %% [3] markdown +# 主要用途:为 ***IpynbCompile.jl*** 提供交互访问接口 +# - 可通过cmd命令行调用 +# - 直接编译 语法:`inliner.ipynb.jl 文件名.ipynb` +# - 可直接打开并进入「交互模式」 +# - 直接键入路径,自动解析、内联并生成`.md`文档 + +# %% [4] markdown +# ## 引入模块 + +# %% [5] code +# 编译后脚本中【直接引入】即可 +LIB_IPYNB_PATH = "IpynbCompile.ipynb" +LIB_JL_PATH = "IpynbCompile.jl" +include(LIB_JL_PATH) + + + + +# %% [7] markdown +# ## 预置函数 + +# %% [8] code +try_inline_notebook(path, destination) = try + printstyled("Inlining \"$path\" => \"$destination\"...\n", color=:white) + local num_bytes = IpynbCompile.inline_notebook_to_markdown(path, destination) + # 编译结果 + printstyled("[√] Inlining succeed with $num_bytes bytes!\n", color=:light_green) +catch e + printstyled("[×] Inlining failed!\n", color=:light_red) + @error e + showerror(e) +end + +# %% [9] markdown +# ## 处理传入的命令行参数 + +# %% [10] markdown +# 处理方法:将其中所有参数视作**Jupyter笔记本路径**,直接【编译】成`.jl`文件 + +# %% [11] code +# ! 📝Julia对「命令行参数」的存储:【不包括】自身路径 +function inline_with_ARGS(ARGS=ARGS) + for path in ARGS + try_inline_notebook(path) + end +end + +# %% [12] markdown +# ## 交互模式 + +# %% [13] code +""" +交互模式 +- 📌不断请求输入路径 +""" +function interactive_mode() + local path::AbstractString, out_path::AbstractString + # 开始循环 + while true + # 请求输入路径 | 空值⇒退出 + printstyled("IpynbCompile> path="; color=:light_cyan, bold=true) + path = readline() + # 空路径⇒退出 + if isempty(path) + printstyled("Compiler exit!\n", color=:light_blue) + return + end + # 先读取笔记本 + printstyled("Reading \"$path\"...\n", color=:white) + local notebook = IpynbCompile.read_notebook(path) + local lang = IpynbCompile.identify_lang(notebook) + local default_out = "$path.md" # ! 默认只会编译成`.md`文件 + # 请求输出路径 | 默认为`输入路径.jl` + printstyled(" > out_path(default \"$default_out\")="; color=:light_cyan, bold=true) + out_path = readline() + isempty(out_path) && (out_path = default_out) # 无⇒自动附加扩展名 + try_inline_notebook(path, out_path) + end +end + +# %% [14] markdown +# ## 主程序 + +# %% [15] markdown +# 定义 + +# %% [16] code +"主程序" +function main() + if isempty(ARGS) + # 【无】附加参数时,进入交互模式 + interactive_mode() + else + # 有附加参数时⇒编译所有文件并自动退出 + compile_with_ARGS(ARGS) + end +end + +# %% [17] markdown +# 执行 + +# %% [18] code +main() + + diff --git a/test/runtests.jl b/test/runtests.jl index caedef0..fe5af8b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -518,7 +518,7 @@ function compile_code_lines(cell::IpynbCell; # 所使用的编程语言 lang::Symbol, # 根路径(默认为「执行编译的文件」所在目录) - root_path::AbstractString=@__DIR__, + root_path::AbstractString=@__DIR__(), # 其它参数 kwargs...)::Union{String,Nothing} @@ -553,9 +553,20 @@ function compile_code_lines(cell::IpynbCell; elseif startswith(current_line, "$(generate_comment_inline(lang)) %include") # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 local relative_path = current_line[nextind(current_line, 1, length("$(generate_comment_inline(lang)) %include ")):end] |> rstrip # ! ←注意`%include`后边有个空格 - # 读取内容 - local content::String = read(joinpath(root_path, relative_path), String) - result *= content # ! 不会自动添加换行! + try + # 读取内容 + local content::String = read(joinpath(root_path, relative_path), String) + result *= content # ! 不会自动添加换行! + catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联 + # 读取失败,显示警告 + if e isa SystemError + @warn "引入文件「$(relative_path)」失败!错误码:$(e.errnum)" current_line current_line_i + else + @warn "引入文件「$(relative_path)」失败!$e" current_line current_line_i + end + # 保留原始行 + result *= current_line + end # * `#= %inline-compiled =# (` 读取``后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀 elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))") @@ -575,20 +586,31 @@ function compile_code_lines(cell::IpynbCell; if expr.head == :call && length(expr.args) > 1 # 在指定的「根路径」参数下行事 # * 无需使用`@inline`,编译器会自动内联 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..., # 其它附加参数 - ) + try + 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 # ! 不会自动添加换行! + catch e # *【2024-02-12 12:48:05】设立缘由:可能将Markdown的示例代码进行不必要的引入/内联 + # 读取失败,显示警告 + if e isa SystemError + @warn "内联文件「$(relative_path)」失败!错误码:$(e.errnum)" current_line current_line_i + else + @warn "内联文件「$(relative_path)」失败!$e" current_line current_line_i + end + # 保留原始行 + result *= current_line end - # 追加内容 - result *= content # ! 不会自动添加换行! else # 若非`include(路径)`的形式⇒警告 @warn "非法表达式,内联失败!" current_line expr end @@ -829,6 +851,7 @@ $(compile_cell(notebook.cells; lang, kwargs...)) 以「配对」方式进行展开,允许同时编译多个笔记本 - 🎯支持形如`compile_notebook(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法 - 📌无论在此的「笔记本」「目标」路径还是其它的 +- @param pairs 笔记本与目标的「配对」 """ function compile_notebook(pairs::Vararg{Pair}) for pair in pairs @@ -842,27 +865,29 @@ end - @param path 要写入的路径 - @return 写入结果 """ -compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = write( - # 使用 `write`函数,自动写入编译结果 - path, - # 传入前编译 - compile_notebook(notebook; kwargs...) -) +compile_notebook(notebook::IpynbNotebook, path::AbstractString; kwargs...) = + write( + # 使用 `write`函数,自动写入编译结果 + path, + # 传入前编译 + compile_notebook(notebook; kwargs...) + ) """ 编译指定路径的笔记本,并写入指定路径 - @param path 要读取的路径 - @return 写入结果 """ -compile_notebook(path::AbstractString, destination; kwargs...) = compile_notebook( - # 直接使用构造函数加载笔记本 - IpynbNotebook(path), - # 保存在目标路径 - destination; - # 其它附加参数 # - # 自动从`path`构造编译根目录 - root_path=dirname(path), -) +compile_notebook(path::AbstractString, destination; kwargs...) = + compile_notebook( + # 直接使用构造函数加载笔记本 + IpynbNotebook(path), + # 保存在目标路径 + destination; + # 其它附加参数 # + # 自动从`path`构造编译根目录 + root_path=dirname(path), + ) """ 编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】 @@ -1046,6 +1071,192 @@ let 引入路径 = joinpath(ROOT_PATH, "test", "%inline-compiled.test.ipynb") rm(引入路径) end +#= %only-compiled # ! 模块上下文:导出元素 +export inline_notebook_to_markdown +%only-compiled =# + +""" +【内部】计算「为避免内部歧义所需涵盖的反引号数量」 +- 核心方法:找到代码中最长的「`」数量,然后+1覆盖之 +- 参考:https://blog.csdn.net/qq_41437512/article/details/128436712 +""" +_quote_marks(raw_content) = + '`' ^ ( + maximum( # 取最大值 + findall(r"(`+)", raw_content) + .|> length; # 批量求长 + init=2 # 最小为2(保证最终值不小于3) + ) + 1 # 保证覆盖 + ) + +""" +【内部】内联一个单元格至Markdown +""" +function inline_cell_to_markdown( + cell::IpynbCell; + lang::Symbol, # ! 这是笔记本所用的语言 + compile::Bool=true, + kwargs... +)::Union{String,Nothing} + # 先根据「是否编译」决定「原始码」 + local raw_content::Union{String,Nothing} = ( + compile ? + compile_code_lines( + cell; + lang=( + # ! 特别 对Markdown单元格做「语言特化」 + cell.cell_type == "markdown" ? + :markdown : + :julia + ), + kwargs... + ) : + # ! ↑此处可能返回`nothing` + join(cell.source) + ) + # 编译为空⇒返回空 # + isnothing(raw_content) && return nothing + + # 封装各单元格「原始码」为Markdown & 返回 # + # * Markdown单元格⇒返回自身 + return if cell.cell_type == "code" + quote_marks = _quote_marks(raw_content) + """\ + $(quote_marks)$lang + $(raw_content) + $(quote_marks)\ + """ + # * Markdown单元格⇒返回自身 + elseif cell.cell_type == "markdown" + raw_content + else + @warn "未支持的单元格类型:$(cell.cell_type)" + # ! 仍然内联,但会放入「无语言代码块」中 + quote_marks = _quote_marks(raw_content) + """\ + $(quote_marks) + $(raw_content) + $(quote_marks)\ + """ + end +end + +""" +内联整个笔记本至Markdown +- 🎯编译/内联整个笔记本对象,形成相应**Markdown文档**(`.md`文件) + - 📌可通过`compile`关键字参数选择「是否编译单元格」 + - 默认启用「编译」 + - ✨由此可使用Jupyter写Markdown文档 +- 📌整体文本:各单元格编译+代码块封装 +- ⚠️末尾固定为一个换行符 +- @param notebook 要内联的笔记本对象 +- @return 内联后的文本 +""" +function inline_notebook_to_markdown( + notebook::IpynbNotebook; + lang::Symbol=identify_lang(notebook), + compile::Bool=true, + kwargs... +) + # 内联所有单元格数据 + local inlined_cells::Vector{String} = String[] + local inlined_cell::Union{String,Nothing} + for cell in notebook.cells + inlined_cell = inline_cell_to_markdown( + cell; + lang, + compile, + kwargs... + ) + # 仅非空者加入 | 处理`%ignore-cell`的情况 + isnothing(inlined_cell) || push!(inlined_cells, inlined_cell) + end + # 合并,固定末尾换行 + (join(inlined_cells, "\n\n") |> rstrip) * '\n' +end + +""" +以「配对」方式进行展开,允许同时内联多个笔记本 +- 🎯支持形如`inline_notebook_to_markdown(笔记本1 => 目标1, 笔记本2 => 目标2)`的语法 +- 📌无论在此的「笔记本」「目标」路径还是其它的 +- @param pairs 笔记本与目标的「配对」 +""" +function inline_notebook_to_markdown(pairs::Vararg{Pair}) + for pair in pairs + inline_notebook_to_markdown(first(pair), last(pair)) + end +end + +""" +内联整个笔记本,并【写入】指定路径 +- @param notebook 要内联的笔记本对象 +- @param path 要写入的路径 +- @return 写入结果 +""" +inline_notebook_to_markdown(notebook::IpynbNotebook, path::AbstractString; kwargs...) = + write( + # 使用 `write`函数,自动写入内联结果 + path, + # 传入前内联 + inline_notebook_to_markdown(notebook; kwargs...) + ) + +""" +内联指定路径的笔记本,并写入指定路径 +- @param path 要读取的路径 +- @return 写入结果 +""" +inline_notebook_to_markdown(path::AbstractString, destination; kwargs...) = + inline_notebook_to_markdown( + # 直接使用构造函数加载笔记本 + IpynbNotebook(path), + # 保存在目标路径 + destination; + # 其它附加参数 # + # 自动从`path`构造内联根目录 + root_path=dirname(path), + ) + +""" +内联指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】 +- @param path 要读取的路径 +- @return 写入结果 +""" +inline_notebook_to_markdown(path::AbstractString; kwargs...) = + inline_notebook_to_markdown( + # 直接使用构造函数加载笔记本 + IpynbNotebook(path), + # 自动追加扩展名,作为目标路径 + "$path.md"; + # 其它附加参数 # + # 自动从`path`构造编译根目录 + root_path=dirname(path), + ) +# %ignore-below + +# * 单元测试:自内联生成Markdown文档 +let OUT_MD_FILE = "IpynbCompile.md" # 直接作为库的主文件 + + # 临时创建文档 + FULL_MD_PATH = joinpath(ROOT_PATH, "src", OUT_MD_FILE) + write_bytes = inline_notebook_to_markdown(SELF_PATH => FULL_MD_PATH) + printstyled( + "✅Jupyter笔记本「主模块」自内联测试成功!\n(共写入 $write_bytes 个字节)\n"; + color=:light_yellow, bold=true + ) + + # 读取&测试 文档内容 + inlined_content = read(FULL_MD_PATH, String) + @test contains(inlined_content, "```julia") # 具有`julia`代码块 + + # 删除文档,并打印其中内容 + rm(FULL_MD_PATH) + printstyled( + "ℹ️自内联测试文件已删除!以下是文件内容:\n"; + color=:light_red, bold=true + ) |> print +end + # %ignore-cell # * 递回执行自身代码(自举) include_notebook(SELF_PATH)