filters/include-files.lua

--- include-files.lua – filter to include Markdown files
---
--- use with:
--- ```{.include shift-heading-level-by=n}
--- file-a.md
--- ```
--- Copyright: © 2019–2021 Albert Krewinkel
--- License:   MIT – see LICENSE file for details
-- Module pandoc.path is required and was added in version 2.12
PANDOC_VERSION:must_be_at_least '2.12'

local List = require 'pandoc.List'
local path = require 'pandoc.path'
local system = require 'pandoc.system'

local warn = pcall(require, 'pandoc.log') and (require 'pandoc.log').warn or warn or function(msg)
    io.stderr:write(msg .. '\n')
end

--- Get include auto mode
local include_auto = false
function get_vars(meta)
    if meta['include-auto'] then
        include_auto = true
    end
end

--- Keep last heading level found
local last_heading_level = 0
function update_last_level(header)
    last_heading_level = header.level
end

--- Update contents of included file
local function update_contents(blocks, shift_by, include_path)
    local update_contents_filter = {
        -- Shift headings in block list by given number
        Header = function(header)
            if shift_by then
                header.level = header.level + shift_by
            end
            return header
        end,
        -- If image paths are relative then prepend include file path
        Image = function(image)
            if path.is_relative(image.src) then
                image.src = path.normalize(path.join({include_path, image.src}))
            end
            return image
        end,
        -- Update path for include-code-files.lua filter style CodeBlocks
        CodeBlock = function(cb)
            if cb.attributes.include and path.is_relative(cb.attributes.include) then
                cb.attributes.include = path.normalize(path.join({include_path, cb.attributes.include}))
            end
            return cb
        end
    }

    return pandoc.walk_block(pandoc.Div(blocks), update_contents_filter).content
end

--- Filter function for code blocks
local transclude
function transclude(cb)
    -- ignore code blocks which are not of class "include".
    if not cb.classes:includes 'include' then
        return
    end

    -- Markdown is used if this is nil.
    local format = cb.attributes['format']

    -- Attributes shift headings
    local shift_heading_level_by = 0
    local shift_input = cb.attributes['shift-heading-level-by']
    if shift_input then
        shift_heading_level_by = tonumber(shift_input)
    else
        if include_auto then
            -- Auto shift headings
            shift_heading_level_by = last_heading_level
        end
    end

    --- keep track of level before recusion
    local buffer_last_heading_level = last_heading_level

    local blocks = List:new()
    for line in cb.text:gmatch('[^\n]+') do
        if line:sub(1, 2) ~= '//' then
            local fh = io.open(line)
            if not fh then
                warn("Cannot open file " .. line .. " | Skipping includes")
            else
                -- read file as the given format with global reader options
                local contents = pandoc.read(fh:read '*a', format, PANDOC_READER_OPTIONS).blocks
                last_heading_level = 0
                -- recursive transclusion
                contents = system.with_working_directory(path.directory(line), function()
                    return pandoc.walk_block(pandoc.Div(contents), {
                        Header = update_last_level,
                        CodeBlock = transclude
                    })
                end).content
                --- reset to level before recursion
                last_heading_level = buffer_last_heading_level
                blocks:extend(update_contents(contents, shift_heading_level_by, path.directory(line)))
                fh:close()
            end
        end
    end
    return blocks
end

return {{
    Meta = get_vars
}, {
    Header = update_last_level,
    CodeBlock = transclude
}}