feat: unified watch server under org-garden
This commit is contained in:
135
org-garden/lib/org_garden/export.ex
Normal file
135
org-garden/lib/org_garden/export.ex
Normal file
@@ -0,0 +1,135 @@
|
||||
defmodule OrgGarden.Export do
|
||||
@moduledoc """
|
||||
Org-to-Markdown export via Emacs batch + ox-hugo.
|
||||
|
||||
Provides both single-file and batch export, plus a helper to compute
|
||||
the expected `.md` output path for a given `.org` source file.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Export a single `.org` file to Markdown via `emacs --batch` + ox-hugo.
|
||||
|
||||
Returns `{:ok, exit_code}` with the emacs exit code (0 = success),
|
||||
or `{:error, reason}` if the command could not be executed.
|
||||
"""
|
||||
@spec export_file(String.t(), String.t(), String.t()) :: {:ok, non_neg_integer()} | {:error, term()}
|
||||
def export_file(orgfile, notes_dir, output_dir) do
|
||||
section =
|
||||
orgfile
|
||||
|> Path.dirname()
|
||||
|> Path.relative_to(notes_dir)
|
||||
|
||||
# ox-hugo requires static/ to exist for image asset copying
|
||||
File.mkdir_p!(Path.join(output_dir, "static"))
|
||||
|
||||
{output, exit_code} =
|
||||
System.cmd(
|
||||
"emacs",
|
||||
[
|
||||
"--batch",
|
||||
"--eval", "(require 'ox-hugo)",
|
||||
"--eval", """
|
||||
(org-cite-register-processor 'passthrough
|
||||
:export-citation
|
||||
(lambda (citation _style _backend _info)
|
||||
(let ((keys (mapcar (lambda (ref)
|
||||
(concat "@" (org-element-property :key ref)))
|
||||
(org-cite-get-references citation))))
|
||||
(format "[cite:%s]" (string-join keys ";")))))
|
||||
""",
|
||||
"--eval", "(setq org-cite-export-processors '((t passthrough)))",
|
||||
"--eval", ~s[(setq org-hugo-base-dir "#{output_dir}")],
|
||||
"--eval", ~s[(setq org-hugo-default-section-directory "#{section}")],
|
||||
"--visit", orgfile,
|
||||
"--funcall", "org-hugo-export-to-md"
|
||||
],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
filtered =
|
||||
output
|
||||
|> String.split("\n")
|
||||
|> Enum.reject(&String.match?(&1, ~r/^Loading|^ad-handle|^For information/))
|
||||
|> Enum.join("\n")
|
||||
|
||||
if filtered != "", do: Logger.info("emacs: #{filtered}")
|
||||
|
||||
if exit_code == 0 do
|
||||
{:ok, exit_code}
|
||||
else
|
||||
{:error, {:emacs_exit, exit_code, filtered}}
|
||||
end
|
||||
rescue
|
||||
e -> {:error, e}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Export all `.org` files found under `notes_dir`.
|
||||
|
||||
Returns `{:ok, count}` where `count` is the number of successfully
|
||||
exported files, or `{:error, failures}` if any files failed.
|
||||
"""
|
||||
@spec export_all(String.t(), String.t()) :: {:ok, non_neg_integer()} | {:error, list()}
|
||||
def export_all(notes_dir, output_dir) do
|
||||
org_files =
|
||||
Path.join(notes_dir, "**/*.org")
|
||||
|> Path.wildcard()
|
||||
|
||||
if org_files == [] do
|
||||
Logger.warning("No .org files found in #{notes_dir}")
|
||||
{:ok, 0}
|
||||
else
|
||||
Logger.info("Exporting #{length(org_files)} org file(s) from #{notes_dir}")
|
||||
|
||||
results =
|
||||
Enum.map(org_files, fn orgfile ->
|
||||
IO.puts(" exporting: #{orgfile}")
|
||||
{orgfile, export_file(orgfile, notes_dir, output_dir)}
|
||||
end)
|
||||
|
||||
failures =
|
||||
Enum.filter(results, fn
|
||||
{_, {:ok, _}} -> false
|
||||
{_, {:error, _}} -> true
|
||||
end)
|
||||
|
||||
if failures == [] do
|
||||
{:ok, length(results)}
|
||||
else
|
||||
{:error, failures}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compute the expected `.md` path for a given `.org` file.
|
||||
|
||||
Uses the same section-mapping logic as ox-hugo: the relative directory
|
||||
of the `.org` file within `notes_dir` becomes the section directory
|
||||
under `content_dir`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> OrgGarden.Export.expected_md_path("/notes/bus/emt.org", "/notes", "/out/content")
|
||||
"/out/content/bus/emt.md"
|
||||
|
||||
iex> OrgGarden.Export.expected_md_path("/notes/top-level.org", "/notes", "/out/content")
|
||||
"/out/content/top-level.md"
|
||||
"""
|
||||
@spec expected_md_path(String.t(), String.t(), String.t()) :: String.t()
|
||||
def expected_md_path(orgfile, notes_dir, content_dir) do
|
||||
section =
|
||||
orgfile
|
||||
|> Path.dirname()
|
||||
|> Path.relative_to(notes_dir)
|
||||
|
||||
basename = Path.basename(orgfile, ".org") <> ".md"
|
||||
|
||||
case section do
|
||||
"." -> Path.join(content_dir, basename)
|
||||
_ -> Path.join([content_dir, section, basename])
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user