-- Copyright 2016 Jeffrey Kegler
-- Permission is hereby granted, free of charge, to any person obtaining a
-- copy of this software and associated documentation files (the "Software"),
-- to deal in the Software without restriction, including without limitation
-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
-- and/or sell copies of the Software, and to permit persons to whom the
-- Software is furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-- OTHER DEALINGS IN THE SOFTWARE.
--
-- [ MIT license: http://www.opensource.org/licenses/mit-license.php ]
local file_ids = {}
local bare_arg_count = 0
for arg_ix = 1,#arg do
local this_arg = arg[arg_ix]
if not this_arg:find("=") then
bare_arg_count = bare_arg_count + 1
if bare_arg_count == 1 then
file_ids.stdin = this_arg
elseif bare_arg_count == 2 then
file_ids.stdout = this_arg
else
return nil, "Bad option: ", arg
end
else
local id, val = this_arg:match("^([^=]+)%=(.*)") -- no space around =
if not id or not val then
return nil, "Bad option: ", this_arg
end
file_ids[id] = val
end
end
file_ids.stdout = file_ids.stdout or '-'
local sections = {}
local outputs = {}
local current_section
local current_subsection
local current_prefix
local input_handle
if file_ids.stdin then
local error_message
input_handle, error_message = io.open(file_ids.stdin)
if not input_handle then error(error_message) end
else input_handle = io.input() end
local line_number = 1
local lines = {}
for line in input_handle:lines() do
lines[#lines+1] = line
end
local function form_section_name(line, words, offset)
local section_name_table = {}
if words[offset]:match("^!") then
error("Section names starting in '!' are reserved\n"
.. " Problem line: " .. line .. "\n"
)
end
for word_ix = offset,#words do
local word = words[word_ix]
section_name_table[#section_name_table+1] = word
end
return table.concat(section_name_table, ' ');
end
local function section_end()
current_section = nil
current_subsection = nil
-- I do not bother nil'ing the current_prefix
end
local function command_do(line, command_line_number)
-- print('command_do', line, command_line_number)
local prefix,command = line:match("^(%s*)--%s*luatangle:%s*(.*)$")
if not prefix then return end
if prefix:match('[^\32]') then
error("Command prefix is not all spaces -- it must be\n"
.. " Problem line: " .. line .. "\n"
)
end
local words = {}
for word in command:gmatch("[^%s]+") do
words[#words+1] = word
end
if words[1] == 'section' then
local section_name = form_section_name(line, words, 2)
if sections[section_name] then
error(
'Line ' .. line_number .. ": Section '" .. section_name .. "' already exists\n"
.. " Problem line: " .. line .. "\n"
)
end
current_subsection = {
first = command_line_number,
last = command_line_number,
prefix = prefix,
}
current_section = { current_subsection }
sections[section_name] = current_section
current_section.name = section_name
current_prefix = prefix
return
elseif words[1] == 'section+' then
local section_name = form_section_name(line, words, 2)
current_section = sections[section_name]
if not current_section then
error("Section '" .. section_name .. "' does not exist\n"
.. " Problem line: " .. line .. "\n"
)
end
current_subsection = {
first = command_line_number,
last = command_line_number,
prefix = prefix,
}
current_prefix = prefix
current_section[#current_section + 1] = current_subsection
return
elseif words[1] == 'write' then
if #words < 3 then
error("Malformed output command\n"
.. " Problem line: " .. line .. "\n"
)
end
local output_fileid = words[2]
local section_name = form_section_name(line, words, 3)
local output_section = sections[section_name]
if not output_section then
error("output command to non-existent section\n"
.. " Problem line: " .. line .. "\n"
)
end
if outputs[section_name] then
error("duplicate output command\n"
.. " Second command is: " .. line .. "\n"
)
end
local output_filename
output_filename = file_ids[output_fileid] or output_fileid
outputs[section_name] = output_filename
return
end
if not current_section then
error("Line " .. line_number .. ": command found outside of a section\n"
.. " It might be valid section command, but found outside of a section,\n"
.. " or it might simply an invalid command\n"
.. " Problem line: " .. line .. "\n"
)
end
if #words == 2 and words[1] == 'end' and words[2] == 'section' then
section_end()
return
elseif words[1] == 'insert' then
-- At this point, do nothing
return
else
error("Unknown command: " .. line .. "\n")
end
end
-- a virtual line is either a physical line or
-- a multiline command
local function read_virtual_line(first_line_no)
if first_line_no > #lines then return end
local line = lines[first_line_no]
local equal_signs = line:match("^%s*--%[(=*)%[%s*luatangle:")
if not equal_signs then
return line, first_line_no+1, first_line_no
end
local closing = ']' .. equal_signs .. ']'
local physical_lines = {}
for line_no = first_line_no,#lines do
line = lines[line_no]
local closing_start, closing_end = line:find(closing)
if closing_end then
if closing_end ~= #line then
error("Command closing has trailing characters, line #" .. line_no .. "\n"
.. ' line is "' .. lines[line_no] .. '"\n'
)
end
physical_lines[#physical_lines+1] = line:sub(1, closing_start-1)
local first_line = physical_lines[1]:gsub('%[=*%[', '', 1)
physical_lines[1] = first_line
local full_line = table.concat(physical_lines, ' ')
return full_line, line_no+1, first_line_no
end
physical_lines[#physical_lines+1] = line
end
error("Command starting at line number " .. first_line_no .. " never ends\n"
.. ' line is "' .. lines[first_line_no] .. '"\n'
)
end
while true do
local line, command_line_number
line, line_number, command_line_number = read_virtual_line(line_number)
-- print("looking at line", line)
if line == nil then break end
if current_subsection then
-- print("current subsection", current_subsection)
local line_prefix = line:sub(1,#current_prefix)
if line_prefix == current_prefix
then
command_do(line, command_line_number)
elseif line_prefix:match("^[^%s]") then
-- outdent ending section must not be a command
current_subsection.last = line_number - 2
section_end()
elseif #line ~= 0 then
local subsection_name = current_subsection.name
local subsection_name_desc
= subsection_name and '"' .. subsection_name .. '"' or '?'
error("Section " .. subsection_name_desc .. " ends in bad line\n"
.. " Problem line: " .. '#' .. line_number .. '"' .. line .. '"\n'
)
end
else
-- print("no current subsection")
command_do(line, command_line_number)
end
-- A command may have ended or started a subsection
if current_subsection then
current_subsection.last = line_number - 1
end
end
local function write_section(handle, section_name, parent_prefix, seen)
if seen[section_name] then
error("Section '" .. section_name .. "' is called recursively\n"
.. " That would create an infinite loop and is not allowed\n"
)
end
local section = sections[section_name]
if not section then
error("Section '" .. section_name .. "' is used, but it was never defined\n")
end
section.used = true
for subsection_ix = 1,#section do
local subsection = section[subsection_ix]
local subsection_prefix = subsection.prefix
local line_no = subsection.first
local last_line_of_subsection = subsection.last
while line_no <= last_line_of_subsection do
local virtual_line, first_command_line_no
virtual_line, line_no, first_command_line_no = read_virtual_line(line_no)
for command_line_no = first_command_line_no, line_no-1 do
local command_line = lines[command_line_no]:sub(#subsection_prefix+1)
handle:write(parent_prefix .. command_line, '\n')
end
virtual_line = virtual_line:sub(#subsection_prefix+1)
local insert_prefix,raw_section_name = virtual_line:match("^(%s*)--%s*luatangle:%s+insert%s+(.*)$")
if insert_prefix then
local words = {}
for word in raw_section_name:gmatch("[^%s]+") do
words[#words+1] = word
end
local insert_section_name = form_section_name(line, words, 1)
seen[section_name] = true
write_section(
handle, insert_section_name,
parent_prefix .. insert_prefix,
seen)
seen[section_name] = nil
end
end
end
end
local was_output
for section_name,output_filename in pairs(outputs) do
was_output = true
local handle
if output_filename == '-' then
handle = io.output()
else
local error_message
handle,error_message = io.open(output_filename, 'w')
if not handle then error(error_message) end
end
write_section(handle, section_name, '', {})
end
if not was_output then
error("No output commands: there must be at least one\n")
end
for section_name,section in pairs(sections) do
if not section.used then
print("Section not used: ", section_name)
end
end
-- vim: expandtab shiftwidth=4: