/ _extensions / quarto-ext / include-code-files / include-code-files.lua
include-code-files.lua
  1  --- include-code-files.lua – filter to include code from source files
  2  ---
  3  --- Copyright: © 2020 Bruno BEAUFILS
  4  --- License:   MIT – see LICENSE file for details
  5  
  6  --- Dedent a line
  7  local function dedent(line, n)
  8    return line:sub(1, n):gsub(" ", "") .. line:sub(n + 1)
  9  end
 10  
 11  --- Find snippet start and end.
 12  --
 13  --  Use this to populate startline and endline.
 14  --  This should work like pandocs snippet functionality: https://github.com/owickstrom/pandoc-include-code/tree/master
 15  local function snippet(cb, fh)
 16    if not cb.attributes.snippet then
 17      return
 18    end
 19  
 20    -- Cannot capture enum: http://lua-users.org/wiki/PatternsTutorial
 21    local comment
 22    local comment_stop = ""
 23    if
 24      string.match(cb.attributes.include, ".py$")
 25      or string.match(cb.attributes.include, ".jl$")
 26      or string.match(cb.attributes.include, ".r$")
 27    then
 28      comment = "#"
 29    elseif string.match(cb.attributes.include, ".o?js$") or string.match(cb.attributes.include, ".css$") then
 30      comment = "//"
 31    elseif string.match(cb.attributes.include, ".lua$") then
 32      comment = "--"
 33    elseif string.match(cb.attributes.include, ".html$") then
 34      comment = "<!%-%-"
 35      comment_stop = " *%-%->"
 36    else
 37      -- If not known assume that it is something one or two long and not alphanumeric.
 38      comment = "%W%W?"
 39    end
 40  
 41    local p_start = string.format("^ *%s start snippet %s%s", comment, cb.attributes.snippet, comment_stop)
 42    local p_stop = string.format("^ *%s end snippet %s%s", comment, cb.attributes.snippet, comment_stop)
 43    local start, stop = nil, nil
 44  
 45    -- Cannot use pairs.
 46    local line_no = 1
 47    for line in fh:lines() do
 48      if start == nil then
 49        if string.match(line, p_start) then
 50          start = line_no + 1
 51        end
 52      elseif stop == nil then
 53        if string.match(line, p_stop) then
 54          stop = line_no - 1
 55        end
 56      else
 57        break
 58      end
 59      line_no = line_no + 1
 60    end
 61  
 62    -- Reset so nothing is broken later on.
 63    fh:seek("set")
 64  
 65    -- If start and stop not found, just continue
 66    if start == nil or stop == nil then
 67      return nil
 68    end
 69  
 70    cb.attributes.startLine = tostring(start)
 71    cb.attributes.endLine = tostring(stop)
 72  end
 73  
 74  --- Filter function for code blocks
 75  local function transclude(cb)
 76    if cb.attributes.include then
 77      local content = ""
 78      local fh = io.open(cb.attributes.include)
 79      if not fh then
 80        io.stderr:write("Cannot open file " .. cb.attributes.include .. " | Skipping includes\n")
 81      else
 82        local number = 1
 83        local start = 1
 84  
 85        -- change hyphenated attributes to PascalCase
 86        for i, pascal in pairs({ "startLine", "endLine" }) do
 87          local hyphen = pascal:gsub("%u", "-%0"):lower()
 88          if cb.attributes[hyphen] then
 89            cb.attributes[pascal] = cb.attributes[hyphen]
 90            cb.attributes[hyphen] = nil
 91          end
 92        end
 93  
 94        -- Overwrite startLine and stopLine with the snippet if any.
 95        snippet(cb, fh)
 96  
 97        if cb.attributes.startLine then
 98          cb.attributes.startFrom = cb.attributes.startLine
 99          start = tonumber(cb.attributes.startLine)
100        end
101  
102        for line in fh:lines("L") do
103          if cb.attributes.dedent then
104            line = dedent(line, cb.attributes.dedent)
105          end
106          if number >= start then
107            if not cb.attributes.endLine or number <= tonumber(cb.attributes.endLine) then
108              content = content .. line
109            end
110          end
111          number = number + 1
112        end
113  
114        fh:close()
115      end
116  
117      -- remove key-value pair for used keys
118      cb.attributes.include = nil
119      cb.attributes.startLine = nil
120      cb.attributes.endLine = nil
121      cb.attributes.dedent = nil
122  
123      -- return final code block
124      return pandoc.CodeBlock(content, cb.attr)
125    end
126  end
127  
128  return {
129    { CodeBlock = transclude },
130  }