-- SPDX-License-Identifier: LPPL-1.3c+ require "lfs" -- Hex-encode a string. Used to build PlantUML server URLs with the "~h" marker, -- which lets us avoid the deflate-based PlantUML text encoding (that would need a -- compression library not available in plain Lua). (#6) local function plantUmlHexEncode(data) return (data:gsub(".", function(c) return string.format("%02x", string.byte(c)) end)) end -- @param mode directly passed to PlantUML. Recommended: png, svg, pdf (requires Apache Batik to convert svg to pdf) -- @param iodir directory prefix (with trailing slash) where the generated -- *-plantuml.* files live. Empty unless lualatex runs with -output-directory: -- LaTeX redirects its writes there, but this script (a shell-escape -- subprocess) runs in the real working directory, so it must reach in -- explicitly. See plantuml.sty for how the prefix is determined (#27). -- @param server PlantUML server base URL, e.g. https://www.plantuml.com/plantuml. -- When set (package option `server`, or the PLANTUML_SERVER environment -- variable as a fallback), png and svg diagrams are fetched from the server via -- curl instead of running plantuml.jar locally. The server cannot produce -- latex/TikZ output, so latex always uses the local jar. (#6) -- @param sourceFile when given, render this existing PlantUML source file -- directly instead of the generated -plantuml.txt. Used by -- \plantumlinput to render a diagram from a file. The output is still -- content-addressed by the source's hash, so caching/server/etc. apply. (#3) -- @param preambleFile when given, a global PlantUML preamble (e.g. skinparam -- styling) applied to every diagram. The local jar applies it via -config; the -- server has no such option, so it is prepended to the source (which works when -- the diagram omits @startuml, as PlantUML then wraps preamble+body together). -- Its hash is folded into the output name so changing it rebuilds diagrams. (#5) -- @return the content hash of the diagram (so plantuml.sty can include the -- hash-named output file), or nil if the diagram could not be generated. function convertPlantUmlToTikz(jobname, mode, iodir, server, sourceFile, preambleFile) iodir = iodir or "" server = server or "" if server == "" then server = os.getenv("PLANTUML_SERVER") or "" end while server:sub(-1) == "/" do server = server:sub(1, -2) end local useServer = (server ~= "") and (mode == "png" or mode == "svg") -- The source is either a user-supplied file (#3) or the generated per-diagram -- source written by the environment. local plantUmlSourceFilename if sourceFile and sourceFile ~= "" then plantUmlSourceFilename = sourceFile else plantUmlSourceFilename = iodir .. jobname .. "-plantuml.txt" end if not (lfs.attributes(plantUmlSourceFilename)) then texio.write_nl("Source " .. plantUmlSourceFilename .. " does not exist.") return nil end -- Read the source and content-address the output by its hash, minted/memoize -- style: identical (or reordered) diagrams reuse the cached file and PlantUML -- is run only when no matching output exists yet. (#2) local sourceHandle = io.open(plantUmlSourceFilename, "rb") if not sourceHandle then texio.write_nl("Error: Could not open source file for reading.") return nil end local sourceContent = sourceHandle:read("*a") io.close(sourceHandle) -- Optional global preamble (#5): read it so its hash can join the cache key and -- its content can be applied to the diagram below. local preambleContent if preambleFile and preambleFile ~= "" then local preambleHandle = io.open(preambleFile, "rb") if preambleHandle then preambleContent = preambleHandle:read("*a") io.close(preambleHandle) else texio.write_nl("PlantUML preamble \"" .. preambleFile .. "\" could not be read; ignoring.") end end local md5lib = md5 or require("md5") -- uppercase to match pdfTeX's \pdf@filemdfivesum, so both engines name the -- cache file identically for the same diagram (#2). local hash = md5lib.sumhexa(sourceContent):upper() -- Fold the preamble hash into the cache key so changing the preamble rebuilds -- every diagram (#5). if preambleContent then hash = hash .. "-" .. md5lib.sumhexa(preambleContent):upper() end local ext = (mode == "latex") and "tex" or mode -- The "-converted-to." infix means the standard TeX .gitignore (*-converted-to.*) -- already ignores these cache files, so users need no extra .gitignore entry (#2). local plantUmlTargetFilename = iodir .. "plantuml-" .. hash .. "-converted-to." .. ext -- A non-empty output for this hash means the diagram was rendered before. local function fileNonEmpty(path) local a = lfs.attributes(path) return a and a.size and a.size > 0 end if fileNonEmpty(plantUmlTargetFilename) then texio.write_nl("PlantUML cache hit \"" .. plantUmlTargetFilename .. "\"; skipping execution.") return hash end local plantUmlJar if not useServer then plantUmlJar = os.getenv("PLANTUML_JAR") if not plantUmlJar then texio.write_nl("Environment variable PLANTUML_JAR not set.") return nil end end texio.write("Executing PlantUML... ") local cmd if useServer then -- Fetch the diagram from the PlantUML server: GET //~h. -- The server has no -config, so a preamble is prepended to the source (#5). local effectiveSource = sourceContent if preambleContent then effectiveSource = preambleContent .. "\n" .. sourceContent end local url = server .. "/" .. mode .. "/~h" .. plantUmlHexEncode(effectiveSource) cmd = [[curl -sS -f -o "]] .. plantUmlTargetFilename .. [[" "]] .. url .. [["]] else -- Quote the jar path so a PLANTUML_JAR with spaces works, e.g. on Windows -- "C:\Program Files (x86)\PlantUML\plantuml.jar" (#7). cmd = [[java -Djava.awt.headless=true -jar "]] .. plantUmlJar .. [[" -charset UTF-8]] -- Apply the global preamble via PlantUML's -config (#5). if preambleContent then cmd = cmd .. [[ -config "]] .. preambleFile .. [["]] end cmd = cmd .. " -pipe -t" if (mode == "latex") then cmd = cmd .. "latex:nopreamble" else cmd = cmd .. mode end cmd = cmd .. [[ < "]] .. plantUmlSourceFilename .. [[" > "]] .. plantUmlTargetFilename .. [["]] -- PlantUML's TikZ output runs xelatex internally to measure text, and that -- xelatex hangs when TEXMF_OUTPUT_DIRECTORY holds a relative path (set by -- lualatex's -output-directory). Clear it for the PlantUML child via a command -- prefix -- os.setenv is not reliable for io.popen children across builds. (#27) if os.getenv("TEXMF_OUTPUT_DIRECTORY") then if package.config:sub(1, 1) == "\\" then cmd = [[set "TEXMF_OUTPUT_DIRECTORY=" && ]] .. cmd else cmd = "env -u TEXMF_OUTPUT_DIRECTORY " .. cmd end end end texio.write_nl(cmd) local handle,error = io.popen(cmd) if not handle then texio.write_nl("Error during execution of PlantUML.") texio.write_nl(error) return nil end io.close(handle) if not fileNonEmpty(plantUmlTargetFilename) then -- Remove a possibly empty file (e.g. a failed shell redirect) so it is not -- cached as a poisoned result, and leave no target so plantuml.sty typesets -- a visible placeholder instead of failing on a missing include. (#16) os.remove(plantUmlTargetFilename) texio.write_nl("PlantUML did not generate anything.") return nil end return hash end