Skip to content

Commit

Permalink
feat: ✨ 支持将笔记本内联至Markdown文件;增强代码健壮性
Browse files Browse the repository at this point in the history
支持将Jupyter笔记本单元格内联,并生成Markdown文件;附带相应的「内联器」`inliner.ipynb.jl`;现在引入/内联文件报错时,会静默失败并保留原始行
  • Loading branch information
ARCJ137442 committed Feb 12, 2024
1 parent efb7307 commit afd058e
Show file tree
Hide file tree
Showing 10 changed files with 1,758 additions and 239 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "IpynbCompile"
uuid = "4eb781bf-a71e-403a-9d46-9d48649f04b2"
authors = ["ARCJ137442 <[email protected]>"]
version = "1.7.1"
version = "1.8.0"

[deps]
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- ⚠️该文件由 `IpynbCompile.ipynb` 自动生成于 2024-02-07T19:07:22.340,无需手动修改 -->
<!-- ⚠️该文件由 `IpynbCompile.ipynb` 自动生成于 2024-02-12T13:48:58.408,无需手动修改 -->
# IpynbCompile.jl: 一个实用的Jupyter笔记本构建工具

[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
Expand Down
20 changes: 4 additions & 16 deletions examples/rust.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"vscode": {
"languageId": "rust"
}
},
"metadata": {},
"outputs": [],
"source": [
"/**\n",
Expand All @@ -65,11 +61,7 @@
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"vscode": {
"languageId": "rust"
}
},
"metadata": {},
"outputs": [],
"source": [
"/**\n",
Expand Down Expand Up @@ -100,11 +92,7 @@
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"vscode": {
"languageId": "rust"
}
},
"metadata": {},
"outputs": [
{
"name": "stdout",
Expand Down Expand Up @@ -181,7 +169,7 @@
"codemirror_mode": "rust",
"file_extension": ".rs",
"mimetype": "text/rust",
"name": "Rust",
"name": "rust",
"pygment_lexer": "rust",
"version": ""
}
Expand Down
989 changes: 835 additions & 154 deletions src/IpynbCompile.ipynb

Large diffs are not rendered by default.

263 changes: 230 additions & 33 deletions src/IpynbCompile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,7 @@ function compile_code_lines(cell::IpynbCell;
# 所使用的编程语言
lang::Symbol,
# 根路径(默认为「执行编译的文件」所在目录)
root_path::AbstractString=@__DIR__,
root_path::AbstractString=@__DIR__(),
# 其它参数
kwargs...)::Union{String,Nothing}

Expand Down Expand Up @@ -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 =# <include>(` 读取`<include>`后边指定的路径,解析其并内容作为「当前行」内联添加(不会自动添加换行!) | 仅需为行前缀
elseif startswith(current_line, "$(generate_comment_multiline_head(lang)) %inline-compiled $(generate_comment_multiline_tail(lang))")
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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),
)

"""
编译指定路径的笔记本,并根据读入的笔记本【自动追加相应扩展名】
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/compiler.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"- 可通过cmd命令行调用\n",
" - 直接编译 语法:`compiler.ipynb.jl 文件名.ipynb`\n",
"- 可直接打开并进入「交互模式」\n",
" - 直接键入路径,自动解析、编译并生成`.jl`文件"
" - 直接键入路径,自动解析、编译并生成源码文件"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/compiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# - 可通过cmd命令行调用
# - 直接编译 语法:`compiler.ipynb.jl 文件名.ipynb`
# - 可直接打开并进入「交互模式」
# - 直接键入路径,自动解析、编译并生成`.jl`文件
# - 直接键入路径,自动解析、编译并生成源码文件

# %% [4] markdown
# ## 引入模块
Expand Down
Loading

0 comments on commit afd058e

Please sign in to comment.