Last active
March 8, 2018 10:45
-
-
Save SquidDev/703e2f46ce68c2ca158673ff0ec4208c to your computer and use it in GitHub Desktop.
A build system for Lua (http://www.computercraft.info/forums2/index.php?/topic/21254- and https://github.com/SquidDev-CC/Howl)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local loading = {} | |
local oldRequire, preload, loaded = require, {}, { startup = loading } | |
local function require(name) | |
local result = loaded[name] | |
if result ~= nil then | |
if result == loading then | |
error("loop or previous error loading module '" .. name .. "'", 2) | |
end | |
return result | |
end | |
loaded[name] = loading | |
local contents = preload[name] | |
if contents then | |
result = contents(name) | |
elseif oldRequire then | |
result = oldRequire(name) | |
else | |
error("cannot load '" .. name .. "'", 2) | |
end | |
if result == nil then result = true end | |
loaded[name] = result | |
return result | |
end | |
preload["howl.tasks.Task"] = function(...) | |
--- The main task class | |
-- @classmod howl.tasks.Task | |
local assert = require "howl.lib.assert" | |
local class = require "howl.class" | |
local colored = require "howl.lib.colored" | |
local mixin = require "howl.class.mixin" | |
local os = require "howl.platform".os | |
local utils = require "howl.lib.utils" | |
local insert = table.insert | |
--- Convert a pattern | |
local function parsePattern(from, to) | |
local fromParsed = utils.parsePattern(from, true) | |
local toParsed = utils.parsePattern(to) | |
local newType = fromParsed.Type | |
assert(newType == toParsed.Type, "Both from and to must be the same type " .. newType .. " and " .. fromParsed.Type) | |
return { Type = newType, From = fromParsed.Text, To = toParsed.Text } | |
end | |
local Task = class("howl.tasks.Task") | |
:include(mixin.configurable) | |
:include(mixin.optionGroup) | |
:addOptions { "description" } | |
--- Create a task | |
-- @tparam string name The name of the task | |
-- @tparam table dependencies A list of tasks this task requires | |
-- @tparam function action The action to run | |
-- @treturn Task The created task | |
function Task:initialize(name, dependencies, action) | |
assert.argType(name, "string", "Task", 1) | |
-- Check calling with no dependencies | |
if type(dependencies) == "function" then | |
action = dependencies | |
dependencies = {} | |
end | |
self.options = {} | |
self.name = name -- The name of the function | |
self.action = action -- The action to call | |
self.dependencies = {} -- Task dependencies | |
self.maps = {} -- Reads and produces list | |
self.produces = {} -- Files this task produces | |
if dependencies then self:depends(dependencies) end | |
end | |
function Task.static:addDependency(class, name) | |
local function apply(self, ...) | |
if select('#', ...) == 1 and type(...) == "table" and (#(...) > 0 or next(...) == nil) then | |
local first = ... | |
for i = 1, #first do | |
insert(self.dependencies, class(self, first[i])) | |
end | |
else | |
insert(self.dependencies, class(self, ...)) | |
end | |
return self | |
end | |
self[name] = apply | |
self[name:gsub("^%l", string.upper)] = apply | |
return self | |
end | |
function Task:setup(context, runner) end | |
--- Sets a file this task produces | |
-- @tparam string|table file The path of the file | |
-- @treturn Task The current object (allows chaining) | |
function Task:Produces(file) | |
if type(file) == "table" then | |
local produces = self.produces | |
for _, file in ipairs(file) do | |
table.insert(produces, file) | |
end | |
else | |
table.insert(self.produces, file) | |
end | |
return self | |
end | |
--- Sets a file mapping | |
-- @tparam string from The file to map form | |
-- @tparam string to The file to map to | |
-- @treturn Task The current object (allows chaining) | |
function Task:Maps(from, to) | |
table.insert(self.maps, parsePattern(from, to)) | |
return self | |
end | |
--- Set the action for this task | |
-- @tparam function action The action to run | |
-- @treturn Task The current object (allows chaining) | |
function Task:Action(action) | |
self.action = action | |
return self | |
end | |
--- Run the action with no bells or whistles | |
function Task:runAction(context, ...) | |
if self.action then | |
return self.action(self, context, ...) | |
else | |
return true | |
end | |
end | |
--- Execute the task | |
-- @tparam Context.Context context The task context | |
-- @param ... The arguments to pass to task | |
-- @tparam boolean Success | |
function Task:Run(context, ...) | |
local shouldRun = false | |
if #self.dependencies == 0 then | |
shouldRun = true | |
else | |
for _, depends in ipairs(self.dependencies) do | |
if depends:resolve(context.env, context) then | |
shouldRun = true | |
end | |
end | |
end | |
if not shouldRun then return false end | |
for _, file in ipairs(self.produces) do | |
context.filesProduced[file] = true | |
end | |
-- Technically we don't need to specify an action | |
local args = { ... } | |
local description = "" | |
-- Get a list of arguments | |
if #args > 0 then | |
local newArgs = {} | |
for _, arg in ipairs(args) do | |
table.insert(newArgs, tostring(arg)) | |
end | |
description = " (" .. table.concat(newArgs, ", ") .. ")" | |
end | |
context.env.logger:info("Running %s", self.name .. description) | |
local oldTime = os.clock() | |
local s, err = true, nil | |
if context.Traceback then | |
xpcall(function() self:runAction(context.env, unpack(args)) end, function(msg) | |
for i = 5, 15 do | |
local _, err = pcall(function() error("", i) end) | |
if msg:match("Howlfile") then break end | |
msg = msg .. "\n " .. err | |
end | |
err = msg | |
s = false | |
end) | |
else | |
s, err = pcall(self.runAction, self, context.env, ...) | |
end | |
if s then | |
context.env.logger:success("%s finished", self.name) | |
else | |
context.env.logger:error("%s: %s", self.name, err or "no message") | |
error("Error running tasks", 0) | |
end | |
if context.ShowTime then | |
print(" ", "Took " .. os.clock() - oldTime .. "s") | |
end | |
return true | |
end | |
return Task | |
end | |
preload["howl.tasks.Runner"] = function(...) | |
--- Handles tasks and dependencies | |
-- @classmod howl.tasks.Runner | |
local class = require "howl.class" | |
local colored = require "howl.lib.colored" | |
local Context = require "howl.tasks.Context" | |
local mixin = require "howl.class.mixin" | |
local os = require "howl.platform".os | |
local Task = require "howl.tasks.Task" | |
--- Handles a collection of tasks and running them | |
-- @type Runner | |
local Runner = class("howl.tasks.Runner"):include(mixin.sealed) | |
--- Create a @{Runner} object | |
-- @tparam env env The current environment | |
-- @treturn Runner The created runner object | |
function Runner:initialize(env) | |
self.tasks = {} | |
self.default = nil | |
self.env = env | |
end | |
function Runner:setup() | |
for _, task in pairs(self.tasks) do | |
task:setup(self.env, self) | |
end | |
if self.env.logger.hasError then return false end | |
for _, task in pairs(self.tasks) do | |
for _, dependency in ipairs(task.dependencies) do | |
dependency:setup(self.env, self) | |
end | |
end | |
if self.env.logger.hasError then return false end | |
return true | |
end | |
--- Create a task | |
-- @tparam string name The name of the task to create | |
-- @treturn function A builder for tasks | |
function Runner:Task(name) | |
return function(dependencies, action) return self:addTask(name, dependencies, action) end | |
end | |
--- Add a task to the collection | |
-- @tparam string name The name of the task to add | |
-- @tparam table dependencies A list of tasks this task requires | |
-- @tparam function action The action to run | |
-- @treturn Task The created task | |
function Runner:addTask(name, dependencies, action) | |
return self:injectTask(Task(name, dependencies, action)) | |
end | |
--- Add a Task object to the collection | |
-- @tparam Task task The task to insert | |
-- @tparam string name The name of the task (optional) | |
-- @treturn Task The current task | |
function Runner:injectTask(task, name) | |
self.tasks[name or task.name] = task | |
return task | |
end | |
--- Set the default task | |
-- @tparam ?|string|function task The task to run or the name of the task | |
-- @treturn Runner The current object for chaining | |
function Runner:Default(task) | |
local defaultTask | |
if task == nil then | |
self.default = nil | |
elseif type(task) == "string" then | |
self.default = self.tasks[task] | |
if not self.default then | |
error("Cannot find task " .. task) | |
end | |
else | |
self.default = Task("<default>", {}, task) | |
end | |
return self | |
end | |
--- Run a task, and all its dependencies | |
-- @tparam string name Name of the task to run | |
-- @treturn Runner The current object for chaining | |
function Runner:Run(name) | |
return self:RunMany({ name }) | |
end | |
--- Run a task, and all its dependencies | |
-- @tparam table names Names of the tasks to run | |
-- @return The result of the last task | |
function Runner:RunMany(names) | |
local oldTime = os.clock() | |
local value = true | |
local context = Context(self) | |
if #names == 0 then | |
context:Start() | |
else | |
for _, name in ipairs(names) do | |
value = context:Start(name) | |
end | |
end | |
if context.ShowTime then | |
colored.printColor("orange", "Took " .. os.clock() - oldTime .. "s in total") | |
end | |
return value | |
end | |
return Runner | |
end | |
preload["howl.tasks.OptionTask"] = function(...) | |
--- A Task that can store options | |
-- @classmod howl.tasks.OptionTask | |
local assert = require "howl.lib.assert" | |
local mixin = require "howl.class.mixin" | |
local rawset = rawset | |
local Task = require "howl.tasks.Task" | |
local OptionTask = Task:subclass("howl.tasks.OptionTask") | |
:include(mixin.configurable) | |
function OptionTask:initialize(name, dependencies, keys, action) | |
Task.initialize(self, name, dependencies, action) | |
self.options = {} | |
self.optionKeys = {} | |
for _, key in ipairs(keys or {}) do | |
self:addOption(key) | |
end | |
end | |
function OptionTask:addOption(key) | |
local options = self.options | |
local func = function(self, value) | |
if value == nil then value = true end | |
options[key] = value | |
return self | |
end | |
self[key:gsub("^%l", string.upper)] = func | |
self[key] = func | |
self.optionKeys[key] = true | |
end | |
function OptionTask:configure(item) | |
assert.argType(item, "table", "configure", 1) | |
for k, v in pairs(item) do | |
if self.optionKeys[k] then | |
self.options[k] = v | |
else | |
-- TODO: Configure filtering | |
-- error("Unknown option " .. tostring(k), 2) | |
end | |
end | |
end | |
return OptionTask | |
end | |
preload["howl.tasks.Dependency"] = function(...) | |
--- An abstract class dependency | |
-- @classmod howl.tasks.Dependency | |
local class = require "howl.class" | |
local Dependency = class("howl.tasks.Dependency") | |
--- Create a new dependency | |
function Dependency:initialize(task) | |
if self.class == Dependency then | |
error("Cannot create instance of abstract class " .. tostring(Dependency), 2) | |
end | |
self.task = task | |
end | |
--- Setup the dependency, checking if it cannot be resolved | |
function Dependency:setup(context, runner) | |
error("setup has not been overridden in " .. self.class, 2) | |
end | |
--- Execute the dependency | |
-- @treturn boolean If the task was run | |
function Dependency:resolve(context, runner) | |
error("resolve has not been overridden in " .. self.class, 2) | |
end | |
return Dependency | |
end | |
preload["howl.tasks.Context"] = function(...) | |
--- Manages the running of tasks | |
-- @classmod howl.tasks.Context | |
local class = require "howl.class" | |
local fs = require "howl.platform".fs | |
local mixin = require "howl.class.mixin" | |
local platform = require "howl.platform" | |
--- Holds task contexts | |
local Context = class("howl.tasks.Context"):include(mixin.sealed) | |
--- Create a new task context | |
-- @tparam Runner.Runner runner The task runner to run tasks from | |
-- @treturn Context The resulting context | |
function Context:initialize(runner) | |
self.ran = {} -- List of task already run | |
self.filesProduced = {} | |
self.tasks = runner.tasks | |
self.default = runner.default | |
self.Traceback = runner.Traceback | |
self.ShowTime = runner.ShowTime | |
self.env = runner.env | |
self:BuildCache() | |
end | |
function Context:DoRequire(path, quite) | |
if self.filesProduced[path] then return true end | |
-- Check for normal files | |
local task = self.producesCache[path] | |
if task then | |
self.filesProduced[path] = true | |
return self:Run(task) | |
end | |
-- Check for file mapping | |
task = self.normalMapsCache[path] | |
local from, name | |
local to = path | |
if task then | |
self.filesProduced[path] = true | |
-- Convert task.Pattern.From to path | |
-- (which should be task.Pattern.To) | |
name = task.Name | |
from = task.Pattern.From | |
end | |
for match, data in pairs(self.patternMapsCache) do | |
if path:match(match) then | |
self.filesProduced[path] = true | |
-- Run task, replacing match with the replacement pattern | |
name = data.Name | |
from = path:gsub(match, data.Pattern.From) | |
break | |
end | |
end | |
if name then | |
local canCreate = self:DoRequire(from, true) | |
if not canCreate then | |
if not quite then | |
self.env.logger:error("Cannot find '" .. from .. "'") | |
end | |
return false | |
end | |
return self:Run(name, from, to) | |
end | |
if fs.exists(fs.combine(self.env.root, path)) then | |
self.filesProduced[path] = true | |
return true | |
end | |
if not quite then | |
self.env.logger:error("Cannot find a task matching '" .. path .. "'") | |
end | |
return false | |
end | |
local function arrayEquals(x, y) | |
local len = #x | |
if #x ~= #y then return false end | |
for i = 1, len do | |
if x[i] ~= y[i] then return false end | |
end | |
return true | |
end | |
--- Run a task | |
-- @tparam string|Task.Task name The name of the task or a Task object | |
-- @param ... The arguments to pass to it | |
-- @treturn boolean Success in running the task? | |
function Context:Run(name, ...) | |
local task = name | |
if type(name) == "string" then | |
task = self.tasks[name] | |
if not task then | |
error("Cannot find a task called '" .. name .. "'") | |
return false | |
end | |
elseif not task or not task.Run then | |
error("Cannot call task " .. tostring(task) .. " as it has no 'Run' method") | |
return false | |
end | |
-- Search if this task has been run with the given arguments | |
local args = { ... } | |
local ran = self.ran[task] | |
if not ran then | |
ran = { args } | |
self.ran[task] = ran | |
else | |
for i = 1, #ran do | |
if arrayEquals(args, ran[i]) then return false end | |
end | |
ran[#ran + 1] = args | |
end | |
-- Sleep before every task just in case | |
platform.refreshYield() | |
return task:Run(self, ...) | |
end | |
Context.run = Context.Run | |
--- Start the task process | |
-- @tparam string name The name of the task (Optional) | |
-- @treturn boolean Success in running the task? | |
function Context:Start(name) | |
local task | |
if name then | |
task = self.tasks[name] | |
else | |
task = self.default | |
name = "<default>" | |
end | |
if not task then | |
self.env.logger:error("Cannot find a task called '" .. name .. "'") | |
return false | |
end | |
return self:Run(task) | |
end | |
--- Build a cache of tasks | |
-- This is used to speed up finding file based tasks | |
-- @treturn Context The current context | |
function Context:BuildCache() | |
local producesCache = {} | |
local patternMapsCache = {} | |
local normalMapsCache = {} | |
self.producesCache = producesCache | |
self.patternMapsCache = patternMapsCache | |
self.normalMapsCache = normalMapsCache | |
for name, task in pairs(self.tasks) do | |
local produces = task.produces | |
if produces then | |
for _, file in ipairs(produces) do | |
local existing = producesCache[file] | |
if existing then | |
error(string.format("Both '%s' and '%s' produces '%s'", existing, name, file)) | |
end | |
producesCache[file] = name | |
end | |
end | |
local maps = task.maps | |
if maps then | |
for _, pattern in ipairs(maps) do | |
-- We store two separate caches for each of them | |
local toMap = (pattern.Type == "Pattern" and patternMapsCache or normalMapsCache) | |
local match = pattern.To | |
local existing = toMap[match] | |
if existing then | |
error(string.format("Both '%s' and '%s' match '%s'", existing, name, match)) | |
end | |
toMap[match] = { Name = name, Pattern = pattern } | |
end | |
end | |
end | |
return self | |
end | |
return Context | |
end | |
preload["howl.scratchpad"] = function(...) | |
local utils = require "howl.lib.utils" | |
local dump = require "howl.lib.dump".dump | |
local printColor = require "howl.lib.colored".printColor | |
local parsePattern = utils.parsePattern | |
local createLookup = utils.createLookup | |
local tasks = { | |
{ | |
name = "input", | |
provides = createLookup { "foo.un.lua" }, | |
}, | |
{ | |
name = "output", | |
requires = createLookup { "foo.min.lua" }, | |
}, | |
{ | |
name = "minify", | |
maps = { | |
{ | |
from = parsePattern("wild:*.lua", true), | |
to = parsePattern("wild:*.min.lua") | |
} | |
}, | |
}, | |
{ | |
name = "licence", | |
maps = { | |
{ | |
from = parsePattern("wild:*.un.lua", true), | |
to = parsePattern("wild:*.lua") | |
} | |
}, | |
}, | |
} | |
for k, v in pairs(tasks) do | |
tasks[v.name] = v | |
if not v.maps then v.maps = {} end | |
v.mapper = #v.maps > 0 | |
if not v.provides then v.provides = {} end | |
if not v.requires then v.requires = {} end | |
end | |
local function matching(name) | |
local out = {} | |
for _, task in ipairs(tasks) do | |
if task.provides[name] then | |
out[#out + 1] = { task = task.name } | |
end | |
for _, mapping in ipairs(task.maps) do | |
if mapping.to.Type == "Text" then | |
if mapping.to.Text == name then | |
out[#out + 1] = { | |
task = task.name, | |
mapping.from.Text, | |
name | |
} | |
end | |
else | |
if name:find(mapping.to.Text) then | |
out[#out + 1] = { | |
task = task.name, | |
name:gsub(mapping.to.Text, mapping.from.Text), | |
name | |
} | |
end | |
end | |
end | |
end | |
return out | |
end | |
local function resolveTasks(...) | |
local out = {} | |
local queue = {} | |
local depCache = {} | |
local function addDep(dependency, depth) | |
local hash = dependency.task .. "|"..table.concat(dependency, "|") | |
local existing = depCache[hash] | |
if existing then | |
existing.depth = math.min(existing.depth, depth) | |
return existing | |
else | |
dependency.depth = depth | |
dependency.needed = {} | |
dependency.solutions = {} | |
dependency.name = dependency.task .. ": " .. table.concat(dependency, " \26 ") | |
depCache[hash] = dependency | |
queue[#queue + 1] = dependency | |
return dependency | |
end | |
end | |
local function addSolution(solution, dependency) | |
local solution = addDep(solution, dependency.depth + 1) | |
solution.needed[#solution.needed + 1] = dependency | |
return solution | |
end | |
for i = 1, select('#', ...) do | |
addDep({ task = select(i, ...)}, 1) | |
end | |
while #queue > 0 do | |
local dependency = table.remove(queue, 1) | |
local task = tasks[dependency.task] | |
print("Task '" .. dependency.name) | |
if #dependency.needed > 0 then | |
print(" Needed for") | |
for i = 1, #dependency.needed do | |
printColor("lightGrey", " " .. dependency.needed[i].name) | |
end | |
end | |
if dependency.depth > 4 then | |
printColor("red", " Too deep") | |
elseif #dependency.solutions > 0 or (#task.requires == 0 and not task.mapper) then | |
printColor("green", " Endpoint") | |
out[#out + 1] = dependency | |
for i = 1, #dependency.needed do | |
local needed = dependency.needed[i] | |
needed.solutions[#needed.solutions + 1] = dependency | |
-- This should only happen once everything has happened | |
if #needed.solutions == 1 then | |
queue[#queue + 1] = needed | |
end | |
end | |
else | |
for i = 1, #task.requires do | |
local requirement = task.requires[i] | |
print(" Depends on '" .. requirement .. "'") | |
local matching = matching(requirement) | |
for i = 1, #matching do | |
local solution = addSolution(matching[i], dependency) | |
printColor("yellow", " Maybe: " .. solution.name) | |
end | |
end | |
if task.mapper then | |
local requirement = dependency[1] | |
print(" Depends on '" .. requirement .. "'") | |
local matching = matching(requirement) | |
for i = 1, #matching do | |
local solution = addSolution(matching[i], dependency) | |
printColor("yellow", " Maybe: " .. solution.name) | |
end | |
end | |
end | |
end | |
return out | |
end | |
-- print(dump(tasks)) | |
-- print("Resolved", dump(matching("foo.min.lua"))) | |
local resolved = resolveTasks("output") | |
for i = 1, #resolved do | |
print(resolved[i].name) | |
end | |
end | |
preload["howl.platform.oc"] = function(...) | |
--- OpenComputers's platform table | |
-- @module howl.platform.oc | |
local filesystem = require("filesystem") | |
local term = require("term") | |
local component = require("component") | |
local hasInternet = pcall(function() return component.internet end) | |
local internet = require("internet") | |
local gpu = component.gpu | |
local function read(filename) | |
local size = getSize(filename) | |
local fh = filesystem.open(filename) | |
local contents = fh:read(size) | |
fh:close() | |
return contents | |
end | |
--readDir and writeDir copied semi-verbatim from CC platform (with a slight modification) | |
local function readDir(directory) | |
local offset = #directory + 2 | |
local stack, n = { directory }, 1 | |
local files = {} | |
while n > 0 do | |
local top = stack[n] | |
n = n - 1 | |
if fs.isDir(top) then | |
for _, file in ipairs(filesystem.list(top)) do | |
n = n + 1 | |
stack[n] = filesystem.combine(top, file) | |
end | |
else | |
files[top:sub(offset)] = read(top) | |
end | |
end | |
return files | |
end | |
local function writeDir(dir, files) | |
for file, contents in pairs(files) do | |
write(filesystem.combine(dir, file), contents) | |
end | |
end | |
local function write(filename,contents) | |
local fh = filesystem.open(filename,"w") | |
local ok, err = fh:write(contents) | |
if not ok then io.stderr:write(err) end | |
fh:close() | |
end | |
local function assertExists(file,name,level) | |
if not filesystem.exists(file) then | |
error("Cannot find "..name.." (looking for "..file..")",level or 1) | |
end | |
end | |
local function getSize(file) | |
local fh = filesystem.open(file) | |
local size = fh:seek("end") | |
fh:close() | |
return size | |
end | |
local function request(url,post,headers) | |
if not hasInternet then error("No internet card found",0) end | |
local resp = "" | |
for chunk in internet.request(url,post,headers) do | |
resp = resp..chunk | |
end | |
return resp | |
end | |
local function notImplemented(name) | |
return function() error(name.." has not been implemented for OpenComputers!",2) end | |
end | |
return { | |
os = { | |
clock = os.clock, | |
time = os.time, | |
getEnv = os.getEnv, | |
}, | |
fs = { | |
-- Path manipulation | |
combine = filesystem.concat, | |
normalise = filesystem.canonical, | |
getDir = filesystem.path, | |
getName = filesystem.name, | |
currentDir = shell.getWorkingDirectory, | |
currentProgram = function() return process.info().command end, | |
-- File access | |
read = read, | |
write = write, | |
readDir = readDir, | |
writeDir = writeDir, | |
getSize = getSize, | |
-- Type checking | |
assertExists = assertExists, | |
exists = filesystem.exists, | |
isDir = filesystem.isDir, | |
-- Other | |
list = filesystem.list, | |
makeDir = filesystem.makeDir, | |
delete = filesystem.delete, | |
move = filesystem.move, | |
copy = filesystem.copy, | |
}, | |
term = { | |
setColor = gpu.setForeground, | |
resetColor = function() gpu.setForeground(colors.white) end, | |
print = print, | |
write = io.write, | |
}, | |
http = { | |
request = request, | |
}, | |
log = function() return end, | |
refreshYield = function() os.sleep(0) end, | |
} | |
end | |
preload["howl.platform.native"] = function(...) | |
--- Platform implementation for vanilla Lua | |
-- @module howl.platform.native | |
local escapeBegin = string.char(27) .. '[' | |
local colorMappings = { | |
white = 97, | |
orange = 33, | |
magenta = 95, | |
lightBlue = 94, | |
yellow = 93, | |
lime = 92, | |
pink = 95, -- No pink | |
gray = 90, grey = 90, | |
lightGray = 37, lightGrey = 37, | |
cyan = 96, | |
purple = 35, -- Dark magenta | |
blue = 36, | |
brown = 31, | |
green = 32, | |
red = 91, | |
black = 30, | |
} | |
local function notImplemented(name) | |
return function() error(name .. " is not implemented", 2) end | |
end | |
local path = require('pl.path') | |
local dir = require('pl.dir') | |
local file = require('pl.file') | |
return { | |
fs = { | |
combine = path.join, | |
normalise = path.normpath, | |
getDir = path.dirname, | |
getName = path.basename, | |
currentDir = function() return path.currentdir end, | |
read = file.read, | |
write = file.write, | |
readDir = notImplemented("fs.readDir"), | |
writeDir = notImplemented("fs.writeDir"), | |
getSize = function(n) | |
local file = io:open(n,"r") | |
local size = file:seek("end") | |
file:close() | |
return size | |
end, | |
assertExists = function(file) | |
if not path.exists(file) then | |
error("File does not exist") | |
end | |
end, | |
exists = path.exists, | |
isDir = path.isdir, | |
-- Other | |
list = function(dir) | |
local result = {} | |
for path in path.dir(dir) do | |
result[#result + 1] = path | |
end | |
return result | |
end, | |
makeDir = dir.makepath, | |
delete = function(pa) | |
if path.isdir(pa) then | |
dir.rmtree(pa) | |
else | |
file.delete(pa) | |
end | |
end, | |
move = file.move, | |
copy = file.copy, | |
}, | |
http = { | |
request = notImplemented("http.request"), | |
}, | |
term = { | |
setColor = function(color) | |
local col = colorMappings[color] | |
if not col then error("Cannot find color " .. tostring(color), 2) end | |
io.write(escapeBegin .. col .. "m") | |
io.flush() | |
end, | |
resetColor = function() | |
io.write(escapeBegin .. "0m") | |
io.flush() | |
end | |
}, | |
refreshYield = function() end | |
} | |
end | |
preload["howl.platform"] = function(...) | |
--- The native loader for platforms | |
-- @module howl.platform | |
if fs and term then | |
return require "howl.platform.cc" | |
elseif _G.component then | |
return require "howl.platform.oc" | |
else | |
return require "howl.platform.native" | |
end | |
end | |
preload["howl.platform.cc"] = function(...) | |
--- CC's platform table | |
-- @module howl.platform.cc | |
local default = term.getTextColor and term.getTextColor() or colors.white | |
local function read(file) | |
local handle = fs.open(file, "r") | |
local contents = handle.readAll() | |
handle.close() | |
return contents | |
end | |
local function write(file, contents) | |
local handle = fs.open(file, "w") | |
handle.write(contents) | |
handle.close() | |
end | |
local function assertExists(file, name, level) | |
if not fs.exists(file) then | |
error("Cannot find " .. name .. " (Looking for " .. file .. ")", level or 1) | |
end | |
end | |
local push, pull = os.queueEvent, coroutine.yield | |
local function refreshYield() | |
push("sleep") | |
if pull() == "terminate" then error("Terminated") end | |
end | |
local function readDir(directory) | |
local offset = #directory + 2 | |
local stack, n = { directory }, 1 | |
local files = {} | |
while n > 0 do | |
local top = stack[n] | |
n = n - 1 | |
if fs.isDir(top) then | |
for _, file in ipairs(fs.list(top)) do | |
n = n + 1 | |
stack[n] = fs.combine(top, file) | |
end | |
else | |
files[top:sub(offset)] = read(top) | |
end | |
end | |
return files | |
end | |
local function writeDir(dir, files) | |
for file, contents in pairs(files) do | |
write(fs.combine(dir, file), contents) | |
end | |
end | |
local request | |
if http.fetch then | |
request = function(url, post, headers) | |
local ok, err = http.fetch(url, post, headers) | |
if ok then | |
while true do | |
local event, param1, param2, param3 = os.pullEvent(e) | |
if event == "http_success" and param1 == url then | |
return true, param2 | |
elseif event == "http_failure" and param1 == url then | |
return false, param3, param2 | |
end | |
end | |
end | |
return false, nil, err | |
end | |
else | |
request = function(...) | |
local ok, result = http.post(...) | |
if ok then | |
return true, result | |
else | |
return false, nil, result | |
end | |
end | |
end | |
local getEnv | |
if settings and fs.exists(".settings") then | |
settings.load(".settings") | |
end | |
if settings and shell.getEnv then | |
getEnv = function(name, default) | |
local value = shell.getEnv(name) | |
if value ~= nil then return value end | |
return settings.get(name, default) | |
end | |
elseif settings then | |
getEnv = settings.get | |
elseif shell.getEnv then | |
getEnv = function(name, default) | |
local value = shell.getEnv(name) | |
if value ~= nil then return value end | |
return default | |
end | |
else | |
getEnv = function(name, default) return default end | |
end | |
local time | |
if profiler and profiler.milliTime then | |
time = function() return profiler.milliTime() * 1e-3 end | |
else | |
time = os.time | |
end | |
local log | |
if howlci then | |
log = howlci.log | |
else | |
log = function() end | |
end | |
return { | |
os = { | |
clock = os.clock, | |
time = time, | |
getEnv = getEnv, | |
}, | |
fs = { | |
-- Path manipulation | |
combine = fs.combine, | |
normalise = function(path) return fs.combine(path, "") end, | |
getDir = fs.getDir, | |
getName = fs.getName, | |
currentDir = shell.dir, | |
currentProgram = shell.getRunningProgram, | |
-- File access | |
read = read, | |
write = write, | |
readDir = readDir, | |
writeDir = writeDir, | |
getSize = fs.getSize, | |
-- Type checking | |
assertExists = assertExists, | |
exists = fs.exists, | |
isDir = fs.isDir, | |
-- Other | |
list = fs.list, | |
makeDir = fs.makeDir, | |
delete = fs.delete, | |
move = fs.move, | |
copy = fs.copy, | |
}, | |
term = { | |
setColor = function(color) | |
local col = colours[color] or colors[color] | |
if not col then error("Unknown color " .. color, 2) end | |
term.setTextColor(col) | |
end, | |
resetColor = function() term.setTextColor(default) end, | |
print = print, | |
write = io.write, | |
}, | |
http = { | |
request = request, | |
}, | |
log = log, | |
refreshYield = refreshYield, | |
} | |
end | |
preload["howl.packages.Proxy"] = function(...) | |
--- A proxy to a package | |
-- @classmod howl.packages.Proxy | |
local class = require "howl.class" | |
local fs = require "howl.platform".fs | |
local mixin = require "howl.class.mixin" | |
local Proxy = class("howl.packages.Proxy") | |
--- Create a new package | |
function Proxy:initialize(manager, name, package) | |
self.name = name | |
self.manager = manager | |
self.package = package | |
end | |
--- Get a unique name for this package | |
-- @treturn string The unique name | |
function Proxy:getName() | |
return self.name | |
end | |
--- Get the files for a set of metadata | |
-- @treturn table Lookup of provided files to actual path. They should not have a leading '/'. | |
function Proxy:files() | |
local cache = self.manager:getCache(self.name) | |
return self.package:files(cache) | |
end | |
--- Resolve this package, fetching if required | |
-- @tparam [string] files List of required files | |
-- @tparam boolean force Force a refresh of dependencies | |
-- @return The list of files within the package | |
function Proxy:require(files, force) | |
return self.manager:require(self.package, files, force) | |
end | |
return Proxy | |
end | |
preload["howl.packages.Package"] = function(...) | |
--- An abstract package | |
-- @classmod howl.packages.Package | |
local class = require "howl.class" | |
local fs = require "howl.platform".fs | |
local mixin = require "howl.class.mixin" | |
local Package = class("howl.packages.Package") | |
:include(mixin.configurable) | |
:include(mixin.optionGroup) | |
--- Create a new package | |
function Package:initialize(context, root) | |
if self.class == Package then | |
error("Cannot create instance of abstract class " .. tostring(Package), 2) | |
end | |
self.context = context | |
self.root = root | |
self.options = {} | |
end | |
--- Setup the package, checking if it is well formed | |
function Package:setup() | |
error("setup has not been overridden in " .. tostring(self.class), 2) | |
end | |
--- Get a unique name for this package | |
-- @treturn string The unique name | |
function Package:getName() | |
error("name has not been overridden in " .. tostring(self.class), 2) | |
end | |
--- Get the files for a set of metadata | |
-- @param cache The previous cache metadata | |
-- @treturn table Lookup of provided files to actual path. They should not have a leading '/'. | |
function Package:files(cache) | |
error("files has not been overridden in " .. tostring(self.class), 2) | |
end | |
--- Resolve this package, fetching if required | |
-- @param previous The previous cache metadata | |
-- @tparam boolean refresh Force a refresh of dependencies | |
-- @return The new cache metadata | |
function Package:require(previous, refresh) | |
error("require has not been overrriden in " .. tostring(self.class), 2) | |
end | |
return Package | |
end | |
preload["howl.packages.Manager"] = function(...) | |
--- Handles external packages | |
-- @module howl.packages.Manager | |
local class = require "howl.class" | |
local fs = require "howl.platform".fs | |
local dump = require "howl.lib.dump" | |
local mixin = require "howl.class.mixin" | |
local Proxy = require "howl.packages.Proxy" | |
local emptyCache = {} | |
local Manager = class("howl.packages.Manager") | |
Manager.providers = {} | |
function Manager:initialize(context) | |
self.context = context | |
self.packages = {} | |
self.packageLookup = {} | |
self.cache = {} | |
self.root = ".howl/packages" | |
self.alwaysRefresh = false | |
end | |
function Manager.static:addProvider(class, name) | |
self.providers[name] = class | |
end | |
function Manager:addPackage(type, details) | |
local provider = Manager.providers[type] | |
if not provider then error("No such package provider " .. type, 2) end | |
local package = provider(self.context, self.root) | |
package:configure(details) | |
local name = type .. "-" .. package:getName() | |
package.installDir = fs.combine(self.root, name) | |
self.packages[name] = package | |
self.packageLookup[package] = name | |
package:setup(self.context) | |
if self.context.logger.hasError then | |
error("Error setting up " .. name, 2) | |
end | |
return Proxy(self, name, package) | |
end | |
function Manager:getCache(name) | |
if not self.packages[name] then | |
error("No such package " .. name, 2) | |
end | |
local cache = self.cache[name] | |
local path = fs.combine(self.root, name .. ".lua") | |
if cache == nil and fs.exists(path) then | |
cache = dump.unserialise(fs.read(path)) | |
end | |
if cache == emptyCache then cache = nil end | |
return cache | |
end | |
function Manager:require(package, files, force) | |
local name = self.packageLookup[package] | |
if not name then error("No such package " .. package:getName(), 2) end | |
force = force or self.alwaysRefresh | |
local cache = self:getCache(name) | |
if cache and files and not force then | |
local existing = package:files(cache) | |
for _, file in ipairs(files) do | |
if not existing[file] then | |
force = true | |
break | |
end | |
end | |
end | |
local newData = package:require(cache, force) | |
-- TODO: Decent equality checking | |
if newData ~= cache then | |
self.context.logger:verbose("Package " .. name .. " updated") | |
if newData == nil then | |
self.cache[name] = emptyCache | |
else | |
self.cache[name] = newData | |
fs.write(fs.combine(self.root, name .. ".lua"), dump.serialise(newData)) | |
end | |
end | |
local newFiles = package:files(newData) | |
if files then | |
for _, file in ipairs(files) do | |
if not newFiles[file] then | |
error("Cannot resolve " .. file .. " for " .. name) | |
end | |
end | |
end | |
return newFiles | |
end | |
return Manager | |
end | |
preload["howl.modules.tasks.require"] = function(...) | |
--- A task that combines files that can be loaded using `require`. | |
-- @module howl.modules.tasks.require | |
local assert = require "howl.lib.assert" | |
local fs = require "howl.platform".fs | |
local mixin = require "howl.class.mixin" | |
local Buffer = require "howl.lib.Buffer" | |
local CopySource = require "howl.files.CopySource" | |
local Runner = require "howl.tasks.Runner" | |
local Task = require "howl.tasks.Task" | |
local header = require "howl.modules.tasks.require.header" | |
local envSetup = "local env = setmetatable({ require = require, preload = preload, }, { __index = getfenv() })\n" | |
local function toModule(file) | |
if file:find("%.lua$") then | |
return file:gsub("%.lua$", ""):gsub("/", "."):gsub("^(.*)%.init$", "%1") | |
end | |
end | |
local function handleRes(file) | |
if file.relative:find("%.res%.") then | |
file.name = file.name:gsub("%.res%.", ".") | |
return ("return %q"):format(file.contents) | |
end | |
end | |
local RequireTask = Task:subclass("howl.modules.require.RequireTask") | |
:include(mixin.filterable) | |
:include(mixin.delegate("sources", {"from", "include", "exclude"})) | |
:addOptions { "link", "startup", "output", "api" } | |
function RequireTask:initialize(context, name, dependencies) | |
Task.initialize(self, name, dependencies) | |
self.sources = CopySource() | |
self.sources:rename(function(file) return toModule(file.name) end) | |
self.sources:modify(handleRes) | |
self:exclude { ".git", ".svn", ".gitignore", context.out } | |
self:description("Packages files together to allow require") | |
end | |
function RequireTask:configure(item) | |
Task.configure(self, item) | |
self.sources:configure(item) | |
end | |
function RequireTask:output(value) | |
assert.argType(value, "string", "output", 1) | |
if self.options.output then error("Cannot set output multiple times") end | |
self.options.output = value | |
self:Produces(value) | |
end | |
function RequireTask:setup(context, runner) | |
Task.setup(self, context, runner) | |
if not self.options.startup then | |
context.logger:error("Task '%s': No startup file", self.name) | |
end | |
self:requires(self.options.startup) | |
if not self.options.output then | |
context.logger:error("Task '%s': No output file", self.name) | |
end | |
end | |
function RequireTask:runAction(context) | |
local files = self.sources:gatherFiles(context.root) | |
local startup = fs.combine(context.root, self.options.startup) | |
local startup_name = nil | |
local output = self.options.output | |
local link = self.options.link | |
local result = Buffer() | |
result:append(header):append("\n") | |
if link then result:append(envSetup) end | |
for _, file in pairs(files) do | |
context.logger:verbose("Including " .. file.relative) | |
result:append("preload[\"" .. file.name .. "\"] = ") | |
if link then | |
assert(fs.exists(file.path), "Cannot find " .. file.relative) | |
result:append("setfenv(assert(loadfile(\"" .. file.path .. "\")), env)\n") | |
else | |
result:append("function(...)\n" .. file.contents .. "\nend\n") | |
end | |
if file.path == startup then | |
startup_name = file.name | |
end | |
end | |
if not startup_name then | |
error("Cannot find startup file " .. self.options.startup .. " in file list", 0) | |
end | |
if self.options.api then | |
result:append("if not shell or type(... or nil) == 'table' then\n") | |
result:append("local tbl = ... or {}\n") | |
result:append("tbl.require = require tbl.preload = preload\n") | |
result:append("return tbl\n") | |
result:append("else\n") | |
end | |
result:append("return preload[\"" .. startup_name .. "\"](...)\n") | |
if self.options.api then | |
result:append("end\n") | |
end | |
fs.write(fs.combine(context.root, output), result:toString()) | |
end | |
local RequireExtensions = { } | |
function RequireExtensions:require(name, taskDepends) | |
return self:injectTask(RequireTask(self.env, name, taskDepends)) | |
end | |
local function apply() | |
Runner:include(RequireExtensions) | |
end | |
return { | |
name = "require task", | |
description = "A task that combines files that can be loaded using `require`.", | |
apply = apply, | |
RequireTask = RequireTask, | |
} | |
end | |
preload["howl.modules.tasks.require.header"] = function(...) | |
return "local loading = {}\ | |
local oldRequire, preload, loaded = require, {}, { startup = loading }\ | |
\ | |
local function require(name)\ | |
\009local result = loaded[name]\ | |
\ | |
\009if result ~= nil then\ | |
\009\009if result == loading then\ | |
\009\009\009error(\"loop or previous error loading module '\" .. name .. \"'\", 2)\ | |
\009\009end\ | |
\ | |
\009\009return result\ | |
\009end\ | |
\ | |
\009loaded[name] = loading\ | |
\009local contents = preload[name]\ | |
\009if contents then\ | |
\009\009result = contents(name)\ | |
\009elseif oldRequire then\ | |
\009\009result = oldRequire(name)\ | |
\009else\ | |
\009\009error(\"cannot load '\" .. name .. \"'\", 2)\ | |
\009end\ | |
\ | |
\009if result == nil then result = true end\ | |
\009loaded[name] = result\ | |
\009return result\ | |
end" | |
end | |
preload["howl.modules.tasks.pack.vfs"] = function(...) | |
return "local fs = fs\ | |
\ | |
local matches = {\ | |
\009[\"^\"] = \"%^\",\ | |
\009[\"$\"] = \"%$\",\ | |
\009[\"(\"] = \"%(\",\ | |
\009[\")\"] = \"%)\",\ | |
\009[\"%\"] = \"%%\",\ | |
\009[\".\"] = \"%.\",\ | |
\009[\"[\"] = \"%[\",\ | |
\009[\"]\"] = \"%]\",\ | |
\009[\"*\"] = \"%*\",\ | |
\009[\"+\"] = \"%+\",\ | |
\009[\"-\"] = \"%-\",\ | |
\009[\"?\"] = \"%?\",\ | |
\009[\"\\0\"] = \"%z\",\ | |
}\ | |
\ | |
--- Escape a string for using in a pattern\ | |
-- @tparam string pattern The string to escape\ | |
-- @treturn string The escaped pattern\ | |
local function escapePattern(pattern)\ | |
\009return (pattern:gsub(\".\", matches))\ | |
end\ | |
\ | |
local function matchesLocal(root, path)\ | |
\009return root == \"\" or path == root or path:sub(1, #root + 1) == root .. \"/\"\ | |
end\ | |
\ | |
local function extractLocal(root, path)\ | |
\009if root == \"\" then\ | |
\009\009return path\ | |
\009else\ | |
\009\009return path:sub(#root + 2)\ | |
\009end\ | |
end\ | |
\ | |
\ | |
local function copy(old)\ | |
\009local new = {}\ | |
\009for k, v in pairs(old) do new[k] = v end\ | |
\009return new\ | |
end\ | |
\ | |
--[[\ | |
\009Emulates a basic file system.\ | |
\009This doesn't have to be too advanced as it is only for Howl's use\ | |
\009The files is a list of paths to file contents, or true if the file\ | |
\009is a directory.\ | |
\009TODO: Override IO\ | |
]]\ | |
local function makeEnv(root, files)\ | |
\009-- Emulated filesystem (partially based of Oeed's)\ | |
\009files = copy(files)\ | |
\009local env\ | |
\009env = {\ | |
\009\009fs = {\ | |
\009\009\009list = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009local list = fs.isDir(path) and fs.list(path) or {}\ | |
\ | |
\009\009\009\009if matchesLocal(root, path) then\ | |
\009\009\009\009\009local pattern = \"^\" .. escapePattern(extractLocal(root, path))\ | |
\009\009\009\009\009if pattern ~= \"^\" then pattern = pattern .. '/' end\ | |
\009\009\009\009\009pattern = pattern .. '([^/]+)$'\ | |
\ | |
\009\009\009\009\009for file, _ in pairs(files) do\ | |
\009\009\009\009\009\009local name = file:match(pattern)\ | |
\009\009\009\009\009\009if name then list[#list + 1] = name end\ | |
\009\009\009\009\009end\ | |
\009\009\009\009end\ | |
\ | |
\009\009\009\009return list\ | |
\009\009\009end,\ | |
\ | |
\009\009\009exists = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if fs.exists(path) then\ | |
\009\009\009\009\009return true\ | |
\009\009\009\009elseif matchesLocal(root, path) then\ | |
\009\009\009\009\009return files[extractLocal(root, path)] ~= nil\ | |
\009\009\009\009end\ | |
\009\009\009end,\ | |
\ | |
\009\009\009isDir = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if fs.isDir(path) then\ | |
\009\009\009\009\009return true\ | |
\009\009\009\009elseif matchesLocal(root, path) then\ | |
\009\009\009\009\009return files[extractLocal(root, path)] == true\ | |
\009\009\009\009end\ | |
\009\009\009end,\ | |
\ | |
\009\009\009isReadOnly = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if fs.exists(path) then\ | |
\009\009\009\009\009return fs.isReadOnly(path)\ | |
\009\009\009\009elseif matchesLocal(root, path) and files[extractLocal(root, path)] ~= nil then\ | |
\009\009\009\009\009return true\ | |
\009\009\009\009else\ | |
\009\009\009\009\009return false\ | |
\009\009\009\009end\ | |
\009\009\009end,\ | |
\ | |
\009\009\009getName = fs.getName,\ | |
\009\009\009getDir = fs.getDir,\ | |
\009\009\009getSize = fs.getSize,\ | |
\009\009\009getFreeSpace = fs.getFreeSpace,\ | |
\009\009\009combine = fs.combine,\ | |
\ | |
\009\009\009-- TODO: This should be implemented\ | |
\009\009\009move = fs.move,\ | |
\009\009\009copy = fs.copy,\ | |
\009\009\009makeDir = function(dir)\ | |
\ | |
\009\009\009end,\ | |
\009\009\009delete = fs.delete,\ | |
\ | |
\009\009\009open = function(path, mode)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if matchesLocal(root, path) then\ | |
\009\009\009\009\009local localPath = extractLocal(root, path)\ | |
\009\009\009\009\009if type(files[localPath]) == 'string' then\ | |
\009\009\009\009\009\009local handle = {close = function()end}\ | |
\009\009\009\009\009\009if mode == 'r' then\ | |
\009\009\009\009\009\009\009local content = files[localPath]\ | |
\009\009\009\009\009\009\009handle.readAll = function()\ | |
\009\009\009\009\009\009\009\009return content\ | |
\009\009\009\009\009\009\009end\ | |
\ | |
\009\009\009\009\009\009\009local line = 1\ | |
\009\009\009\009\009\009\009local lines\ | |
\009\009\009\009\009\009\009handle.readLine = function()\ | |
\009\009\009\009\009\009\009\009if not lines then -- Lazy load lines\ | |
\009\009\009\009\009\009\009\009\009lines = {content:match((content:gsub(\"[^\\n]+\\n?\", \"([^\\n]+)\\n?\")))}\ | |
\009\009\009\009\009\009\009\009end\ | |
\009\009\009\009\009\009\009\009if line > #lines then\ | |
\009\009\009\009\009\009\009\009\009return nil\ | |
\009\009\009\009\009\009\009\009else\ | |
\009\009\009\009\009\009\009\009\009return lines[line]\ | |
\009\009\009\009\009\009\009\009end\ | |
\009\009\009\009\009\009\009\009line = line + 1\ | |
\009\009\009\009\009\009\009end\ | |
\ | |
\009\009\009\009\009\009\009return handle\ | |
\009\009\009\009\009\009else\ | |
\009\009\009\009\009\009\009error('Cannot write to read-only file.', 2)\ | |
\009\009\009\009\009\009end\ | |
\009\009\009\009\009end\ | |
\009\009\009\009end\ | |
\ | |
\009\009\009\009return fs.open(path, mode)\ | |
\009\009\009end\ | |
\009\009},\ | |
\ | |
\009\009loadfile = function(name)\ | |
\009\009\009local file = env.fs.open(name, \"r\")\ | |
\009\009\009if file then\ | |
\009\009\009\009local func, err = load(file.readAll(), fs.getName(name), nil, env)\ | |
\009\009\009\009file.close()\ | |
\009\009\009\009return func, err\ | |
\009\009\009end\ | |
\009\009\009return nil, \"File not found: \"..name\ | |
\009\009end,\ | |
\ | |
\009\009dofile = function(name)\ | |
\009\009\009local file, e = env.loadfile(name, env)\ | |
\009\009\009if file then\ | |
\009\009\009\009return file()\ | |
\009\009\009else\ | |
\009\009\009\009error(e, 2)\ | |
\009\009\009end\ | |
\009\009end,\ | |
\009}\ | |
\ | |
\009env._G = env\ | |
\009env._ENV = env\ | |
\009return setmetatable(env, {__index = _ENV or getfenv()})\ | |
end\ | |
\ | |
local function extract(root, files, from, to)\ | |
\009local pattern = \"^\" .. escapePattern(extractLocal(root, from))\ | |
\009if pattern ~= \"^\" then pattern = pattern .. '/' end\ | |
\009pattern = pattern .. '(.*)$'\ | |
\ | |
\009for file, contents in pairs(files) do\ | |
\009\009local name = file:match(pattern)\ | |
\009\009if name then\ | |
\009\009\009print(\"Extracting \" .. name)\ | |
\009\009\009local handle = fs.open(fs.combine(to, name), \"w\")\ | |
\009\009\009handle.write(contents)\ | |
\009\009\009handle.close()\ | |
\009\009end\ | |
\009end\ | |
end" | |
end | |
preload["howl.modules.tasks.pack.template"] = function(...) | |
return "local files = ${files}\ | |
\ | |
${vfs}\ | |
\ | |
local root = \"\"\ | |
local args = {...}\ | |
if #args == 1 and args[1] == '--extract' then\ | |
\009extract(root, files, \"\", root)\ | |
else\ | |
\009local env = makeEnv(root, files)\ | |
\009local func, err = env.loadfile(${startup})\ | |
\009if not func then error(err, 0) end\ | |
\009return func(...)\ | |
end" | |
end | |
preload["howl.modules.tasks.pack"] = function(...) | |
--- A task to combine multiple files into one which are then executed within a virtual file system. | |
-- @module howl.modules.tasks.Pack | |
local assert = require "howl.lib.assert" | |
local dump = require "howl.lib.dump" | |
local fs = require "howl.platform".fs | |
local mixin = require "howl.class.mixin" | |
local rebuild = require "howl.lexer.rebuild" | |
local CopySource = require "howl.files.CopySource" | |
local Runner = require "howl.tasks.Runner" | |
local Task = require "howl.tasks.Task" | |
local formatTemplate = require "howl.lib.utils".formatTemplate | |
local template = require "howl.modules.tasks.pack.template" | |
local vfs = require "howl.modules.tasks.pack.vfs" | |
local PackTask = Task:subclass("howl.modules.tasks.pack.PackTask") | |
:include(mixin.filterable) | |
:include(mixin.delegate("sources", {"from", "include", "exclude"})) | |
:addOptions { "minify", "startup", "output" } | |
function PackTask:initialize(context, name, dependencies) | |
Task.initialize(self, name, dependencies) | |
self.root = context.root | |
self.sources = CopySource() | |
self.sources:modify(function(file) | |
local contents = file.contents | |
if self.options.minify and loadstring(contents) then | |
return rebuild.minifyString(contents) | |
end | |
end) | |
self:exclude { ".git", ".svn", ".gitignore", context.out } | |
self:description("Combines multiple files using Pack") | |
end | |
function PackTask:configure(item) | |
Task.configure(self, item) | |
self.sources:configure(item) | |
end | |
-- TODO: Add a custom "ouput" mixin | |
function PackTask:output(value) | |
assert.argType(value, "string", "output", 1) | |
if self.options.output then error("Cannot set output multiple times") end | |
self.options.output = value | |
self:Produces(value) | |
end | |
function PackTask:setup(context, runner) | |
Task.setup(self, context, runner) | |
if not self.options.startup then | |
context.logger:error("Task '%s': No startup file", self.name) | |
end | |
self:requires(self.options.startup) | |
if not self.options.output then | |
context.logger:error("Task '%s': No output file", self.name) | |
end | |
end | |
function PackTask:runAction(context) | |
local files = self.sources:gatherFiles(self.root) | |
local startup = self.options.startup | |
local output = self.options.output | |
local minify = self.options.minify | |
local resultFiles = {} | |
for _, file in pairs(files) do | |
context.logger:verbose("Including " .. file.relative) | |
resultFiles[file.name] = file.contents | |
end | |
local result = formatTemplate(template, { | |
files = dump.serialise(resultFiles), | |
startup = ("%q"):format(startup), | |
vfs = vfs, | |
}) | |
if minify then | |
result = rebuild.minifyString(result) | |
end | |
fs.write(fs.combine(context.root, output), result) | |
end | |
local PackExtensions = { } | |
function PackExtensions:pack(name, taskDepends) | |
return self:injectTask(PackTask(self.env, name, taskDepends)) | |
end | |
local function apply() | |
Runner:include(PackExtensions) | |
end | |
return { | |
name = "pack task", | |
description = "A task to combine multiple files into one which are then executed within a virtual file system.", | |
apply = apply, | |
PackTask = PackTask, | |
} | |
end | |
preload["howl.modules.tasks.minify"] = function(...) | |
--- Adds various tasks to minify files. | |
-- @module howl.modules.tasks.minify | |
local assert = require "howl.lib.assert" | |
local rebuild = require "howl.lexer.rebuild" | |
local Runner = require "howl.tasks.Runner" | |
local Task = require "howl.tasks.Task" | |
local minifyFile = rebuild.minifyFile | |
local minifyDiscard = function(self, env, i, o) | |
return minifyFile(env.root, i, o) | |
end | |
local MinifyTask = Task:subclass("howl.modules.minify.tasks.MinifyTask") | |
:addOptions { "input", "output" } | |
function MinifyTask:initialize(context, name, dependencies) | |
Task.initialize(self, name, dependencies) | |
self:description "Minify a file" | |
end | |
function MinifyTask:input(value) | |
assert.argType(value, "string", "input", 1) | |
if self.options.input then error("Cannot set input multiple times") end | |
self.options.input = value | |
self:requires(value) | |
end | |
function MinifyTask:output(value) | |
assert.argType(value, "string", "output", 1) | |
if self.options.output then error("Cannot set output multiple times") end | |
self.options.output = value | |
self:Produces(value) | |
end | |
function MinifyTask:setup(context, runner) | |
Task.setup(self, context, runner) | |
if not self.options.input then | |
context.logger:error("Task '%s': No input file specified", self.name) | |
end | |
if not self.options.output then | |
context.logger:error("Task '%s': No output file specified", self.name) | |
end | |
end | |
function MinifyTask:runAction(context) | |
local oldSize, newSize = minifyFile(context.root, self.options.input, self.options.output) | |
local percentDecreased = (oldSize - newSize) / oldSize * 100 | |
-- Ugly hack as length specifiers don't work on %f under LuaJ. | |
percentDecreased = math.floor(percentDecreased * 100) / 100 | |
context.logger:verbose(("%.20f%% decrease in file size"):format(percentDecreased)) | |
end | |
local MinifyExtensions = {} | |
function MinifyExtensions:minify(name, taskDepends) | |
return self:injectTask(MinifyTask(self.env, name, taskDepends)) | |
end | |
--- A task that minifies to a pattern instead | |
-- @tparam string name Name of the task | |
-- @tparam string inputPattern The pattern to read in | |
-- @tparam string outputPattern The pattern to produce | |
-- @treturn howl.tasks.Task The created task | |
function MinifyExtensions:addMinifier(name, inputPattern, outputPattern) | |
name = name or "_minify" | |
return self:addTask(name, {}, minifyDiscard) | |
:Description("Minifies files") | |
:Maps(inputPattern or "wild:*.lua", outputPattern or "wild:*.min.lua") | |
end | |
local function apply() | |
Runner:include(MinifyExtensions) | |
end | |
local function setup(context) | |
context.mediator:subscribe({ "HowlFile", "env" }, function(env) | |
env.minify = minifyFile | |
end) | |
end | |
return { | |
name = "minify task", | |
description = "Adds various tasks to minify files.", | |
apply = apply, | |
setup = setup, | |
} | |
end | |
preload["howl.modules.tasks.gist"] = function(...) | |
--- A task that uploads files to a Gist. | |
-- @module howl.modules.tasks.gist | |
local assert = require "howl.lib.assert" | |
local mixin = require "howl.class.mixin" | |
local settings = require "howl.lib.settings" | |
local json = require "howl.lib.json" | |
local platform = require "howl.platform" | |
local http = platform.http | |
local Buffer = require "howl.lib.Buffer" | |
local Task = require "howl.tasks.Task" | |
local Runner = require "howl.tasks.Runner" | |
local CopySource = require "howl.files.CopySource" | |
local GistTask = Task:subclass("howl.modules.tasks.gist.GistTask") | |
:include(mixin.filterable) | |
:include(mixin.delegate("sources", {"from", "include", "exclude"})) | |
:addOptions { "gist", "summary" } | |
function GistTask:initialize(context, name, dependencies) | |
Task.initialize(self, name, dependencies) | |
self.root = context.root | |
self.sources = CopySource() | |
self:exclude { ".git", ".svn", ".gitignore" } | |
self:description "Uploads files to a gist" | |
end | |
function GistTask:configure(item) | |
Task.configure(self, context, runner) | |
self.sources:configure(item) | |
end | |
function GistTask:setup(context, runner) | |
Task.setup(self, context, runner) | |
if not self.options.gist then | |
context.logger:error("Task '%s': No gist ID specified", self.name) | |
end | |
if not settings.githubKey then | |
context.logger:error("Task '%s': No GitHub API key specified. Goto https://github.com/settings/tokens/new to create one.", self.name) | |
end | |
end | |
function GistTask:runAction(context) | |
local files = self.sources:gatherFiles(self.root) | |
local gist = self.options.gist | |
local token = settings.githubKey | |
local out = {} | |
for _, file in pairs(files) do | |
context.logger:verbose("Including " .. file.relative) | |
out[file.name] = { content = file.contents } | |
end | |
local url = "https://api.github.com/gists/" .. gist .. "?access_token=" .. token | |
local headers = { Accept = "application/vnd.github.v3+json", ["X-HTTP-Method-Override"] = "PATCH" } | |
local data = json.encodePretty({ files = out, description = self.options.summary }) | |
local ok, handle, message = http.request(url, data, headers) | |
if not ok then | |
if handle then | |
context.logger:error(handle.readAll()) | |
end | |
error(result, 0) | |
end | |
end | |
local GistExtensions = { } | |
function GistExtensions:gist(name, taskDepends) | |
return self:injectTask(GistTask(self.env, name, taskDepends)) | |
end | |
local function apply() | |
Runner:include(GistExtensions) | |
end | |
return { | |
name = "gist task", | |
description = "A task that uploads files to a Gist.", | |
apply = apply, | |
GistTask = GistTask, | |
} | |
end | |
preload["howl.modules.tasks.clean"] = function(...) | |
--- A task that deletes all specified files | |
-- @module howl.modules.tasks.clean | |
local mixin = require "howl.class.mixin" | |
local fs = require "howl.platform".fs | |
local Task = require "howl.tasks.Task" | |
local Runner = require "howl.tasks.Runner" | |
local Source = require "howl.files.Source" | |
local CleanTask = Task:subclass("howl.modules.tasks.clean.CleanTask") | |
:include(mixin.configurable) | |
:include(mixin.filterable) | |
:include(mixin.delegate("sources", {"from", "include", "exclude"})) | |
function CleanTask:initialize(context, name, dependencies) | |
Task.initialize(self, name, dependencies) | |
self.root = context.root | |
self.sources = Source() | |
self:exclude { ".git", ".svn", ".gitignore" } | |
self:description "Deletes all files matching a pattern" | |
end | |
function CleanTask:configure(item) | |
self.sources:configure(item) | |
end | |
function CleanTask:setup(context, runner) | |
Task.setup(self, context, runner) | |
local root = self.sources | |
if root.allowEmpty and #root.includes == 0 then | |
-- Include the build directory if nothing is set | |
root:include(fs.combine(context.out, "*")) | |
end | |
end | |
function CleanTask:runAction(context) | |
for _, file in ipairs(self.sources:gatherFiles(self.root, true)) do | |
context.logger:verbose("Deleting " .. file.path) | |
fs.delete(file.path) | |
end | |
end | |
local CleanExtensions = {} | |
function CleanExtensions:clean(name, taskDepends) | |
return self:injectTask(CleanTask(self.env, name or "clean", taskDepends)) | |
end | |
local function apply() | |
Runner:include(CleanExtensions) | |
end | |
return { | |
name = "clean task", | |
description = "A task that deletes all specified files.", | |
apply = apply, | |
CleanTask = CleanTask, | |
} | |
end | |
preload["howl.modules.plugins"] = function(...) | |
--- A way of injecting plugins via the Howl DSL | |
-- @module howl.modules.plugins | |
local class = require "howl.class" | |
local mixin = require "howl.class.mixin" | |
local fs = require "howl.platform".fs | |
local Plugins = class("howl.modules.plugins") | |
:include(mixin.configurable) | |
function Plugins:initialize(context) | |
self.context = context | |
end | |
function Plugins:configure(data) | |
if #data == 0 then | |
self:addPlugin(data, data) | |
else | |
for i = 1, #data do | |
self:addPlugin(data[i]) | |
end | |
end | |
end | |
local function toModule(root, file) | |
local name = file:gsub("%.lua$", ""):gsub("/", "."):gsub("^(.*)%.init$", "%1") | |
if name == "" or name == "init" then | |
return root | |
else | |
return root .. "." .. name | |
end | |
end | |
function Plugins:addPlugin(data) | |
if not data.type then error("No plugin type specified") end | |
local type = data.type | |
data.type = nil | |
local file | |
if data.file then | |
file = data.file | |
data.file = nil | |
end | |
local manager = self.context.packageManager | |
local package = manager:addPackage(type, data) | |
self.context.logger:verbose("Using plugin from package " .. package:getName()) | |
local fetchedFiles = package:require(file and {file}) | |
local root = "external." .. package:getName() | |
local count = 0 | |
for file, loc in pairs(fetchedFiles) do | |
if file:find("%.lua$") then | |
count = count + 1 | |
local func, msg = loadfile(fetchedFiles[file], _ENV) | |
if func then | |
local name = toModule(root, file) | |
preload[name] = func | |
self.context.logger:verbose("Including plugin file " .. file .. " as " .. name) | |
else | |
self.context.logger:warning("Cannot load plugin file " .. file .. ": " .. msg) | |
end | |
end | |
end | |
if not file then | |
if fetchedFiles["init.lua"] then | |
file = "init.lua" | |
elseif count == 1 then | |
file = next(fetchedFiles) | |
elseif count == 0 then | |
self.context.logger:error(package:getName() .. " does not export any files") | |
error("Error adding plugin") | |
else | |
self.context.logger:error("Cannot guess a file for " .. package:getName()) | |
error("Error adding plugin") | |
end | |
end | |
self.context.logger:verbose("Using package " .. package:getName() .. " with " .. file) | |
local name = toModule(root, file) | |
if not preload[name] then | |
self.context.logger:error("Cannot load plugin as " .. name .. " could not be loaded") | |
error("Error adding plugin") | |
end | |
self.context:include(require(name)) | |
return self | |
end | |
return { | |
name = "plugins", | |
description = "Inject plugins into Howl at runtime.", | |
setup = function(context) | |
context.mediator:subscribe({ "HowlFile", "env" }, function(env) | |
env.plugins = Plugins(context) | |
end) | |
end | |
} | |
end | |
preload["howl.modules.packages.pastebin"] = function(...) | |
--- A package provider that installs pastebins. | |
-- @module howl.modules.packages.pastebin | |
local class = require "howl.class" | |
local platform = require "howl.platform" | |
local Manager = require "howl.packages.Manager" | |
local Package = require "howl.packages.Package" | |
local PastebinPackage = Package:subclass("howl.modules.packages.pastebin.PastebinPackage") | |
:addOptions { "id" } | |
--- Setup the dependency, checking if it cannot be resolved | |
function PastebinPackage:setup(runner) | |
if not self.options.id then | |
self.context.logger:error("Pastebin has no ID") | |
end | |
end | |
function PastebinPackage:getName() | |
return self.options.id | |
end | |
function PastebinPackage:files(previous) | |
if previous then | |
return {} | |
else | |
return { ["init.lua"] = platform.fs.combine(self.installDir, "init.lua") } | |
end | |
end | |
function PastebinPackage:require(previous, refresh) | |
local id = self.options.id | |
local dir = self.installDir | |
if not refresh and previous then | |
return previous | |
end | |
local success, request = platform.http.request("http://pastebin.com/raw/" .. id) | |
if not success or not request then | |
self.context.logger:error("Cannot find pastebin " .. id) | |
return previous | |
end | |
local contents = request.readAll() | |
request.close() | |
platform.fs.write(platform.fs.combine(dir, "init.lua"), contents) | |
return { } | |
end | |
return { | |
name = "pastebin package", | |
description = "Allows downloading a pastebin dependency.", | |
apply = function() | |
Manager:addProvider(PastebinPackage, "pastebin") | |
end, | |
PastebinPackage = PastebinPackage, | |
} | |
end | |
preload["howl.modules.packages.gist"] = function(...) | |
--- A package provider that installs gists. | |
-- @module howl.modules.packages.gist | |
local class = require "howl.class" | |
local json = require "howl.lib.json" | |
local platform = require "howl.platform" | |
local Manager = require "howl.packages.Manager" | |
local Package = require "howl.packages.Package" | |
local GistPackage = Package:subclass("howl.modules.packages.gist.GistPackage") | |
:addOptions { "id" } | |
--- Setup the dependency, checking if it cannot be resolved | |
function GistPackage:setup(runner) | |
if not self.options.id then | |
self.context.logger:error("Gist has no ID") | |
end | |
end | |
function GistPackage:getName() | |
return self.options.id | |
end | |
function GistPackage:files(previous) | |
if previous then | |
local files = {} | |
for k, _ in pairs(previous.files) do | |
files[k] = platform.fs.combine(self.installDir, k) | |
end | |
return files | |
else | |
return {} | |
end | |
end | |
function GistPackage:require(previous, refresh) | |
local id = self.options.id | |
local dir = self.installDir | |
if not refresh and previous then | |
return previous | |
end | |
-- TODO: Fetch gists/:id/commits [1].version first if we have a hash | |
-- TODO: Worth storing individual versions? | |
local success, request = platform.http.request("https://api.github.com/gists/" .. id) | |
if not success or not request then | |
self.context.logger:error("Cannot find gist " .. id) | |
return false | |
end | |
local contents = request.readAll() | |
request.close() | |
local data = json.decode(contents) | |
local hash = data.history[1].version | |
local current | |
if previous and hash == previous.hash then | |
current = previous | |
else | |
current = { hash = hash, files = {} } | |
for path, file in pairs(data.files) do | |
if file.truncated then | |
self.context.logger:error("Skipping " .. path .. " as it is truncated") | |
else | |
platform.fs.write(platform.fs.combine(dir, path), file.content) | |
current.files[path] = true | |
end | |
end | |
end | |
return current | |
end | |
return { | |
name = "gist package", | |
description = "Allows downloading a gist dependency.", | |
apply = function() | |
Manager:addProvider(GistPackage, "gist") | |
end, | |
GistPackage = GistPackage, | |
} | |
end | |
preload["howl.modules.packages.file"] = function(...) | |
--- A package provider that uses a local file. | |
-- @module howl.modules.packages.file | |
local class = require "howl.class" | |
local mixin = require "howl.class.mixin" | |
local fs = require "howl.platform".fs | |
local Manager = require "howl.packages.Manager" | |
local Package = require "howl.packages.Package" | |
local Source = require "howl.files.Source" | |
local FilePackage = Package:subclass("howl.modules.packages.file.FilePackage") | |
:include(mixin.filterable) | |
:include(mixin.delegate("sources", {"from", "include", "exclude"})) | |
function FilePackage:initialize(context, root) | |
Package.initialize(self, context, root) | |
self.sources = Source(false) | |
self.name = tostring({}):sub(8) | |
self:exclude { ".git", ".svn", ".gitignore", context.out } | |
end | |
--- Setup the dependency, checking if it cannot be resolved | |
function FilePackage:setup(runner) | |
if not self.sources:hasFiles() then | |
self.context.logger:error("No files specified") | |
end | |
end | |
function FilePackage:configure(item) | |
Package.configure(self, item) | |
self.sources:configure(item) | |
end | |
function FilePackage:getName() | |
return self.name | |
end | |
function FilePackage:files(previous) | |
local files = {} | |
for _, v in pairs(self.sources:gatherFiles(self.context.root)) do | |
files[v.name] = v.path | |
end | |
return files | |
end | |
function FilePackage:require(previous, refresh) | |
end | |
return { | |
name = "file package", | |
description = "Allows using a local file as a dependency", | |
apply = function() | |
Manager:addProvider(FilePackage, "file") | |
end, | |
FilePackage = FilePackage, | |
} | |
end | |
preload["howl.modules.list"] = function(...) | |
--- Lists all tasks on a runner. | |
-- @module howl.modules.list | |
local assert = require "howl.lib.assert" | |
local colored = require "howl.lib.colored" | |
local Runner = require "howl.tasks.Runner" | |
local ListTasksExtensions = { } | |
function ListTasksExtensions:listTasks(indent, all) | |
local taskNames = {} | |
local maxLength = 0 | |
for name, task in pairs(self.tasks) do | |
local start = name:sub(1, 1) | |
if all or (start ~= "_" and start ~= ".") then | |
local description = task.options.description or "" | |
local length = #name | |
if length > maxLength then | |
maxLength = length | |
end | |
taskNames[name] = description | |
end | |
end | |
maxLength = maxLength + 2 | |
indent = indent or "" | |
for name, description in pairs(taskNames) do | |
colored.writeColor("white", indent .. name) | |
colored.printColor("lightGray", string.rep(" ", maxLength - #name) .. description) | |
end | |
return self | |
end | |
local function apply() | |
Runner:include(ListTasksExtensions) | |
end | |
return { | |
name = "list", | |
description = "List all tasks on a runner.", | |
apply = apply, | |
} | |
end | |
preload["howl.modules.dependencies.task"] = function(...) | |
--- Allows depending on a task. | |
-- @module howl.modules.dependencies.task | |
local assert = require "howl.lib.assert" | |
local Task = require "howl.tasks.Task" | |
local Dependency = require "howl.tasks.Dependency" | |
local TaskDependency = Dependency:subclass("howl.modules.dependencies.task.TaskDependency") | |
--- Create a new task dependency | |
function TaskDependency:initialize(task, name) | |
Dependency.initialize(self, task) | |
assert.argType(name, "string", "initialize", 1) | |
self.name = name | |
end | |
function TaskDependency:setup(context, runner) | |
if not runner.tasks[self.name] then | |
context.logger:error("Task '%s': cannot resolve dependency '%s'", self.task.name, self.name) | |
end | |
end | |
function TaskDependency:resolve(context, runner) | |
return runner:run(self.name) | |
end | |
return { | |
name = "task dependency", | |
description = "Allows depending on a task.", | |
apply = function() | |
Task:addDependency(TaskDependency, "depends") | |
end, | |
TaskDependency = TaskDependency, | |
} | |
end | |
preload["howl.modules.dependencies.file"] = function(...) | |
--- Allows depending on a file. | |
-- @module howl.modules.dependencies.file | |
local assert = require "howl.lib.assert" | |
local Task = require "howl.tasks.Task" | |
local Dependency = require "howl.tasks.Dependency" | |
local FileDependency = Dependency:subclass("howl.modules.dependencies.file.FileDependency") | |
--- Create a new task dependency | |
function FileDependency:initialize(task, path) | |
Dependency.initialize(self, task) | |
assert.argType(path, "string", "initialize", 1) | |
self.path = path | |
end | |
function FileDependency:setup(context, runner) | |
-- TODO: Check that this can be resolved | |
end | |
function FileDependency:resolve(context, runner) | |
return runner:DoRequire(self.path) | |
end | |
return { | |
name = "file dependency", | |
description = "Allows depending on a file.", | |
apply = function() | |
Task:addDependency(FileDependency, "requires") | |
end, | |
FileDependency = FileDependency, | |
} | |
end | |
preload["howl.loader"] = function(...) | |
--- Handles loading and creation of HowlFiles | |
-- @module howl.loader | |
local fs = require "howl.platform".fs | |
local Runner = require "howl.tasks.Runner" | |
local Utils = require "howl.lib.utils" | |
--- Names to test when searching for Howlfiles | |
local Names = { "Howlfile", "Howlfile.lua" } | |
--- Finds the howl file | |
-- @treturn string The name of the howl file or nil if not found | |
-- @treturn string The path of the howl file or the error message if not found | |
local function FindHowl() | |
local currentDirectory = fs.currentDir() | |
while true do | |
for _, file in ipairs(Names) do | |
local howlFile = fs.combine(currentDirectory, file) | |
if fs.exists(howlFile) and not fs.isDir(howlFile) then | |
return file, currentDirectory | |
end | |
end | |
if currentDirectory == "/" or currentDirectory == "" then | |
break | |
end | |
currentDirectory = fs.getDir(currentDirectory) | |
end | |
return nil, "Cannot find HowlFile. Looking for '" .. table.concat(Names, "', '") .. "'." | |
end | |
--- Create an environment for running howl files | |
-- @tparam table variables A list of variables to include in the environment | |
-- @treturn table The created environment | |
local function SetupEnvironment(variables) | |
local env = setmetatable(variables or {}, { __index = _ENV }) | |
function env.loadfile(path) | |
return assert(loadfile(path, env)) | |
end | |
function env.dofile(path) | |
return env.loadfile(path)() | |
end | |
return env | |
end | |
--- Setup tasks | |
-- @tparam howl.Context context The current environment | |
-- @tparam string howlFile location of Howlfile relative to current directory | |
-- @treturn Runner The task runner | |
local function SetupTasks(context, howlFile) | |
local tasks = Runner(context) | |
context.mediator:subscribe({ "ArgParse", "changed" }, function(options) | |
tasks.ShowTime = options:Get "time" | |
tasks.Traceback = options:Get "trace" | |
end) | |
-- Setup an environment | |
local environment = SetupEnvironment({ | |
-- Core globals | |
require = require, | |
CurrentDirectory = context.root, | |
Tasks = tasks, | |
Options = context.arguments, | |
-- Helper functions | |
Verbose = context.logger/"verbose", | |
Log = context.logger/"dump", | |
File = function(...) return fs.combine(context.root, ...) end, | |
}) | |
context.mediator:publish({ "HowlFile", "env" }, environment, context) | |
return tasks, environment | |
end | |
--- @export | |
return { | |
FindHowl = FindHowl, | |
SetupEnvironment = SetupEnvironment, | |
SetupTasks = SetupTasks, | |
Names = Names, | |
} | |
end | |
preload["howl.lib.utils"] = function(...) | |
--- Useful little helpers for things | |
-- @module howl.lib.utils | |
local assert = require "howl.lib.assert" | |
local matches = { | |
["^"] = "%^", | |
["$"] = "%$", | |
["("] = "%(", | |
[")"] = "%)", | |
["%"] = "%%", | |
["."] = "%.", | |
["["] = "%[", | |
["]"] = "%]", | |
["*"] = "%*", | |
["+"] = "%+", | |
["-"] = "%-", | |
["?"] = "%?", | |
["\0"] = "%z", | |
} | |
--- Escape a string for using in a pattern | |
-- @tparam string pattern The string to escape | |
-- @treturn string The escaped pattern | |
local function escapePattern(pattern) | |
return (pattern:gsub(".", matches)) | |
end | |
local basicMatches = { | |
["^"] = "%^", | |
["$"] = "%$", | |
["("] = "%(", | |
[")"] = "%)", | |
["%"] = "%%", | |
["."] = "%.", | |
["["] = "%[", | |
["]"] = "%]", | |
["+"] = "%+", | |
["-"] = "%-", | |
["?"] = "%?", | |
["\0"] = "%z", | |
} | |
--- A resulting pattern | |
-- @table Pattern | |
-- @tfield string Type `Pattern` or `Normal` | |
-- @tfield string Text The resulting pattern | |
--- Parse a series of patterns | |
-- @tparam string text Pattern to parse | |
-- @tparam boolean invert If using a wildcard, invert it | |
-- @treturn Pattern | |
local function parsePattern(text, invert) | |
local beginning = text:sub(1, 5) | |
if beginning == "ptrn:" or beginning == "wild:" then | |
local text = text:sub(6) | |
if beginning == "wild:" then | |
if invert then | |
local counter = 0 | |
-- Escape the pattern and then replace wildcards with the results of the capture %1, %2, etc... | |
text = ((text:gsub(".", basicMatches)):gsub("(%*)", function() | |
counter = counter + 1 | |
return "%" .. counter | |
end)) | |
else | |
-- Escape the pattern and replace wildcards with (.*) capture | |
text = "^" .. ((text:gsub(".", basicMatches)):gsub("(%*)", "(.*)")) .. "$" | |
end | |
end | |
return { Type = "Pattern", Text = text } | |
else | |
return { Type = "Normal", Text = text } | |
end | |
end | |
--- Create a lookup table from a list of values | |
-- @tparam table tbl The table of values | |
-- @treturn The same table, with lookups as well | |
local function createLookup(tbl) | |
for _, v in ipairs(tbl) do | |
tbl[v] = true | |
end | |
return tbl | |
end | |
--- Checks if two tables are equal | |
-- @tparam table a | |
-- @tparam table b | |
-- @treturn boolean If they match | |
local function matchTables(a, b) | |
local length = #a | |
if length ~= #b then return false end | |
for i = 1, length do | |
if a[i] ~= b[i] then return false end | |
end | |
return true | |
end | |
local function startsWith(string, text) | |
if string:sub(1, #text) == text then | |
return string:sub(#text + 1) | |
else | |
return false | |
end | |
end | |
--- Format a template string with data. | |
-- Anything of the form `${var}` will be replaced with the appropriate variable in the table. | |
-- @tparam string template The template to format | |
-- @tparam table data The data to replace with | |
-- @treturn string The formatted template | |
local function formatTemplate(template, data) | |
return (template:gsub("${([^}]+)}", function(str) | |
local res = data[str] | |
if res == nil then | |
return "${" .. str .. "}" | |
else | |
return tostring(res) | |
end | |
end)) | |
end | |
--- Mark a function as deprecated | |
-- @tparam string name The name of the function | |
-- @tparam function function The function to delegate to | |
-- @tparam string|nil msg Additional message to print | |
local function deprecated(name, func, msg) | |
assert.argType(name, "string", "deprecated", 1) | |
assert.argType(func, "function", "deprecated", 2) | |
if msg ~= nil then | |
assert.argType(msg, "string", "msg", 4) | |
msg = " " .. msg | |
else | |
msg = "" | |
end | |
local doneDeprc = false | |
return function(...) | |
if not doneDeprc then | |
local _, callee = pcall(error, "", 3) | |
callee = callee:gsub(":%s*$", "") | |
print(name .. " is deprecated (called at " .. callee .. ")." .. msg) | |
doneDeprc = true | |
end | |
return func(...) | |
end | |
end | |
--- @export | |
return { | |
escapePattern = escapePattern, | |
parsePattern = parsePattern, | |
createLookup = createLookup, | |
matchTables = matchTables, | |
startsWith = startsWith, | |
formatTemplate = formatTemplate, | |
deprecated = deprecated, | |
} | |
end | |
preload["howl.lib.settings"] = function(...) | |
local platform = require "howl.platform" | |
local fs = platform.fs | |
local dump = require "howl.lib.dump" | |
local currentSettings = { | |
} | |
if fs.exists(".howl.settings.lua") then | |
local contents = fs.read(".howl.settings.lua") | |
for k, v in pairs(dump.unserialise(contents)) do | |
currentSettings[k] = v | |
end | |
end | |
if fs.exists(".howl/settings.lua") then | |
local contents = fs.read(".howl/settings.lua") | |
for k, v in pairs(dump.unserialise(contents)) do | |
currentSettings[k] = v | |
end | |
end | |
-- Things have to be defined in currentSettings for this to work. We need to improve this. | |
for k, v in pairs(currentSettings) do | |
currentSettings[k] = platform.os.getEnv("howl." .. k, v) | |
end | |
return currentSettings | |
end | |
preload["howl.lib.mediator"] = function(...) | |
--- Mediator pattern implementation for pub-sub management | |
-- | |
-- [Adapted from Olivine Labs' Mediator](http://olivinelabs.com/mediator_lua/) | |
-- @module howl.lib.mediator | |
local class = require "howl.class" | |
local mixin = require "howl.class.mixin" | |
local function getUniqueId() | |
return tonumber(tostring({}):match(':%s*[0xX]*(%x+)'), 16) | |
end | |
--- A subscriber to a channel | |
-- @type Subscriber | |
local Subscriber = class("howl.lib.mediator.Subscriber"):include(mixin.sealed) | |
--- Create a new subscriber | |
-- @tparam function fn The function to execute | |
-- @tparam table options Options to use | |
-- @constructor | |
function Subscriber:initialize(fn, options) | |
self.id = getUniqueId() | |
self.options = options or {} | |
self.fn = fn | |
end | |
--- Update the subscriber with new options | |
-- @tparam table options Options to use | |
function Subscriber:update(options) | |
self.fn = options.fn or self.fn | |
self.options = options.options or self.options | |
end | |
--- Channel class and functions | |
-- @type Channel | |
local Channel = class("howl.lib.mediator.Channel"):include(mixin.sealed) | |
function Channel:initialize(namespace, parent) | |
self.stopped = false | |
self.namespace = namespace | |
self.callbacks = {} | |
self.channels = {} | |
self.parent = parent | |
end | |
function Channel:addSubscriber(fn, options) | |
local callback = Subscriber(fn, options) | |
local priority = (#self.callbacks + 1) | |
options = options or {} | |
if options.priority and | |
options.priority >= 0 and | |
options.priority < priority | |
then | |
priority = options.priority | |
end | |
table.insert(self.callbacks, priority, callback) | |
return callback | |
end | |
function Channel:getSubscriber(id) | |
for i = 1, #self.callbacks do | |
local callback = self.callbacks[i] | |
if callback.id == id then return { index = i, value = callback } end | |
end | |
local sub | |
for _, channel in pairs(self.channels) do | |
sub = channel:getSubscriber(id) | |
if sub then break end | |
end | |
return sub | |
end | |
function Channel:setPriority(id, priority) | |
local callback = self:getSubscriber(id) | |
if callback.value then | |
table.remove(self.callbacks, callback.index) | |
table.insert(self.callbacks, priority, callback.value) | |
end | |
end | |
function Channel:addChannel(namespace) | |
self.channels[namespace] = Channel(namespace, self) | |
return self.channels[namespace] | |
end | |
function Channel:hasChannel(namespace) | |
return namespace and self.channels[namespace] and true | |
end | |
function Channel:getChannel(namespace) | |
return self.channels[namespace] or self:addChannel(namespace) | |
end | |
function Channel:removeSubscriber(id) | |
local callback = self:getSubscriber(id) | |
if callback and callback.value then | |
for _, channel in pairs(self.channels) do | |
channel:removeSubscriber(id) | |
end | |
return table.remove(self.callbacks, callback.index) | |
end | |
end | |
function Channel:publish(result, ...) | |
for i = 1, #self.callbacks do | |
local callback = self.callbacks[i] | |
-- if it doesn't have a predicate, or it does and it's true then run it | |
if not callback.options.predicate or callback.options.predicate(...) then | |
-- just take the first result and insert it into the result table | |
local continue, value = callback.fn(...) | |
if value ~= nil then table.insert(result, value) end | |
if continue == false then return false, result end | |
end | |
end | |
if self.parent then | |
return self.parent:publish(result, ...) | |
else | |
return true, result | |
end | |
end | |
--- Mediator class and functions | |
local Mediator = setmetatable( | |
{ | |
Channel = Channel, | |
Subscriber = Subscriber | |
}, | |
{ | |
__call = function(fn, options) | |
return { | |
channel = Channel('root'), | |
getChannel = function(self, channelNamespace) | |
local channel = self.channel | |
for i=1, #channelNamespace do | |
channel = channel:getChannel(channelNamespace[i]) | |
end | |
return channel | |
end, | |
subscribe = function(self, channelNamespace, fn, options) | |
return self:getChannel(channelNamespace):addSubscriber(fn, options) | |
end, | |
getSubscriber = function(self, id, channelNamespace) | |
return self:getChannel(channelNamespace):getSubscriber(id) | |
end, | |
removeSubscriber = function(self, id, channelNamespace) | |
return self:getChannel(channelNamespace):removeSubscriber(id) | |
end, | |
publish = function(self, channelNamespace, ...) | |
return self:getChannel(channelNamespace):publish({}, ...) | |
end | |
} | |
end | |
} | |
) | |
return Mediator() | |
end | |
preload["howl.lib.Logger"] = function(...) | |
--- The main logger for Lua | |
-- @classmod howl.lib.Logger | |
local class = require "howl.class" | |
local mixin = require "howl.class.mixin" | |
local dump = require "howl.lib.dump".dump | |
local colored = require "howl.lib.colored" | |
local platformLog = require "howl.platform".log | |
local select, tostring = select, tostring | |
local function concat(...) | |
local buffer = {} | |
for i = 1, select('#', ...) do | |
buffer[i] = tostring(select(i, ...)) | |
end | |
return table.concat(buffer, " ") | |
end | |
local Logger = class("howl.lib.Logger") | |
:include(mixin.sealed) | |
:include(mixin.curry) | |
function Logger:initialize(context) | |
self.isVerbose = false | |
context.mediator:subscribe({ "ArgParse", "changed" }, function(options) | |
self.isVerbose = options:Get "verbose" or false | |
end) | |
end | |
--- Print a series of objects if verbose mode is enabled | |
function Logger:verbose(...) | |
if self.isVerbose then | |
local _, m = pcall(function() error("", 4) end) | |
colored.writeColor("gray", m) | |
colored.printColor("lightGray", ...) | |
platformLog("verbose", m .. concat(...)) | |
end | |
end | |
--- Dump a series of objects if verbose mode is enabled | |
function Logger:dump(...) | |
if self.isVerbose then | |
local _, m = pcall(function() error("", 4) end) | |
colored.writeColor("gray", m) | |
local len = select('#', ...) | |
local args = {...} | |
for i = 1, len do | |
local value = args[i] | |
local t = type(value) | |
if t == "table" then | |
value = dump(value) | |
else | |
value = tostring(value) | |
end | |
if i > 1 then value = " " .. value end | |
-- TODO: use platformLog too. | |
colored.writeColor("lightGray", value) | |
end | |
print() | |
end | |
end | |
local types = { | |
{ "success", "ok", "green" }, | |
{ "error", "error", "red" }, | |
{ "info", "info", "cyan" }, | |
{ "warning", "warn", "yellow" }, | |
} | |
local max = 0 | |
for _, v in ipairs(types) do | |
max = math.max(max, #v[2]) | |
end | |
for _, v in ipairs(types) do | |
local color = v[3] | |
local format = '[' .. v[2] .. ']' .. (' '):rep(max - #v[2] + 1) | |
local field = "has" .. v[2]:gsub("^%l", string.upper) | |
local name = v[1] | |
Logger[name] = function(self, fmt, ...) | |
self[field] = true | |
colored.writeColor(color, format) | |
local text | |
if type(fmt) == "string" then | |
text = fmt:format(...) | |
else | |
end | |
colored.printColor(color, text) | |
platformLog(name, text) | |
end | |
end | |
return Logger | |
end | |
preload["howl.lib.json"] = function(...) | |
local controls = {["\n"]="\\n", ["\r"]="\\r", ["\t"]="\\t", ["\b"]="\\b", ["\f"]="\\f", ["\""]="\\\"", ["\\"]="\\\\"} | |
local function isArray(t) | |
local max = 0 | |
for k,v in pairs(t) do | |
if type(k) ~= "number" then | |
return false | |
elseif k > max then | |
max = k | |
end | |
end | |
return max == #t | |
end | |
local function encodeCommon(val, pretty, tabLevel, tTracking, ctx) | |
local str = "" | |
-- Tabbing util | |
local function tab(s) | |
str = str .. ("\t"):rep(tabLevel) .. s | |
end | |
local function arrEncoding(val, bracket, closeBracket, iterator, loopFunc) | |
str = str .. bracket | |
if pretty then | |
str = str .. "\n" | |
tabLevel = tabLevel + 1 | |
end | |
for k,v in iterator(val) do | |
tab("") | |
loopFunc(k,v) | |
str = str .. "," | |
if pretty then str = str .. "\n" end | |
end | |
if pretty then | |
tabLevel = tabLevel - 1 | |
end | |
if str:sub(-2) == ",\n" then | |
str = str:sub(1, -3) .. "\n" | |
elseif str:sub(-1) == "," then | |
str = str:sub(1, -2) | |
end | |
tab(closeBracket) | |
end | |
-- Table encoding | |
if type(val) == "table" then | |
assert(not tTracking[val], "Cannot encode a table holding itself recursively") | |
tTracking[val] = true | |
if isArray(val) then | |
arrEncoding(val, "[", "]", ipairs, function(k,v) | |
str = str .. encodeCommon(v, pretty, tabLevel, tTracking) | |
end) | |
else | |
arrEncoding(val, "{", "}", pairs, function(k,v) | |
assert(type(k) == "string", "JSON object keys must be strings", 2) | |
str = str .. encodeCommon(k, pretty, tabLevel, tTracking) | |
str = str .. (pretty and ": " or ":") .. encodeCommon(v, pretty, tabLevel, tTracking, k) | |
end) | |
end | |
-- String encoding | |
elseif type(val) == "string" then | |
str = '"' .. val:gsub("[%c\"\\]", controls) .. '"' | |
-- Number encoding | |
elseif type(val) == "number" or type(val) == "boolean" then | |
str = tostring(val) | |
else | |
error("JSON only supports arrays, objects, numbers, booleans, and strings, got " .. type(val) .. " in " .. tostring(ctx), 2) | |
end | |
return str | |
end | |
local function encode(val) | |
return encodeCommon(val, false, 0, {}) | |
end | |
local function encodePretty(val) | |
return encodeCommon(val, true, 0, {}) | |
end | |
-- Decoding | |
local whites = {['\n']=true; ['\r']=true; ['\t']=true; [' ']=true; [',']=true; [':']=true} | |
local function removeWhite(str) | |
while whites[str:sub(1, 1)] do | |
str = str:sub(2) | |
end | |
return str | |
end | |
local decodeControls = {} | |
for k,v in pairs(controls) do | |
decodeControls[v] = k | |
end | |
local function parseBoolean(str) | |
if str:sub(1, 4) == "true" then | |
return true, removeWhite(str:sub(5)) | |
else | |
return false, removeWhite(str:sub(6)) | |
end | |
end | |
local function parseNull(str) | |
return nil, removeWhite(str:sub(5)) | |
end | |
local numChars = {['e']=true; ['E']=true; ['+']=true; ['-']=true; ['.']=true} | |
local function parseNumber(str) | |
local i = 1 | |
while numChars[str:sub(i, i)] or tonumber(str:sub(i, i)) do | |
i = i + 1 | |
end | |
local val = tonumber(str:sub(1, i - 1)) | |
str = removeWhite(str:sub(i)) | |
return val, str | |
end | |
local function parseString(str) | |
str = str:sub(2) | |
local s = "" | |
while str:sub(1,1) ~= "\"" do | |
local next = str:sub(1,1) | |
str = str:sub(2) | |
assert(next ~= "\n", "Unclosed string") | |
if next == "\\" then | |
local escape = str:sub(1,1) | |
str = str:sub(2) | |
next = assert(decodeControls[next..escape], "Invalid escape character") | |
end | |
s = s .. next | |
end | |
return s, removeWhite(str:sub(2)) | |
end | |
local parseValue | |
local function parseArray(str) | |
str = removeWhite(str:sub(2)) | |
local val = {} | |
local i = 1 | |
while str:sub(1, 1) ~= "]" do | |
local v = nil | |
v, str = parseValue(str) | |
val[i] = v | |
i = i + 1 | |
str = removeWhite(str) | |
end | |
str = removeWhite(str:sub(2)) | |
return val, str | |
end | |
local function parseMember(str) | |
local k = nil | |
k, str = parseValue(str) | |
local val = nil | |
val, str = parseValue(str) | |
return k, val, str | |
end | |
local function parseObject(str) | |
str = removeWhite(str:sub(2)) | |
local val = {} | |
while str:sub(1, 1) ~= "}" do | |
local k, v = nil, nil | |
k, v, str = parseMember(str) | |
val[k] = v | |
str = removeWhite(str) | |
end | |
str = removeWhite(str:sub(2)) | |
return val, str | |
end | |
function parseValue(str) | |
local fchar = str:sub(1, 1) | |
if fchar == "{" then | |
return parseObject(str) | |
elseif fchar == "[" then | |
return parseArray(str) | |
elseif tonumber(fchar) ~= nil or numChars[fchar] then | |
return parseNumber(str) | |
elseif str:sub(1, 4) == "true" or str:sub(1, 5) == "false" then | |
return parseBoolean(str) | |
elseif fchar == "\"" then | |
return parseString(str) | |
elseif str:sub(1, 4) == "null" then | |
return parseNull(str) | |
end | |
return nil | |
end | |
local function decode(str) | |
str = removeWhite(str) | |
return parseValue(str) | |
end | |
return { | |
encode = encode, | |
encodePretty = encodePretty, | |
decode = decode, | |
} | |
end | |
preload["howl.lib.dump"] = function(...) | |
--- Allows formatting tables for logging and storing | |
-- @module howl.lib.dump | |
local Buffer = require("howl.lib.Buffer") | |
local createLookup = require("howl.lib.utils").createLookup | |
local type, tostring, format = type, tostring, string.format | |
local getmetatable, error = getmetatable, error | |
-- TODO: Switch to LuaCP's pprint | |
local function dumpImpl(t, tracking, indent, tupleLength) | |
local objType = type(t) | |
if objType == "table" and not tracking[t] then | |
tracking[t] = true | |
if next(t) == nil then | |
return "{}" | |
else | |
local shouldNewLine = false | |
local length = #t | |
local builder = 0 | |
for k,v in pairs(t) do | |
if type(k) == "table" or type(v) == "table" then | |
shouldNewLine = true | |
break | |
elseif type(k) == "number" and k >= 1 and k <= length and k % 1 == 0 then | |
builder = builder + #tostring(v) + 2 | |
else | |
builder = builder + #tostring(v) + #tostring(k) + 2 | |
end | |
if builder > 40 then | |
shouldNewLine = true | |
break | |
end | |
end | |
local newLine, nextNewLine, subIndent = "", ", ", " " | |
if shouldNewLine then | |
newLine = "\n" | |
nextNewLine = ",\n" | |
subIndent = indent .. " " | |
end | |
local result, n = {(tupleLength and "(" or "{") .. newLine}, 1 | |
local seen = {} | |
local first = true | |
for k = 1, length do | |
seen[k] = true | |
n = n + 1 | |
local entry = subIndent .. dumpImpl(t[k], tracking, subIndent) | |
if not first then | |
entry = nextNewLine .. entry | |
else | |
first = false | |
end | |
result[n] = entry | |
end | |
for k,v in pairs(t) do | |
if not seen[k] then | |
local entry | |
if type(k) == "string" and string.match( k, "^[%a_][%a%d_]*$" ) then | |
entry = k .. " = " .. dumpImpl(v, tracking, subIndent) | |
else | |
entry = "[" .. dumpImpl(k, tracking, subIndent) .. "] = " .. dumpImpl(v, tracking, subIndent) | |
end | |
entry = subIndent .. entry | |
if not first then | |
entry = nextNewLine .. entry | |
else | |
first = false | |
end | |
n = n + 1 | |
result[n] = entry | |
end | |
end | |
n = n + 1 | |
result[n] = newLine .. indent .. (tupleLength and ")" or "}") | |
return table.concat(result) | |
end | |
elseif objType == "string" then | |
return (string.format("%q", t):gsub("\\\n", "\\n")) | |
else | |
return tostring(t) | |
end | |
end | |
local function dump(t, n) | |
return dumpImpl(t, {}, "", n) | |
end | |
local keywords = createLookup { | |
"and", "break", "do", "else", "elseif", "end", "false", | |
"for", "function", "if", "in", "local", "nil", "not", "or", | |
"repeat", "return", "then", "true", "until", "while", | |
} | |
--- Internal serialiser | |
-- @param object The object being serialised | |
-- @tparam table tracking List of items being tracked | |
-- @tparam Buffer buffer Buffer to append to | |
-- @treturn Buffer The buffer passed | |
local function internalSerialise(object, tracking, buffer) | |
local sType = type(object) | |
if sType == "table" then | |
if tracking[object] then | |
error("Cannot serialise table with recursive entries", 1) | |
end | |
tracking[object] = true | |
if next(object) == nil then | |
buffer:append("{}") | |
else | |
-- Other tables take more work | |
buffer:append("{") | |
local seen = {} | |
-- Attempt array only method | |
for k, v in ipairs(object) do | |
seen[k] = true | |
internalSerialise(v, tracking, buffer) | |
buffer:append(",") | |
end | |
for k, v in pairs(object) do | |
if not seen[k] then | |
if type(k) == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then | |
buffer:append(k .. "=") | |
else | |
buffer:append("[") | |
internalSerialise(k, tracking, buffer) | |
buffer:append("]=") | |
end | |
internalSerialise(v, tracking, buffer) | |
buffer:append(",") | |
end | |
end | |
buffer:append("}") | |
end | |
elseif sType == "string" then | |
buffer:append(format("%q", object)) | |
elseif sType == "number" or sType == "boolean" or sType == "nil" then | |
buffer:append(tostring(object)) | |
else | |
error("Cannot serialise type " .. sType) | |
end | |
return buffer | |
end | |
--- Used for serialising a data structure. | |
-- | |
-- This does not handle recursive structures or functions. | |
-- @param object The object to dump | |
-- @treturn string The serialised string | |
local function serialise(object) | |
return internalSerialise(object, {}, Buffer()):toString() | |
end | |
local function unserialise(msg) | |
local func = loadstring("return " .. msg, "unserialise-temp", nil, {}) | |
if not func then return nil end | |
local ok, res = pcall(func) | |
return ok and res | |
end | |
--- @export | |
return { | |
serialise = serialise, | |
unserialise = unserialise, | |
deserialise = unserialise, | |
dump = dump, | |
} | |
end | |
preload["howl.lib.colored"] = function(...) | |
--- Print coloured strings | |
-- @module howl.lib.utils | |
local term = require "howl.platform".term | |
--- Prints a string in a colour if colour is supported | |
-- @tparam int color The colour to print | |
-- @param ... Values to print | |
local function printColor(color, ...) | |
term.setColor(color) | |
term.print(...) | |
term.resetColor(color) | |
end | |
--- Writes a string in a colour if colour is supported | |
-- @tparam int color The colour to print | |
-- @tparam string text Values to print | |
local function writeColor(color, text) | |
term.setColor(color) | |
term.write(text) | |
term.resetColor(color) | |
end | |
return { | |
printColor = printColor, | |
writeColor = writeColor, | |
} | |
end | |
preload["howl.lib.Buffer"] = function(...) | |
--- An optimised class for appending strings | |
-- @classmod howl.lib.Buffer | |
local concat = table.concat | |
--- Append to this buffer | |
-- @tparam string text | |
-- @treturn Buffer The current buffer to allow chaining | |
local function append(self, text) | |
local n = self.n + 1 | |
self[n] = text | |
self.n = n | |
return self | |
end | |
--- Convert this buffer to a string | |
-- @treturn string String representation of the buffer | |
local function toString(self) | |
return concat(self) | |
end | |
--- Create a new buffer | |
-- @treturn Buffer The buffer | |
return function() | |
return { | |
n = 0, append = append, toString = toString | |
} | |
end | |
end | |
preload["howl.lib.assert"] = function(...) | |
--- Assertion helpers | |
-- @module howl.lib.assert | |
local type, error, floor, select = type, error, select, math.floor | |
local nativeAssert = assert | |
local assert = setmetatable( | |
{ assert = nativeAssert }, | |
{ __call = function(self, ...) return nativeAssert(...) end } | |
) | |
local function typeError(type, expected, message) | |
if message then | |
return error(message:format(type)) | |
else | |
return error(expected .. " expected, got " .. type) | |
end | |
end | |
function assert.type(value, expected, message) | |
local t = type(value) | |
if t ~= expected then | |
return typeError(t, expected, message) | |
end | |
end | |
local function argError(type, expected, func, index) | |
return error("bad argument #" .. index .. " for " .. func .. " (expected " .. expected .. ", got " .. type .. ")") | |
end | |
function assert.argType(value, expected, func, index) | |
local t = type(value) | |
if t ~= expected then | |
return argError(t, expected, func, index) | |
end | |
end | |
function assert.args(func, ...) | |
local len = select('#', ...) | |
local args = {...} | |
for k = 1, len, 2 do | |
local t = type(args[i]) | |
local expected = args[i + 1] | |
if t ~= expected then | |
return argError(t, expected, func, math.floor(k / 2)) | |
end | |
end | |
end | |
assert.typeError = typeError | |
assert.argError = argError | |
function assert.class(value, expected, message) | |
local t = type(value) | |
if t ~= "table" or not value.isInstanceOf then | |
return typeError(t, expected, message) | |
elseif not value:isInstanceOf(expected) then | |
return typeError(value.class.name, expected, message) | |
end | |
end | |
return assert | |
end | |
preload["howl.lib.argparse"] = function(...) | |
--- Parses command line arguments | |
-- @module howl.lib.argparse | |
local colored = require "howl.lib.colored" | |
--- Simple wrapper for Options | |
-- @type Option | |
local Option = { | |
__index = function(self, func) | |
return function(self, ...) | |
local parser = self.parser | |
local value = parser[func](parser, self.name, ...) | |
if value == parser then return self end -- Allow chaining | |
return value | |
end | |
end | |
} | |
--- Parses command line arguments | |
-- @type Parser | |
local Parser = {} | |
--- Returns the value of a option | |
-- @tparam string name The name of the option | |
-- @tparam string|boolean default The default value (optional) | |
-- @treturn string|boolean The value of the option | |
function Parser:Get(name, default) | |
local options = self.options | |
local value = options[name] | |
if value ~= nil then return value end | |
local settings = self.settings[name] | |
if settings then | |
local aliases = settings.aliases | |
if aliases then | |
for _, alias in ipairs(aliases) do | |
value = options[alias] | |
if value ~= nil then return value end | |
end | |
end | |
value = settings.default | |
if value ~= nil then return value end | |
end | |
return default | |
end | |
--- Ensure a option exists, throw an error otherwise | |
-- @tparam string name The name of the option | |
-- @treturn string|boolean The resulting value | |
function Parser:Ensure(name) | |
local value = self:Get(name) | |
if value == nil then | |
error(name .. " must be set") | |
end | |
return value | |
end | |
--- Set the default value for an option | |
-- @tparam string name The name of the options | |
-- @tparam string|boolean value The default value | |
-- @treturn Parser The current object | |
function Parser:Default(name, value) | |
if value == nil then value = true end | |
self:_SetSetting(name, "default", value) | |
self:_Changed() | |
return self | |
end | |
--- Sets an alias for an option | |
-- @tparam string name The name of the option | |
-- @tparam string alias The alias of the option | |
-- @treturn Parser The current object | |
function Parser:Alias(name, alias) | |
local settings = self.settings | |
local currentSettings = settings[name] | |
if currentSettings then | |
local currentAliases = currentSettings.aliases | |
if currentAliases == nil then | |
currentSettings.aliases = { alias } | |
else | |
table.insert(currentAliases, alias) | |
end | |
else | |
settings[name] = { aliases = { alias } } | |
end | |
self:_Changed() | |
return self | |
end | |
--- Sets the description, and type for an option | |
-- @tparam string name The name of the option | |
-- @tparam string description The description of the option | |
-- @treturn Parser The current object | |
function Parser:Description(name, description) | |
return self:_SetSetting(name, "description", description) | |
end | |
--- Sets if this option takes a value | |
-- @tparam string name The name of the option | |
-- @tparam boolean takesValue If the option takes a value | |
-- @treturn Parser The current object | |
function Parser:TakesValue(name, takesValue) | |
if takesValue == nil then | |
takesValue = true | |
end | |
return self:_SetSetting(name, "takesValue", takesValue) | |
end | |
--- Sets a setting for an option | |
-- @tparam string name The name of the option | |
-- @tparam string key The key of the setting | |
-- @tparam boolean|string value The value of the setting | |
-- @treturn Parser The current object | |
-- @local | |
function Parser:_SetSetting(name, key, value) | |
local settings = self.settings | |
local thisSettings = settings[name] | |
if thisSettings then | |
thisSettings[key] = value | |
else | |
settings[name] = { [key] = value } | |
end | |
return self | |
end | |
--- Creates a useful option helper object | |
-- @tparam string name The name of the option | |
-- @treturn Option The created option | |
function Parser:Option(name) | |
return setmetatable({ | |
name = name, | |
parser = self | |
}, Option) | |
end | |
--- Returns a list of arguments | |
-- @treturn table The argument list | |
function Parser:Arguments() | |
return self.arguments | |
end | |
--- Fires the on changed event | |
-- @local | |
function Parser:_Changed() | |
self.mediator:publish({ "ArgParse", "changed" }, self) | |
end | |
--- Generates a help string | |
-- @tparam string indent The indent to print it at | |
function Parser:Help(indent) | |
for name, settings in pairs(self.settings) do | |
local prefix = '-' | |
-- If we take a value then we should say so | |
if settings.takesValue then | |
prefix = "--" | |
name = name .. "=value" | |
end | |
-- If length is more than one then we should set | |
-- the prefix to be -- | |
if #name > 1 then | |
prefix = '--' | |
end | |
colored.writeColor("white", indent .. prefix .. name) | |
local aliasStr = "" | |
local aliases = settings.aliases | |
if aliases and #aliases > 0 then | |
local aliasLength = #aliases | |
aliasStr = aliasStr .. " (" | |
for i = 1, aliasLength do | |
local alias = "-" .. aliases[i] | |
if #alias > 2 then -- "-" and another character | |
alias = "-" .. alias | |
end | |
if i < aliasLength then | |
alias = alias .. ', ' | |
end | |
aliasStr = aliasStr .. alias | |
end | |
aliasStr = aliasStr .. ")" | |
end | |
colored.writeColor("brown", aliasStr) | |
local description = settings.description | |
if description and description ~= "" then | |
colored.printColor("lightGray", " " .. description) | |
end | |
end | |
end | |
--- Parse the options | |
-- @treturn Parser The current object | |
function Parser:Parse(args) | |
local options = self.options | |
local arguments = self.arguments | |
for _, arg in ipairs(args) do | |
if arg:sub(1, 1) == "-" then -- Match `-` | |
if arg:sub(2, 2) == "-" then -- Match `--` | |
local key, value = arg:match("([%w_%-]+)=([%w_%-]+)", 3) -- Match [a-zA-Z0-9_-] in form key=value | |
if key then | |
options[key] = value | |
else | |
-- If it starts with not- or not_ then negate it | |
arg = arg:sub(3) | |
local beginning = arg:sub(1, 4) | |
local value = true | |
if beginning == "not-" or beginning == "not_" then | |
value = false | |
arg = arg:sub(5) | |
end | |
options[arg] = value | |
end | |
else -- Handle switches | |
for i = 2, #arg do | |
options[arg:sub(i, i)] = true | |
end | |
end | |
else | |
table.insert(arguments, arg) | |
end | |
end | |
return self | |
end | |
--- Create a new options parser | |
-- @tparam table mediator The mediator instance | |
-- @tparam table args The command line arguments passed | |
-- @treturn Parser The resulting parser | |
local function Options(mediator, args) | |
return setmetatable({ | |
options = {}, -- The resulting values | |
arguments = {}, -- Spare arguments | |
mediator = mediator, | |
settings = {}, -- Settings for options | |
}, { __index = Parser }):Parse(args) | |
end | |
--- @export | |
return { | |
Parser = Parser, | |
Options = Options, | |
} | |
end | |
preload["howl.lexer.walk"] = function(...) | |
local function terminate() end | |
local function callExpr(node, visitor) | |
visitor(node.Base) | |
for _, v in ipairs(node.Arguments) do visitor(v) end | |
end | |
local function indexExpr(node, visitor) | |
visitor(node.Base) | |
visitor(node.Index) | |
end | |
local visitors | |
local function visit(node, visitor) | |
local traverse = visitors[node.AstType] | |
if not traverse then | |
error("No visitor for " .. node.AstType) | |
end | |
traverse(node, visitor) | |
end | |
visitors = { | |
VarExpr = terminate, | |
NumberExpr = terminate, | |
StringExpr = terminate, | |
BooleanExpr = terminate, | |
NilExpr = terminate, | |
DotsExpr = terminate, | |
Eof = terminate, | |
BinopExpr = function(node, visitor) | |
visitor(node.Lhs) | |
visitor(node.Rhs) | |
end, | |
UnopExpr = function(node, visitor) | |
visitor(node.Rhs) | |
end, | |
CallExpr = callExpr, | |
TableCallExpr = callExpr, | |
StringCallExpr = callExpr, | |
IndexExpr = indexExpr, | |
MemberExpr = indexExpr, | |
Function = function(node, visitor) | |
if node.Name and not node.IsLocal then visitor(node.Name) end | |
visitor(node.Body) | |
end, | |
ConstructorExpr = function(node, visitor) | |
for _, v in ipairs(node.EntryList) do | |
if v.Type == "Key" then visitor(v.Key) end | |
visitor(v.Value) | |
end | |
end, | |
Parentheses = function(node, visitor) | |
visitor(v.Inner) | |
end, | |
Statlist = function(node, visitor) | |
for _, v in ipairs(node.Body) do | |
visitor(v) | |
end | |
end, | |
ReturnStatement = function(node, visitor) | |
for _, v in ipairs(node.Arguments) do visitor(v) end | |
end, | |
AssignmentStatement = function(node, visitor) | |
for _, v in ipairs(node.Lhs) do visitor(v) end | |
for _, v in ipairs(node.Rhs) do visitor(v) end | |
end, | |
LocalStatement = function(node, visitor) | |
for _, v in ipairs(node.InitList) do visitor(v) end | |
end, | |
CallStatement = function(node, visitor) | |
visitor(v.Expression) | |
end, | |
IfStatement = function(node, visitor) | |
for _, v in ipairs(node.Clauses) do | |
if v.Condition then visitor(v.Condition) end | |
visitor(v.Body) | |
end | |
end, | |
WhileStatement = function(node, visitor) | |
visitor(node.Condition) | |
visitor(node.Body) | |
end, | |
DoStatement = function(node, visitor) visitor(node.Body) end, | |
BreakStatement = terminate, | |
LabelStatement = terminate, | |
GotoStatement = terminate, | |
RepeatStatement = function(node, visitor) | |
visitor(node.Body) | |
visitor(node.Condition) | |
end, | |
GenericForStatement = function(node, visitor) | |
for _, v in ipairs(node.Generators) do visitor(v) end | |
visitor(node.Body) | |
end, | |
NumericForStatement = function(node, visitor) | |
visitor(node.Start) | |
visitor(node.End) | |
if node.Step then visitor(node.Step) end | |
visitor(node.Body) | |
end | |
} | |
return visit | |
end | |
preload["howl.lexer.TokenList"] = function(...) | |
--- Provides utilities for reading tokens from a 'stream' | |
-- @module howl.lexer.TokenList | |
local min = math.min | |
local insert = table.insert | |
return function(tokens) | |
local n = #tokens | |
local pointer = 1 | |
--- Get this element in the token list | |
-- @tparam int offset The offset in the token list | |
local function Peek(offset) | |
return tokens[min(n, pointer + (offset or 0))] | |
end | |
--- Get the next token in the list | |
-- @tparam table tokenList Add the token onto this table | |
-- @treturn Token The token | |
local function Get(tokenList) | |
local token = tokens[pointer] | |
pointer = min(pointer + 1, n) | |
if tokenList then | |
insert(tokenList, token) | |
end | |
return token | |
end | |
--- Check if the next token is of a type | |
-- @tparam string type The type to compare it with | |
-- @treturn bool If the type matches | |
local function Is(type) | |
return Peek().Type == type | |
end | |
--- Check if the next token is a symbol and return it | |
-- @tparam string symbol Symbol to check (Optional) | |
-- @tparam table tokenList Add the token onto this table | |
-- @treturn [ 0 ] ?|token If symbol is not specified, return the token | |
-- @treturn [ 1 ] boolean If symbol is specified, return true if it matches | |
local function ConsumeSymbol(symbol, tokenList) | |
local token = Peek() | |
if token.Type == 'Symbol' then | |
if symbol then | |
if token.Data == symbol then | |
if tokenList then insert(tokenList, token) end | |
pointer = pointer + 1 | |
return true | |
else | |
return nil | |
end | |
else | |
if tokenList then insert(tokenList, token) end | |
pointer = pointer + 1 | |
return token | |
end | |
else | |
return nil | |
end | |
end | |
--- Check if the next token is a keyword and return it | |
-- @tparam string kw Keyword to check (Optional) | |
-- @tparam table tokenList Add the token onto this table | |
-- @treturn [ 0 ] ?|token If kw is not specified, return the token | |
-- @treturn [ 1 ] boolean If kw is specified, return true if it matches | |
local function ConsumeKeyword(kw, tokenList) | |
local token = Peek() | |
if token.Type == 'Keyword' and token.Data == kw then | |
if tokenList then insert(tokenList, token) end | |
pointer = pointer + 1 | |
return true | |
else | |
return nil | |
end | |
end | |
--- Check if the next token matches is a keyword | |
-- @tparam string kw The particular keyword | |
-- @treturn boolean If it matches or not | |
local function IsKeyword(kw) | |
local token = Peek() | |
return token.Type == 'Keyword' and token.Data == kw | |
end | |
--- Check if the next token matches is a symbol | |
-- @tparam string symbol The particular symbol | |
-- @treturn boolean If it matches or not | |
local function IsSymbol(symbol) | |
local token = Peek() | |
return token.Type == 'Symbol' and token.Data == symbol | |
end | |
--- Check if the next token is an end of file | |
-- @treturn boolean If the next token is an end of file | |
local function IsEof() | |
return Peek().Type == 'Eof' | |
end | |
--- Produce a string off all tokens | |
-- @tparam boolean includeLeading Include the leading whitespace | |
-- @treturn string The resulting string | |
local function Print(includeLeading) | |
includeLeading = (includeLeading == nil and true or includeLeading) | |
local out = "" | |
for _, token in ipairs(tokens) do | |
if includeLeading then | |
for _, whitespace in ipairs(token.LeadingWhite) do | |
out = out .. whitespace:Print() .. "\n" | |
end | |
end | |
out = out .. token:Print() .. "\n" | |
end | |
return out | |
end | |
return { | |
Peek = Peek, | |
Get = Get, | |
Is = Is, | |
ConsumeSymbol = ConsumeSymbol, | |
ConsumeKeyword = ConsumeKeyword, | |
IsKeyword = IsKeyword, | |
IsSymbol = IsSymbol, | |
IsEof = IsEof, | |
Print = Print, | |
Tokens = tokens, | |
} | |
end | |
end | |
preload["howl.lexer.Scope"] = function(...) | |
--- Holds variables for one scope | |
-- This implementation is inefficient. Instead of using hashes, | |
-- a linear search is used instead to look up variables | |
-- @module howl.lexer.Scope | |
local keywords = require "howl.lexer.constants".Keywords | |
--- Holds the data for one variable | |
-- @table Variable | |
-- @tfield Scope Scope The parent scope | |
-- @tfield string Name The name of the variable | |
-- @tfield boolean IsGlobal Is the variable global | |
-- @tfield boolean CanRename If the variable can be renamed | |
-- @tfield int References Number of references | |
--- Holds variables for one scope | |
-- @type Scope | |
-- @tfield ?|Scope Parent The parent scope | |
-- @tfield table Locals A list of locals variables | |
-- @tfield table Globals A list of global variables | |
-- @tfield table Children A list of children @{Scope|scopes} | |
local Scope = {} | |
--- Add a local to this scope | |
-- @tparam Variable variable The local object | |
function Scope:AddLocal(name, variable) | |
table.insert(self.Locals, variable) | |
self.LocalMap[name] = variable | |
end | |
--- Create a @{Variable} and add it to the scope | |
-- @tparam string name The name of the local | |
-- @treturn Variable The created local | |
function Scope:CreateLocal(name) | |
local variable = self:GetLocal(name) | |
if variable then return variable end | |
variable = { | |
Scope = self, | |
Name = name, | |
IsGlobal = false, | |
CanRename = true, | |
References = 1, | |
} | |
self:AddLocal(name, variable) | |
return variable | |
end | |
--- Get a local variable | |
-- @tparam string name The name of the local | |
-- @treturn ?|Variable The variable | |
function Scope:GetLocal(name) | |
repeat | |
local var = self.LocalMap[name] | |
if var then return var end | |
self = self.Parent | |
until not self | |
end | |
--- Find an local variable by its old name | |
-- @tparam string name The old name of the local | |
-- @treturn ?|Variable The local variable | |
function Scope:GetOldLocal(name) | |
if self.oldLocalNamesMap[name] then | |
return self.oldLocalNamesMap[name] | |
end | |
return self:GetLocal(name) | |
end | |
--- Rename a local variable | |
-- @tparam string|Variable oldName The old variable name | |
-- @tparam string newName The new variable name | |
function Scope:RenameLocal(oldName, newName) | |
oldName = type(oldName) == 'string' and oldName or oldName.Name | |
repeat | |
local var = self.LocalMap[oldName] | |
if var then | |
var.Name = newName | |
self.oldLocalNamesMap[oldName] = var | |
self.LocalMap[oldName] = nil | |
self.LocalMap[newName] = var | |
break | |
end | |
self = self.Parent | |
until not self | |
end | |
--- Add a global to this scope | |
-- @tparam Variable name The name of the global | |
function Scope:AddGlobal(name, variable) | |
table.insert(self.Globals, variable) | |
self.GlobalMap[name] = variable | |
end | |
--- Create a @{Variable} and add it to the scope | |
-- @tparam string name The name of the global | |
-- @treturn Variable The created global | |
function Scope:CreateGlobal(name) | |
local variable = self:GetGlobal(name) | |
if variable then return variable end | |
variable = { | |
Scope = self, | |
Name = name, | |
IsGlobal = true, | |
CanRename = true, | |
References = 1, | |
} | |
self:AddGlobal(name, variable) | |
return variable | |
end | |
--- Get a global variable | |
-- @tparam string name The name of the global | |
-- @treturn ?|Variable The variable | |
function Scope:GetGlobal(name) | |
repeat | |
local var = self.GlobalMap[name] | |
if var then return var end | |
self = self.Parent | |
until not self | |
end | |
--- Get a variable by name | |
-- @tparam string name The name of the variable | |
-- @treturn ?|Variable The found variable | |
-- @fixme This is a very inefficient implementation, as with @{Scope:GetLocal} and @{Scope:GetGlocal} | |
function Scope:GetVariable(name) | |
return self:GetLocal(name) or self:GetGlobal(name) | |
end | |
--- Get all variables in the scope | |
-- @treturn table A list of @{Variable|variables} | |
function Scope:GetAllVariables() | |
return self:getVars(true, self:getVars(true)) | |
end | |
--- Get all variables | |
-- @tparam boolean top If this values is the 'top' of the function stack | |
-- @tparam table ret Table to fill with return values (optional) | |
-- @treturn table The variables | |
-- @local | |
function Scope:getVars(top, ret) | |
local ret = ret or {} | |
if top then | |
for _, v in pairs(self.Children) do | |
v:getVars(true, ret) | |
end | |
else | |
for _, v in pairs(self.Locals) do | |
table.insert(ret, v) | |
end | |
for _, v in pairs(self.Globals) do | |
table.insert(ret, v) | |
end | |
if self.Parent then | |
self.Parent:getVars(false, ret) | |
end | |
end | |
return ret | |
end | |
--- Rename all locals to smaller values | |
-- @tparam string validNameChars All characters that can be used to make a variable name | |
-- @fixme Some of the string generation happens a lot, this could be looked at | |
function Scope:ObfuscateLocals(validNameChars) | |
-- Use values sorted for letter frequency instead | |
local startChars = validNameChars or "etaoinshrdlucmfwypvbgkqjxz_ETAOINSHRDLUCMFWYPVBGKQJXZ" | |
local otherChars = validNameChars or "etaoinshrdlucmfwypvbgkqjxz_0123456789ETAOINSHRDLUCMFWYPVBGKQJXZ" | |
local startCharsLength, otherCharsLength = #startChars, #otherChars | |
local index = 0 | |
local floor = math.floor | |
for _, var in pairs(self.Locals) do | |
local name | |
repeat | |
if index < startCharsLength then | |
index = index + 1 | |
name = startChars:sub(index, index) | |
else | |
if index < startCharsLength then | |
index = index + 1 | |
name = startChars:sub(index, index) | |
else | |
local varIndex = floor(index / startCharsLength) | |
local offset = index % startCharsLength | |
name = startChars:sub(offset, offset) | |
while varIndex > 0 do | |
offset = varIndex % otherCharsLength | |
name = otherChars:sub(offset, offset) .. name | |
varIndex = floor(varIndex / otherCharsLength) | |
end | |
index = index + 1 | |
end | |
end | |
until not (keywords[name] or self:GetVariable(name)) | |
self:RenameLocal(var.Name, name) | |
end | |
end | |
--- Converts the scope to a string | |
-- No, it actually just returns '<scope>' | |
-- @treturn string '<scope>' | |
function Scope:ToString() | |
return '<Scope>' | |
end | |
--- Create a new scope | |
-- @tparam Scope parent The parent scope | |
-- @treturn Scope The created scope | |
local function NewScope(parent) | |
local scope = setmetatable({ | |
Parent = parent, | |
Locals = {}, | |
LocalMap = {}, | |
Globals = {}, | |
GlobalMap = {}, | |
oldLocalNamesMap = {}, | |
Children = {}, | |
}, { __index = Scope }) | |
if parent then | |
table.insert(parent.Children, scope) | |
end | |
return scope | |
end | |
return NewScope | |
end | |
preload["howl.lexer.rebuild"] = function(...) | |
--- Rebuild source code from an AST | |
-- Does not preserve whitespace | |
-- @module howl.lexer.rebuild | |
local constants = require "howl.lexer.constants" | |
local parse = require "howl.lexer.parse" | |
local platform = require "howl.platform" | |
local lowerChars = constants.LowerChars | |
local upperChars = constants.UpperChars | |
local digits = constants.Digits | |
local symbols = constants.Symbols | |
--- Join two statements together | |
-- @tparam string left The left statement | |
-- @tparam string right The right statement | |
-- @tparam string sep The string used to separate the characters | |
-- @treturn string The joined strings | |
local function doJoinStatements(left, right, sep) | |
sep = sep or ' ' | |
local leftEnd, rightStart = left:sub(-1, -1), right:sub(1, 1) | |
if upperChars[leftEnd] or lowerChars[leftEnd] or leftEnd == '_' then | |
if not (rightStart == '_' or upperChars[rightStart] or lowerChars[rightStart] or digits[rightStart]) then | |
--rightStart is left symbol, can join without seperation | |
return left .. right | |
else | |
return left .. sep .. right | |
end | |
elseif digits[leftEnd] then | |
if rightStart == '(' then | |
--can join statements directly | |
return left .. right | |
elseif symbols[rightStart] then | |
return left .. right | |
else | |
return left .. sep .. right | |
end | |
elseif leftEnd == '' then | |
return left .. right | |
else | |
if rightStart == '(' then | |
--don't want to accidentally call last statement, can't join directly | |
return left .. sep .. right | |
else | |
return left .. right | |
end | |
end | |
end | |
--- Returns the minified version of an AST. Operations which are performed: | |
-- - All comments and whitespace are ignored | |
-- - All local variables are renamed | |
-- @tparam Node ast The AST tree | |
-- @treturn string The minified string | |
-- @todo Ability to control minification level | |
-- @todo Convert to a buffer | |
local function minify(ast) | |
local formatStatlist, formatExpr | |
local count = 0 | |
local function joinStatements(left, right, sep) | |
if count > 150 then | |
count = 0 | |
return left .. "\n" .. right | |
else | |
return doJoinStatements(left, right, sep) | |
end | |
end | |
formatExpr = function(expr, precedence) | |
local precedence = precedence or 0 | |
local currentPrecedence = 0 | |
local skipParens = false | |
local out = "" | |
if expr.AstType == 'VarExpr' then | |
if expr.Variable then | |
out = out .. expr.Variable.Name | |
else | |
out = out .. expr.Name | |
end | |
elseif expr.AstType == 'NumberExpr' then | |
out = out .. expr.Value.Data | |
elseif expr.AstType == 'StringExpr' then | |
out = out .. expr.Value.Data | |
elseif expr.AstType == 'BooleanExpr' then | |
out = out .. tostring(expr.Value) | |
elseif expr.AstType == 'NilExpr' then | |
out = joinStatements(out, "nil") | |
elseif expr.AstType == 'BinopExpr' then | |
currentPrecedence = expr.OperatorPrecedence | |
out = joinStatements(out, formatExpr(expr.Lhs, currentPrecedence)) | |
out = joinStatements(out, expr.Op) | |
out = joinStatements(out, formatExpr(expr.Rhs)) | |
if expr.Op == '^' or expr.Op == '..' then | |
currentPrecedence = currentPrecedence - 1 | |
end | |
if currentPrecedence < precedence then | |
skipParens = false | |
else | |
skipParens = true | |
end | |
elseif expr.AstType == 'UnopExpr' then | |
out = joinStatements(out, expr.Op) | |
out = joinStatements(out, formatExpr(expr.Rhs)) | |
elseif expr.AstType == 'DotsExpr' then | |
out = out .. "..." | |
elseif expr.AstType == 'CallExpr' then | |
out = out .. formatExpr(expr.Base) | |
out = out .. "(" | |
for i = 1, #expr.Arguments do | |
out = out .. formatExpr(expr.Arguments[i]) | |
if i ~= #expr.Arguments then | |
out = out .. "," | |
end | |
end | |
out = out .. ")" | |
elseif expr.AstType == 'TableCallExpr' then | |
out = out .. formatExpr(expr.Base) | |
out = out .. formatExpr(expr.Arguments[1]) | |
elseif expr.AstType == 'StringCallExpr' then | |
out = out .. formatExpr(expr.Base) | |
out = out .. expr.Arguments[1].Data | |
elseif expr.AstType == 'IndexExpr' then | |
out = out .. formatExpr(expr.Base) .. "[" .. formatExpr(expr.Index) .. "]" | |
elseif expr.AstType == 'MemberExpr' then | |
out = out .. formatExpr(expr.Base) .. expr.Indexer .. expr.Ident.Data | |
elseif expr.AstType == 'Function' then | |
expr.Scope:ObfuscateLocals() | |
out = out .. "function(" | |
if #expr.Arguments > 0 then | |
for i = 1, #expr.Arguments do | |
out = out .. expr.Arguments[i].Name | |
if i ~= #expr.Arguments then | |
out = out .. "," | |
elseif expr.VarArg then | |
out = out .. ",..." | |
end | |
end | |
elseif expr.VarArg then | |
out = out .. "..." | |
end | |
out = out .. ")" | |
out = joinStatements(out, formatStatlist(expr.Body)) | |
out = joinStatements(out, "end") | |
elseif expr.AstType == 'ConstructorExpr' then | |
out = out .. "{" | |
for i = 1, #expr.EntryList do | |
local entry = expr.EntryList[i] | |
if entry.Type == 'Key' then | |
out = out .. "[" .. formatExpr(entry.Key) .. "]=" .. formatExpr(entry.Value) | |
elseif entry.Type == 'Value' then | |
out = out .. formatExpr(entry.Value) | |
elseif entry.Type == 'KeyString' then | |
out = out .. entry.Key .. "=" .. formatExpr(entry.Value) | |
end | |
if i ~= #expr.EntryList then | |
out = out .. "," | |
end | |
end | |
out = out .. "}" | |
elseif expr.AstType == 'Parentheses' then | |
out = out .. "(" .. formatExpr(expr.Inner) .. ")" | |
end | |
if not skipParens then | |
out = string.rep('(', expr.ParenCount or 0) .. out | |
out = out .. string.rep(')', expr.ParenCount or 0) | |
end | |
count = count + #out | |
return out | |
end | |
local formatStatement = function(statement) | |
local out = '' | |
if statement.AstType == 'AssignmentStatement' then | |
for i = 1, #statement.Lhs do | |
out = out .. formatExpr(statement.Lhs[i]) | |
if i ~= #statement.Lhs then | |
out = out .. "," | |
end | |
end | |
if #statement.Rhs > 0 then | |
out = out .. "=" | |
for i = 1, #statement.Rhs do | |
out = out .. formatExpr(statement.Rhs[i]) | |
if i ~= #statement.Rhs then | |
out = out .. "," | |
end | |
end | |
end | |
elseif statement.AstType == 'CallStatement' then | |
out = formatExpr(statement.Expression) | |
elseif statement.AstType == 'LocalStatement' then | |
out = out .. "local " | |
for i = 1, #statement.LocalList do | |
out = out .. statement.LocalList[i].Name | |
if i ~= #statement.LocalList then | |
out = out .. "," | |
end | |
end | |
if #statement.InitList > 0 then | |
out = out .. "=" | |
for i = 1, #statement.InitList do | |
out = out .. formatExpr(statement.InitList[i]) | |
if i ~= #statement.InitList then | |
out = out .. "," | |
end | |
end | |
end | |
elseif statement.AstType == 'IfStatement' then | |
out = joinStatements("if", formatExpr(statement.Clauses[1].Condition)) | |
out = joinStatements(out, "then") | |
out = joinStatements(out, formatStatlist(statement.Clauses[1].Body)) | |
for i = 2, #statement.Clauses do | |
local st = statement.Clauses[i] | |
if st.Condition then | |
out = joinStatements(out, "elseif") | |
out = joinStatements(out, formatExpr(st.Condition)) | |
out = joinStatements(out, "then") | |
else | |
out = joinStatements(out, "else") | |
end | |
out = joinStatements(out, formatStatlist(st.Body)) | |
end | |
out = joinStatements(out, "end") | |
elseif statement.AstType == 'WhileStatement' then | |
out = joinStatements("while", formatExpr(statement.Condition)) | |
out = joinStatements(out, "do") | |
out = joinStatements(out, formatStatlist(statement.Body)) | |
out = joinStatements(out, "end") | |
elseif statement.AstType == 'DoStatement' then | |
out = joinStatements(out, "do") | |
out = joinStatements(out, formatStatlist(statement.Body)) | |
out = joinStatements(out, "end") | |
elseif statement.AstType == 'ReturnStatement' then | |
out = "return" | |
for i = 1, #statement.Arguments do | |
out = joinStatements(out, formatExpr(statement.Arguments[i])) | |
if i ~= #statement.Arguments then | |
out = out .. "," | |
end | |
end | |
elseif statement.AstType == 'BreakStatement' then | |
out = "break" | |
elseif statement.AstType == 'RepeatStatement' then | |
out = "repeat" | |
out = joinStatements(out, formatStatlist(statement.Body)) | |
out = joinStatements(out, "until") | |
out = joinStatements(out, formatExpr(statement.Condition)) | |
elseif statement.AstType == 'Function' then | |
statement.Scope:ObfuscateLocals() | |
if statement.IsLocal then | |
out = "local" | |
end | |
out = joinStatements(out, "function ") | |
if statement.IsLocal then | |
out = out .. statement.Name.Name | |
else | |
out = out .. formatExpr(statement.Name) | |
end | |
out = out .. "(" | |
if #statement.Arguments > 0 then | |
for i = 1, #statement.Arguments do | |
out = out .. statement.Arguments[i].Name | |
if i ~= #statement.Arguments then | |
out = out .. "," | |
elseif statement.VarArg then | |
out = out .. ",..." | |
end | |
end | |
elseif statement.VarArg then | |
out = out .. "..." | |
end | |
out = out .. ")" | |
out = joinStatements(out, formatStatlist(statement.Body)) | |
out = joinStatements(out, "end") | |
elseif statement.AstType == 'GenericForStatement' then | |
statement.Scope:ObfuscateLocals() | |
out = "for " | |
for i = 1, #statement.VariableList do | |
out = out .. statement.VariableList[i].Name | |
if i ~= #statement.VariableList then | |
out = out .. "," | |
end | |
end | |
out = out .. " in" | |
for i = 1, #statement.Generators do | |
out = joinStatements(out, formatExpr(statement.Generators[i])) | |
if i ~= #statement.Generators then | |
out = joinStatements(out, ',') | |
end | |
end | |
out = joinStatements(out, "do") | |
out = joinStatements(out, formatStatlist(statement.Body)) | |
out = joinStatements(out, "end") | |
elseif statement.AstType == 'NumericForStatement' then | |
statement.Scope:ObfuscateLocals() | |
out = "for " | |
out = out .. statement.Variable.Name .. "=" | |
out = out .. formatExpr(statement.Start) .. "," .. formatExpr(statement.End) | |
if statement.Step then | |
out = out .. "," .. formatExpr(statement.Step) | |
end | |
out = joinStatements(out, "do") | |
out = joinStatements(out, formatStatlist(statement.Body)) | |
out = joinStatements(out, "end") | |
elseif statement.AstType == 'LabelStatement' then | |
out = "::" .. statement.Label .. "::" | |
elseif statement.AstType == 'GotoStatement' then | |
out = "goto " .. statement.Label | |
elseif statement.AstType == 'Comment' then | |
-- ignore | |
elseif statement.AstType == 'Eof' then | |
-- ignore | |
else | |
error("Unknown AST Type: " .. statement.AstType) | |
end | |
count = count + #out | |
return out | |
end | |
formatStatlist = function(statList) | |
local out = '' | |
statList.Scope:ObfuscateLocals() | |
for _, stat in pairs(statList.Body) do | |
out = joinStatements(out, formatStatement(stat), ';') | |
end | |
return out | |
end | |
return formatStatlist(ast) | |
end | |
--- Minify a string | |
-- @tparam string input The input string | |
-- @treturn string The minifyied string | |
local function minifyString(input) | |
local lex = parse.LexLua(input) | |
platform.refreshYield() | |
local tree = parse.ParseLua(lex) | |
platform.refreshYield() | |
local min = minify(tree) | |
platform.refreshYield() | |
return min | |
end | |
--- Minify a file | |
-- @tparam string cd Current directory | |
-- @tparam string inputFile File to read from | |
-- @tparam string outputFile File to write to (Defaults to inputFile) | |
local function minifyFile(cd, inputFile, outputFile) | |
outputFile = outputFile or inputFile | |
local oldContents = platform.fs.read(platform.fs.combine(cd, inputFile)) | |
local newContents = minifyString(oldContents) | |
platform.fs.write(platform.fs.combine(cd, outputFile), newContents) | |
return #oldContents, #newContents | |
end | |
--- @export | |
return { | |
minify = minify, | |
minifyString = minifyString, | |
minifyFile = minifyFile, | |
} | |
end | |
preload["howl.lexer.parse"] = function(...) | |
--- The main lua parser and lexer. | |
-- LexLua returns a Lua token stream, with tokens that preserve | |
-- all whitespace formatting information. | |
-- ParseLua returns an AST, internally relying on LexLua. | |
-- @module howl.lexer.parse | |
local Constants = require "howl.lexer.constants" | |
local Scope = require "howl.lexer.Scope" | |
local TokenList = require "howl.lexer.TokenList" | |
local lowerChars = Constants.LowerChars | |
local upperChars = Constants.UpperChars | |
local digits = Constants.Digits | |
local symbols = Constants.Symbols | |
local hexDigits = Constants.HexDigits | |
local keywords = Constants.Keywords | |
local statListCloseKeywords = Constants.StatListCloseKeywords | |
local unops = Constants.UnOps | |
local insert, setmeta = table.insert, setmetatable | |
--- One token | |
-- @table Token | |
-- @tparam string Type The token type | |
-- @param Data Data about the token | |
-- @tparam string CommentType The type of comment (Optional) | |
-- @tparam number Line Line number (Optional) | |
-- @tparam number Char Character number (Optional) | |
local Token = {} | |
--- Creates a string representation of the token | |
-- @treturn string The resulting string | |
function Token:Print() | |
return "<"..(self.Type .. string.rep(' ', math.max(3, 12-#self.Type))).." "..(self.Data or '').." >" | |
end | |
local tokenMeta = { __index = Token } | |
--- Create a list of @{Token|tokens} from a Lua source | |
-- @tparam string src Lua source code | |
-- @treturn TokenList The list of @{Token|tokens} | |
local function LexLua(src) | |
--token dump | |
local tokens = {} | |
do -- Main bulk of the work | |
local sub = string.sub | |
--line / char / pointer tracking | |
local pointer = 1 | |
local line = 1 | |
local char = 1 | |
--get / peek functions | |
local function get() | |
local c = sub(src, pointer,pointer) | |
if c == '\n' then | |
char = 1 | |
line = line + 1 | |
else | |
char = char + 1 | |
end | |
pointer = pointer + 1 | |
return c | |
end | |
local function peek(n) | |
n = n or 0 | |
return sub(src, pointer+n,pointer+n) | |
end | |
local function consume(chars) | |
local c = peek() | |
for i = 1, #chars do | |
if c == chars:sub(i,i) then return get() end | |
end | |
end | |
--shared stuff | |
local function generateError(err) | |
error(">> :"..line..":"..char..": "..err, 0) | |
end | |
local function tryGetLongString() | |
local start = pointer | |
if peek() == '[' then | |
local equalsCount = 0 | |
local depth = 1 | |
while peek(equalsCount+1) == '=' do | |
equalsCount = equalsCount + 1 | |
end | |
if peek(equalsCount+1) == '[' then | |
--start parsing the string. Strip the starting bit | |
for _ = 0, equalsCount+1 do get() end | |
--get the contents | |
local contentStart = pointer | |
while true do | |
--check for eof | |
if peek() == '' then | |
generateError("Expected `]"..string.rep('=', equalsCount).."]` near <eof>.", 3) | |
end | |
--check for the end | |
local foundEnd = true | |
if peek() == ']' then | |
for i = 1, equalsCount do | |
if peek(i) ~= '=' then foundEnd = false end | |
end | |
if peek(equalsCount+1) ~= ']' then | |
foundEnd = false | |
end | |
else | |
if peek() == '[' then | |
-- is there an embedded long string? | |
local embedded = true | |
for i = 1, equalsCount do | |
if peek(i) ~= '=' then | |
embedded = false | |
break | |
end | |
end | |
if peek(equalsCount + 1) == '[' and embedded then | |
-- oh look, there was | |
depth = depth + 1 | |
for i = 1, (equalsCount + 2) do | |
get() | |
end | |
end | |
end | |
foundEnd = false | |
end | |
if foundEnd then | |
depth = depth - 1 | |
if depth == 0 then | |
break | |
else | |
for i = 1, equalsCount + 2 do | |
get() | |
end | |
end | |
else | |
get() | |
end | |
end | |
--get the interior string | |
local contentString = src:sub(contentStart, pointer-1) | |
--found the end. Get rid of the trailing bit | |
for i = 0, equalsCount+1 do get() end | |
--get the exterior string | |
local longString = src:sub(start, pointer-1) | |
--return the stuff | |
return contentString, longString | |
else | |
return nil | |
end | |
else | |
return nil | |
end | |
end | |
local function isDigit(c) return c >= '0' and c <= '9' end | |
--main token emitting loop | |
while true do | |
--get leading whitespace. The leading whitespace will include any comments | |
--preceding the token. This prevents the parser needing to deal with comments | |
--separately. | |
local comments, cn | |
while true do | |
local c = sub(src, pointer, pointer) | |
if c == '#' and peek(1) == '!' and line == 1 then | |
-- #! shebang for linux scripts | |
get() | |
get() | |
leadingWhite = "#!" | |
while peek() ~= '\n' and peek() ~= '' do | |
get() | |
end | |
end | |
if c == ' ' or c == '\t' then | |
--whitespace | |
char = char + 1 | |
pointer = pointer + 1 | |
elseif c == '\n' or c == '\r' then | |
char = 1 | |
line = line + 1 | |
pointer = pointer + 1 | |
elseif c == '-' and peek(1) == '-' then | |
--comment | |
get() | |
get() | |
local startLine, startChar, startPointer = line, char, pointer | |
local wholeText, _ = tryGetLongString() | |
if not wholeText then | |
local next = sub(src, pointer, pointer) | |
while next ~= '\n' and next ~= '' do | |
get() | |
next = sub(src, pointer, pointer) | |
end | |
wholeText = sub(src, startPointer, pointer - 1) | |
end | |
if not comments then | |
comments = {} | |
cn = 0 | |
end | |
cn = cn + 1 | |
comments[cn] = { | |
Data = wholeText, | |
Line = startLine, | |
Char = startChar, | |
} | |
else | |
break | |
end | |
end | |
--get the initial char | |
local thisLine = line | |
local thisChar = char | |
local c = sub(src, pointer, pointer) | |
--symbol to emit | |
local toEmit = nil | |
--branch on type | |
if c == '' then | |
--eof | |
toEmit = { Type = 'Eof' } | |
elseif (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_' then | |
--ident or keyword | |
local start = pointer | |
repeat | |
get() | |
c = sub(src, pointer, pointer) | |
until not ((c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_' or (c >= '0' and c <= '9')) | |
local dat = src:sub(start, pointer-1) | |
if keywords[dat] then | |
toEmit = {Type = 'Keyword', Data = dat} | |
else | |
toEmit = {Type = 'Ident', Data = dat} | |
end | |
elseif (c >= '0' and c <= '9') or (c == '.' and digits[peek(1)]) then | |
--number const | |
local start = pointer | |
if c == '0' and peek(1) == 'x' then | |
get();get() | |
while hexDigits[peek()] do get() end | |
if consume('Pp') then | |
consume('+-') | |
while digits[peek()] do get() end | |
end | |
else | |
while digits[peek()] do get() end | |
if consume('.') then | |
while digits[peek()] do get() end | |
end | |
if consume('Ee') then | |
consume('+-') | |
if not digits[peek()] then generateError("Expected exponent") end | |
repeat get() until not digits[peek()] | |
end | |
local n = peek():lower() | |
if (n >= 'a' and n <= 'z') or n == '_' then | |
generateError("Invalid number format") | |
end | |
end | |
toEmit = {Type = 'Number', Data = src:sub(start, pointer-1)} | |
elseif c == '\'' or c == '\"' then | |
local start = pointer | |
--string const | |
local delim = get() | |
local contentStart = pointer | |
while true do | |
local c = get() | |
if c == '\\' then | |
get() --get the escape char | |
elseif c == delim then | |
break | |
elseif c == '' then | |
generateError("Unfinished string near <eof>") | |
end | |
end | |
local content = src:sub(contentStart, pointer-2) | |
local constant = src:sub(start, pointer-1) | |
toEmit = {Type = 'String', Data = constant, Constant = content} | |
elseif c == '[' then | |
local content, wholetext = tryGetLongString() | |
if wholetext then | |
toEmit = {Type = 'String', Data = wholetext, Constant = content} | |
else | |
get() | |
toEmit = {Type = 'Symbol', Data = '['} | |
end | |
elseif c == '>' or c == '<' or c == '=' then | |
get() | |
if consume('=') then | |
toEmit = {Type = 'Symbol', Data = c..'='} | |
else | |
toEmit = {Type = 'Symbol', Data = c} | |
end | |
elseif c == '~' then | |
get() | |
if consume('=') then | |
toEmit = {Type = 'Symbol', Data = '~='} | |
else | |
generateError("Unexpected symbol `~` in source.", 2) | |
end | |
elseif c == '.' then | |
get() | |
if consume('.') then | |
if consume('.') then | |
toEmit = {Type = 'Symbol', Data = '...'} | |
else | |
toEmit = {Type = 'Symbol', Data = '..'} | |
end | |
else | |
toEmit = {Type = 'Symbol', Data = '.'} | |
end | |
elseif c == ':' then | |
get() | |
if consume(':') then | |
toEmit = {Type = 'Symbol', Data = '::'} | |
else | |
toEmit = {Type = 'Symbol', Data = ':'} | |
end | |
elseif symbols[c] then | |
get() | |
toEmit = {Type = 'Symbol', Data = c} | |
else | |
local contents, all = tryGetLongString() | |
if contents then | |
toEmit = {Type = 'String', Data = all, Constant = contents} | |
else | |
generateError("Unexpected Symbol `"..c.."` in source.", 2) | |
end | |
end | |
--add the emitted symbol, after adding some common data | |
toEmit.Line = thisLine | |
toEmit.Char = thisChar | |
if comments then toEmit.Comments = comments end | |
tokens[#tokens+1] = toEmit | |
--halt after eof has been emitted | |
if toEmit.Type == 'Eof' then break end | |
end | |
end | |
--public interface: | |
local tokenList = TokenList(tokens) | |
return tokenList | |
end | |
--- Create a AST tree from a Lua Source | |
-- @tparam TokenList tok List of tokens from @{LexLua} | |
-- @treturn table The AST tree | |
local function ParseLua(tok, src) | |
--- Generate an error | |
-- @tparam string msg The error message | |
-- @raise The produces error message | |
local function GenerateError(msg) | |
local err = tok.Peek().Line..":"..tok.Peek().Char..": "..msg.."\n" | |
local peek = tok.Peek() | |
err = err.. " got " .. peek.Type .. ": " .. peek.Data.. "\n" | |
--find the line | |
local lineNum = 0 | |
if type(src) == 'string' then | |
for line in src:gmatch("[^\n]*\n?") do | |
if line:sub(-1,-1) == '\n' then line = line:sub(1,-2) end | |
lineNum = lineNum+1 | |
if lineNum == tok.Peek().Line then | |
err = err..""..line:gsub('\t',' ').."\n" | |
for i = 1, tok.Peek().Char do | |
local c = line:sub(i,i) | |
err = err..' ' | |
end | |
err = err.."^" | |
break | |
end | |
end | |
end | |
error(err) | |
end | |
local ParseExpr, | |
ParseStatementList, | |
ParseSimpleExpr, | |
ParsePrimaryExpr, | |
ParseSuffixedExpr | |
--- Parse the function definition and its arguments | |
-- @tparam Scope.Scope scope The current scope | |
-- @tparam table tokenList A table to fill with tokens | |
-- @treturn Node A function Node | |
local function ParseFunctionArgsAndBody(scope, tokenList) | |
local funcScope = Scope(scope) | |
if not tok.ConsumeSymbol('(', tokenList) then | |
GenerateError("`(` expected.") | |
end | |
--arg list | |
local argList = {} | |
local isVarArg = false | |
while not tok.ConsumeSymbol(')', tokenList) do | |
if tok.Is('Ident') then | |
local arg = funcScope:CreateLocal(tok.Get(tokenList).Data) | |
argList[#argList+1] = arg | |
if not tok.ConsumeSymbol(',', tokenList) then | |
if tok.ConsumeSymbol(')', tokenList) then | |
break | |
else | |
GenerateError("`)` expected.") | |
end | |
end | |
elseif tok.ConsumeSymbol('...', tokenList) then | |
isVarArg = true | |
if not tok.ConsumeSymbol(')', tokenList) then | |
GenerateError("`...` must be the last argument of a function.") | |
end | |
break | |
else | |
GenerateError("Argument name or `...` expected") | |
end | |
end | |
--body | |
local body = ParseStatementList(funcScope) | |
--end | |
if not tok.ConsumeKeyword('end', tokenList) then | |
GenerateError("`end` expected after function body") | |
end | |
return { | |
AstType = 'Function', | |
Scope = funcScope, | |
Arguments = argList, | |
Body = body, | |
VarArg = isVarArg, | |
Tokens = tokenList, | |
} | |
end | |
--- Parse a simple expression | |
-- @tparam Scope.Scope scope The current scope | |
-- @treturn Node the resulting node | |
function ParsePrimaryExpr(scope) | |
local tokenList = {} | |
if tok.ConsumeSymbol('(', tokenList) then | |
local ex = ParseExpr(scope) | |
if not tok.ConsumeSymbol(')', tokenList) then | |
GenerateError("`)` Expected.") | |
end | |
return { | |
AstType = 'Parentheses', | |
Inner = ex, | |
Tokens = tokenList, | |
} | |
elseif tok.Is('Ident') then | |
local id = tok.Get(tokenList) | |
local var = scope:GetLocal(id.Data) | |
if not var then | |
var = scope:GetGlobal(id.Data) | |
if not var then | |
var = scope:CreateGlobal(id.Data) | |
else | |
var.References = var.References + 1 | |
end | |
else | |
var.References = var.References + 1 | |
end | |
return { | |
AstType = 'VarExpr', | |
Name = id.Data, | |
Variable = var, | |
Tokens = tokenList, | |
} | |
else | |
GenerateError("primary expression expected") | |
end | |
end | |
--- Parse some table related expressions | |
-- @tparam Scope.Scope scope The current scope | |
-- @tparam boolean onlyDotColon Only allow '.' or ':' nodes | |
-- @treturn Node The resulting node | |
function ParseSuffixedExpr(scope, onlyDotColon) | |
--base primary expression | |
local prim = ParsePrimaryExpr(scope) | |
while true do | |
local tokenList = {} | |
if tok.IsSymbol('.') or tok.IsSymbol(':') then | |
local symb = tok.Get(tokenList).Data | |
if not tok.Is('Ident') then | |
GenerateError("<Ident> expected.") | |
end | |
local id = tok.Get(tokenList) | |
prim = { | |
AstType = 'MemberExpr', | |
Base = prim, | |
Indexer = symb, | |
Ident = id, | |
Tokens = tokenList, | |
} | |
elseif not onlyDotColon and tok.ConsumeSymbol('[', tokenList) then | |
local ex = ParseExpr(scope) | |
if not tok.ConsumeSymbol(']', tokenList) then | |
GenerateError("`]` expected.") | |
end | |
prim = { | |
AstType = 'IndexExpr', | |
Base = prim, | |
Index = ex, | |
Tokens = tokenList, | |
} | |
elseif not onlyDotColon and tok.ConsumeSymbol('(', tokenList) then | |
local args = {} | |
while not tok.ConsumeSymbol(')', tokenList) do | |
args[#args+1] = ParseExpr(scope) | |
if not tok.ConsumeSymbol(',', tokenList) then | |
if tok.ConsumeSymbol(')', tokenList) then | |
break | |
else | |
GenerateError("`)` Expected.") | |
end | |
end | |
end | |
prim = { | |
AstType = 'CallExpr', | |
Base = prim, | |
Arguments = args, | |
Tokens = tokenList, | |
} | |
elseif not onlyDotColon and tok.Is('String') then | |
--string call | |
prim = { | |
AstType = 'StringCallExpr', | |
Base = prim, | |
Arguments = { tok.Get(tokenList) }, | |
Tokens = tokenList, | |
} | |
elseif not onlyDotColon and tok.IsSymbol('{') then | |
--table call | |
local ex = ParseSimpleExpr(scope) | |
-- FIX: ParseExpr(scope) parses the table AND and any following binary expressions. | |
-- We just want the table | |
prim = { | |
AstType = 'TableCallExpr', | |
Base = prim, | |
Arguments = { ex }, | |
Tokens = tokenList, | |
} | |
else | |
break | |
end | |
end | |
return prim | |
end | |
--- Parse a simple expression (strings, numbers, booleans, varargs) | |
-- @tparam Scope.Scope scope The current scope | |
-- @treturn Node The resulting node | |
function ParseSimpleExpr(scope) | |
local tokenList = {} | |
local next = tok.Peek() | |
local type = next.Type | |
if type == 'Number' then | |
return { | |
AstType = 'NumberExpr', | |
Value = tok.Get(tokenList), | |
Tokens = tokenList, | |
} | |
elseif type == 'String' then | |
return { | |
AstType = 'StringExpr', | |
Value = tok.Get(tokenList), | |
Tokens = tokenList, | |
} | |
elseif type == 'Keyword' then | |
local data = next.Data | |
if data == 'nil' then | |
tok.Get(tokenList) | |
return { | |
AstType = 'NilExpr', | |
Tokens = tokenList, | |
} | |
elseif data == 'false' or data == 'true' then | |
return { | |
AstType = 'BooleanExpr', | |
Value = (tok.Get(tokenList).Data == 'true'), | |
Tokens = tokenList, | |
} | |
elseif data == 'function' then | |
tok.Get(tokenList) | |
local func = ParseFunctionArgsAndBody(scope, tokenList) | |
func.IsLocal = true | |
return func | |
end | |
elseif type == 'Symbol' then | |
local data = next.Data | |
if data == '...' then | |
tok.Get(tokenList) | |
return { | |
AstType = 'DotsExpr', | |
Tokens = tokenList, | |
} | |
elseif data == '{' then | |
tok.Get(tokenList) | |
local entryList = {} | |
local v = { | |
AstType = 'ConstructorExpr', | |
EntryList = entryList, | |
Tokens = tokenList, | |
} | |
while true do | |
if tok.IsSymbol('[', tokenList) then | |
--key | |
tok.Get(tokenList) | |
local key = ParseExpr(scope) | |
if not tok.ConsumeSymbol(']', tokenList) then | |
GenerateError("`]` Expected") | |
end | |
if not tok.ConsumeSymbol('=', tokenList) then | |
GenerateError("`=` Expected") | |
end | |
local value = ParseExpr(scope) | |
entryList[#entryList+1] = { | |
Type = 'Key', | |
Key = key, | |
Value = value, | |
} | |
elseif tok.Is('Ident') then | |
--value or key | |
local lookahead = tok.Peek(1) | |
if lookahead.Type == 'Symbol' and lookahead.Data == '=' then | |
--we are a key | |
local key = tok.Get(tokenList) | |
if not tok.ConsumeSymbol('=', tokenList) then | |
GenerateError("`=` Expected") | |
end | |
local value = ParseExpr(scope) | |
entryList[#entryList+1] = { | |
Type = 'KeyString', | |
Key = key.Data, | |
Value = value, | |
} | |
else | |
--we are a value | |
local value = ParseExpr(scope) | |
entryList[#entryList+1] = { | |
Type = 'Value', | |
Value = value, | |
} | |
end | |
elseif tok.ConsumeSymbol('}', tokenList) then | |
break | |
else | |
--value | |
local value = ParseExpr(scope) | |
entryList[#entryList+1] = { | |
Type = 'Value', | |
Value = value, | |
} | |
end | |
if tok.ConsumeSymbol(';', tokenList) or tok.ConsumeSymbol(',', tokenList) then | |
--all is good | |
elseif tok.ConsumeSymbol('}', tokenList) then | |
break | |
else | |
GenerateError("`}` or table entry Expected") | |
end | |
end | |
return v | |
end | |
end | |
return ParseSuffixedExpr(scope) | |
end | |
local unopprio = 8 | |
local priority = { | |
['+'] = {6,6}, | |
['-'] = {6,6}, | |
['%'] = {7,7}, | |
['/'] = {7,7}, | |
['*'] = {7,7}, | |
['^'] = {10,9}, | |
['..'] = {5,4}, | |
['=='] = {3,3}, | |
['<'] = {3,3}, | |
['<='] = {3,3}, | |
['~='] = {3,3}, | |
['>'] = {3,3}, | |
['>='] = {3,3}, | |
['and'] = {2,2}, | |
['or'] = {1,1}, | |
} | |
--- Parse an expression | |
-- @tparam Skcope.Scope scope The current scope | |
-- @tparam int level Current level (Optional) | |
-- @treturn Node The resulting node | |
function ParseExpr(scope, level) | |
level = level or 0 | |
--base item, possibly with unop prefix | |
local exp | |
if unops[tok.Peek().Data] then | |
local tokenList = {} | |
local op = tok.Get(tokenList).Data | |
exp = ParseExpr(scope, unopprio) | |
local nodeEx = { | |
AstType = 'UnopExpr', | |
Rhs = exp, | |
Op = op, | |
OperatorPrecedence = unopprio, | |
Tokens = tokenList, | |
} | |
exp = nodeEx | |
else | |
exp = ParseSimpleExpr(scope) | |
end | |
--next items in chain | |
while true do | |
local prio = priority[tok.Peek().Data] | |
if prio and prio[1] > level then | |
local tokenList = {} | |
local op = tok.Get(tokenList).Data | |
local rhs = ParseExpr(scope, prio[2]) | |
local nodeEx = { | |
AstType = 'BinopExpr', | |
Lhs = exp, | |
Op = op, | |
OperatorPrecedence = prio[1], | |
Rhs = rhs, | |
Tokens = tokenList, | |
} | |
exp = nodeEx | |
else | |
break | |
end | |
end | |
return exp | |
end | |
--- Parse a statement (if, for, while, etc...) | |
-- @tparam Scope.Scope scope The current scope | |
-- @treturn Node The resulting node | |
local function ParseStatement(scope) | |
local stat = nil | |
local tokenList = {} | |
local next = tok.Peek() | |
if next.Type == "Keyword" then | |
local type = next.Data | |
if type == 'if' then | |
tok.Get(tokenList) | |
--setup | |
local clauses = {} | |
local nodeIfStat = { | |
AstType = 'IfStatement', | |
Clauses = clauses, | |
} | |
--clauses | |
repeat | |
local nodeCond = ParseExpr(scope) | |
if not tok.ConsumeKeyword('then', tokenList) then | |
GenerateError("`then` expected.") | |
end | |
local nodeBody = ParseStatementList(scope) | |
clauses[#clauses+1] = { | |
Condition = nodeCond, | |
Body = nodeBody, | |
} | |
until not tok.ConsumeKeyword('elseif', tokenList) | |
--else clause | |
if tok.ConsumeKeyword('else', tokenList) then | |
local nodeBody = ParseStatementList(scope) | |
clauses[#clauses+1] = { | |
Body = nodeBody, | |
} | |
end | |
--end | |
if not tok.ConsumeKeyword('end', tokenList) then | |
GenerateError("`end` expected.") | |
end | |
nodeIfStat.Tokens = tokenList | |
stat = nodeIfStat | |
elseif type == 'while' then | |
tok.Get(tokenList) | |
--condition | |
local nodeCond = ParseExpr(scope) | |
--do | |
if not tok.ConsumeKeyword('do', tokenList) then | |
return GenerateError("`do` expected.") | |
end | |
--body | |
local nodeBody = ParseStatementList(scope) | |
--end | |
if not tok.ConsumeKeyword('end', tokenList) then | |
GenerateError("`end` expected.") | |
end | |
--return | |
stat = { | |
AstType = 'WhileStatement', | |
Condition = nodeCond, | |
Body = nodeBody, | |
Tokens = tokenList, | |
} | |
elseif type == 'do' then | |
tok.Get(tokenList) | |
--do block | |
local nodeBlock = ParseStatementList(scope) | |
if not tok.ConsumeKeyword('end', tokenList) then | |
GenerateError("`end` expected.") | |
end | |
stat = { | |
AstType = 'DoStatement', | |
Body = nodeBlock, | |
Tokens = tokenList, | |
} | |
elseif type == 'for' then | |
tok.Get(tokenList) | |
--for block | |
if not tok.Is('Ident') then | |
GenerateError("<ident> expected.") | |
end | |
local baseVarName = tok.Get(tokenList) | |
if tok.ConsumeSymbol('=', tokenList) then | |
--numeric for | |
local forScope = Scope(scope) | |
local forVar = forScope:CreateLocal(baseVarName.Data) | |
local startEx = ParseExpr(scope) | |
if not tok.ConsumeSymbol(',', tokenList) then | |
GenerateError("`,` Expected") | |
end | |
local endEx = ParseExpr(scope) | |
local stepEx | |
if tok.ConsumeSymbol(',', tokenList) then | |
stepEx = ParseExpr(scope) | |
end | |
if not tok.ConsumeKeyword('do', tokenList) then | |
GenerateError("`do` expected") | |
end | |
local body = ParseStatementList(forScope) | |
if not tok.ConsumeKeyword('end', tokenList) then | |
GenerateError("`end` expected") | |
end | |
stat = { | |
AstType = 'NumericForStatement', | |
Scope = forScope, | |
Variable = forVar, | |
Start = startEx, | |
End = endEx, | |
Step = stepEx, | |
Body = body, | |
Tokens = tokenList, | |
} | |
else | |
--generic for | |
local forScope = Scope(scope) | |
local varList = { forScope:CreateLocal(baseVarName.Data) } | |
while tok.ConsumeSymbol(',', tokenList) do | |
if not tok.Is('Ident') then | |
GenerateError("for variable expected.") | |
end | |
varList[#varList+1] = forScope:CreateLocal(tok.Get(tokenList).Data) | |
end | |
if not tok.ConsumeKeyword('in', tokenList) then | |
GenerateError("`in` expected.") | |
end | |
local generators = {ParseExpr(scope)} | |
while tok.ConsumeSymbol(',', tokenList) do | |
generators[#generators+1] = ParseExpr(scope) | |
end | |
if not tok.ConsumeKeyword('do', tokenList) then | |
GenerateError("`do` expected.") | |
end | |
local body = ParseStatementList(forScope) | |
if not tok.ConsumeKeyword('end', tokenList) then | |
GenerateError("`end` expected.") | |
end | |
stat = { | |
AstType = 'GenericForStatement', | |
Scope = forScope, | |
VariableList = varList, | |
Generators = generators, | |
Body = body, | |
Tokens = tokenList, | |
} | |
end | |
elseif type == 'repeat' then | |
tok.Get(tokenList) | |
local body = ParseStatementList(scope) | |
if not tok.ConsumeKeyword('until', tokenList) then | |
GenerateError("`until` expected.") | |
end | |
local cond = ParseExpr(body.Scope) | |
stat = { | |
AstType = 'RepeatStatement', | |
Condition = cond, | |
Body = body, | |
Tokens = tokenList, | |
} | |
elseif type == 'function' then | |
tok.Get(tokenList) | |
if not tok.Is('Ident') then | |
GenerateError("Function name expected") | |
end | |
local name = ParseSuffixedExpr(scope, true) --true => only dots and colons | |
local func = ParseFunctionArgsAndBody(scope, tokenList) | |
func.IsLocal = false | |
func.Name = name | |
stat = func | |
elseif type == 'local' then | |
tok.Get(tokenList) | |
if tok.Is('Ident') then | |
local varList = { tok.Get(tokenList).Data } | |
while tok.ConsumeSymbol(',', tokenList) do | |
if not tok.Is('Ident') then | |
GenerateError("local var name expected") | |
end | |
varList[#varList+1] = tok.Get(tokenList).Data | |
end | |
local initList = {} | |
if tok.ConsumeSymbol('=', tokenList) then | |
repeat | |
initList[#initList+1] = ParseExpr(scope) | |
until not tok.ConsumeSymbol(',', tokenList) | |
end | |
--now patch var list | |
--we can't do this before getting the init list, because the init list does not | |
--have the locals themselves in scope. | |
for i, v in pairs(varList) do | |
varList[i] = scope:CreateLocal(v) | |
end | |
stat = { | |
AstType = 'LocalStatement', | |
LocalList = varList, | |
InitList = initList, | |
Tokens = tokenList, | |
} | |
elseif tok.ConsumeKeyword('function', tokenList) then | |
if not tok.Is('Ident') then | |
GenerateError("Function name expected") | |
end | |
local name = tok.Get(tokenList).Data | |
local localVar = scope:CreateLocal(name) | |
local func = ParseFunctionArgsAndBody(scope, tokenList) | |
func.Name = localVar | |
func.IsLocal = true | |
stat = func | |
else | |
GenerateError("local var or function def expected") | |
end | |
elseif type == '::' then | |
tok.Get(tokenList) | |
if not tok.Is('Ident') then | |
GenerateError('Label name expected') | |
end | |
local label = tok.Get(tokenList).Data | |
if not tok.ConsumeSymbol('::', tokenList) then | |
GenerateError("`::` expected") | |
end | |
stat = { | |
AstType = 'LabelStatement', | |
Label = label, | |
Tokens = tokenList, | |
} | |
elseif type == 'return' then | |
tok.Get(tokenList) | |
local exList = {} | |
if not tok.IsKeyword('end') then | |
-- Use PCall as this may produce an error | |
local st, firstEx = pcall(function() return ParseExpr(scope) end) | |
if st then | |
exList[1] = firstEx | |
while tok.ConsumeSymbol(',', tokenList) do | |
exList[#exList+1] = ParseExpr(scope) | |
end | |
end | |
end | |
stat = { | |
AstType = 'ReturnStatement', | |
Arguments = exList, | |
Tokens = tokenList, | |
} | |
elseif type == 'break' then | |
tok.Get(tokenList) | |
stat = { | |
AstType = 'BreakStatement', | |
Tokens = tokenList, | |
} | |
elseif type == 'goto' then | |
tok.Get(tokenList) | |
if not tok.Is('Ident') then | |
GenerateError("Label expected") | |
end | |
local label = tok.Get(tokenList).Data | |
stat = { | |
AstType = 'GotoStatement', | |
Label = label, | |
Tokens = tokenList, | |
} | |
end | |
end | |
if not stat then | |
--statementParseExpr | |
local suffixed = ParseSuffixedExpr(scope) | |
--assignment or call? | |
if tok.IsSymbol(',') or tok.IsSymbol('=') then | |
--check that it was not parenthesized, making it not an lvalue | |
if (suffixed.ParenCount or 0) > 0 then | |
GenerateError("Can not assign to parenthesized expression, is not an lvalue") | |
end | |
--more processing needed | |
local lhs = { suffixed } | |
while tok.ConsumeSymbol(',', tokenList) do | |
lhs[#lhs+1] = ParseSuffixedExpr(scope) | |
end | |
--equals | |
if not tok.ConsumeSymbol('=', tokenList) then | |
GenerateError("`=` Expected.") | |
end | |
--rhs | |
local rhs = {ParseExpr(scope)} | |
while tok.ConsumeSymbol(',', tokenList) do | |
rhs[#rhs+1] = ParseExpr(scope) | |
end | |
--done | |
stat = { | |
AstType = 'AssignmentStatement', | |
Lhs = lhs, | |
Rhs = rhs, | |
Tokens = tokenList, | |
} | |
elseif suffixed.AstType == 'CallExpr' or | |
suffixed.AstType == 'TableCallExpr' or | |
suffixed.AstType == 'StringCallExpr' | |
then | |
--it's a call statement | |
stat = { | |
AstType = 'CallStatement', | |
Expression = suffixed, | |
Tokens = tokenList, | |
} | |
else | |
GenerateError("Assignment Statement Expected") | |
end | |
end | |
if tok.IsSymbol(';') then | |
stat.Semicolon = tok.Get( stat.Tokens ) | |
end | |
return stat | |
end | |
--- Parse a a list of statements | |
-- @tparam Scope.Scope scope The current scope | |
-- @treturn Node The resulting node | |
function ParseStatementList(scope) | |
local body = {} | |
local nodeStatlist = { | |
Scope = Scope(scope), | |
AstType = 'Statlist', | |
Body = body, | |
Tokens = {}, | |
} | |
while not statListCloseKeywords[tok.Peek().Data] and not tok.IsEof() do | |
local nodeStatement = ParseStatement(nodeStatlist.Scope) | |
--stats[#stats+1] = nodeStatement | |
body[#body + 1] = nodeStatement | |
end | |
if tok.IsEof() then | |
local nodeEof = {} | |
nodeEof.AstType = 'Eof' | |
nodeEof.Tokens = { tok.Get() } | |
body[#body + 1] = nodeEof | |
end | |
--nodeStatlist.Body = stats | |
return nodeStatlist | |
end | |
return ParseStatementList(Scope()) | |
end | |
--- @export | |
return { LexLua = LexLua, ParseLua = ParseLua } | |
end | |
preload["howl.lexer.constants"] = function(...) | |
--- Lexer constants | |
-- @module howl.lexer.constants | |
local function createLookup(tbl) | |
for k,v in ipairs(tbl) do tbl[v] = k end | |
return tbl | |
end | |
return { | |
--- List of white chars | |
WhiteChars = createLookup { ' ', '\n', '\t', '\r' }, | |
--- Lookup of escape characters | |
EscapeLookup = { ['\r'] = '\\r', ['\n'] = '\\n', ['\t'] = '\\t', ['"'] = '\\"', ["'"] = "\\'" }, | |
--- Lookup of lower case characters | |
LowerChars = createLookup { | |
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', | |
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' | |
}, | |
--- Lookup of upper case characters | |
UpperChars = createLookup { | |
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', | |
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' | |
}, | |
--- Lookup of digits | |
Digits = createLookup { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }, | |
--- Lookup of hex digits | |
HexDigits = createLookup { | |
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', | |
'A', 'a', 'B', 'b', 'C', 'c', 'D', 'd', 'E', 'e', 'F', 'f' | |
}, | |
--- Lookup of valid symbols | |
Symbols = createLookup { '+', '-', '*', '/', '^', '%', ',', '{', '}', '[', ']', '(', ')', ';', '#' }, | |
--- Lookup of valid keywords | |
Keywords = createLookup { | |
'and', 'break', 'do', 'else', 'elseif', | |
'end', 'false', 'for', 'function', 'goto', 'if', | |
'in', 'local', 'nil', 'not', 'or', 'repeat', | |
'return', 'then', 'true', 'until', 'while', | |
}, | |
--- Keywords that end a block | |
StatListCloseKeywords = createLookup { 'end', 'else', 'elseif', 'until' }, | |
--- Unary operators | |
UnOps = createLookup { '-', 'not', '#' }, | |
} | |
end | |
preload["howl.files.Source"] = function(...) | |
--- A source location for a series of files. | |
-- This holds a list of inclusion and exclusion filters. | |
-- @classmod howl.files.Source | |
local assert = require "howl.lib.assert" | |
local class = require "howl.class" | |
local matcher = require "howl.files.matcher" | |
local mixin = require "howl.class.mixin" | |
local fs = require "howl.platform".fs | |
local insert = table.insert | |
local Source = class("howl.files.Source") | |
:include(mixin.configurable) | |
:include(mixin.filterable) | |
local function extractPattern(item) | |
local t = type(item) | |
if t == "function" or t == "string" then | |
return matcher.createMatcher(item) | |
elseif t == "table" and item.tag and item.predicate then | |
return item | |
elseif t == "table" and item.isInstanceOf and item:isInstanceOf(Source) then | |
return matcher.createMatcher(function(text) return item:matches(text) end) | |
else | |
return nil | |
end | |
end | |
local function append(destination, source, func, i) | |
local extracted = extractPattern(source) | |
local t = type(source) | |
if extracted then | |
insert(destination, extracted) | |
elseif t == "table" then | |
for i, item in ipairs(source) do | |
local extracted = extractPattern(item) | |
if extracted then | |
insert(destination, extracted) | |
else | |
error("bad item #" .. i .. " for " .. func .. " (expected pattern, got " .. type(item) .. ")") | |
end | |
end | |
else | |
error("bad argument #" .. i .. " for " .. func .. " (expected pattern, got " .. t .. ")") | |
end | |
end | |
local function matches(items, text) | |
for _, pattern in pairs(items) do | |
if pattern:match(text) then | |
return true | |
end | |
end | |
return false | |
end | |
function Source:initialize(allowEmpty, parent) | |
if allowEmpty == nil then allowEmpty = true end | |
self.parent = parent | |
self.children = {} | |
self.includes = {} | |
self.excludes = {} | |
self.allowEmpty = allowEmpty | |
end | |
function Source:from(path, configure) | |
assert.argType(path, "string", "from", 1) | |
path = fs.normalise(path) | |
local source = self.children[path] | |
if not source then | |
source = self.class(true, self) | |
self.children[path] = source | |
self.allowEmpty = false | |
end | |
if configure ~= nil then | |
return source:configureWith(configure) | |
else | |
return source | |
end | |
end | |
function Source:include(...) | |
local n = select('#', ...) | |
local args = {...} | |
for i = 1, n do | |
append(self.includes, args[i], "include", i) | |
end | |
return self | |
end | |
function Source:exclude(...) | |
local n = select('#', ...) | |
local args = {...} | |
for i = 1, n do | |
append(self.excludes, args[i], "exclude", i) | |
end | |
return self | |
end | |
function Source:excluded(text) | |
if matches(self.excludes, text) then | |
return true | |
elseif self.parent then | |
-- FIXME: Combine this path | |
return self.parent:excluded(text) | |
else | |
return false | |
end | |
end | |
function Source:included(text) | |
if #self.includes == 0 then | |
return self.allowEmpty | |
else | |
return matches(self.includes, text) | |
end | |
end | |
function Source:configure(item) | |
assert.argType(item, "table", "configure", 1) | |
-- TODO: Ensure other keys aren't passed | |
-- TODO: Fix passing other source instances | |
if item.include ~= nil then self:include(item.include) end | |
if item.exclude ~= nil then self:exclude(item.exclude) end | |
if item.with ~= nil then | |
assert.type(item.with, "table", "expected table for with, got %s") | |
for _, v in ipairs(item.with) do | |
self:with(v) | |
end | |
end | |
end | |
function Source:matches(text) | |
return self:included(text) and not self:excluded(text) | |
end | |
function Source:hasFiles() | |
if self.allowEmpty or #self.includes > 0 then return true end | |
for _, source in pairs(self.children) do | |
if source:hasFiles() then return true end | |
end | |
return false | |
end | |
function Source:gatherFiles(root, includeDirectories, outList) | |
if not outList then outList = {} end | |
for dir, source in pairs(self.children) do | |
local path = fs.combine(root, dir) | |
source:gatherFiles(path, includeDirectories, outList) | |
end | |
if self.allowEmpty or #self.includes > 0 then | |
-- I lied. Its a stack | |
local queue, queueN = { root }, 1 | |
local n = #outList | |
while queueN > 0 do | |
local path = queue[queueN] | |
local relative = path | |
if root ~= "" then relative = relative:sub(#root + 2) end | |
queueN = queueN - 1 | |
if fs.isDir(path) then | |
if not self:excluded(relative) then | |
if includeDirectories and self:included(relative) then | |
n = n + 1 | |
outList[n] = self:buildFile(path, relative) | |
end | |
for _, v in ipairs(fs.list(path)) do | |
queueN = queueN + 1 | |
queue[queueN] = fs.combine(path, v) | |
end | |
end | |
elseif self:included(relative) and not self:excluded(relative) then | |
n = n + 1 | |
outList[n] = self:buildFile(path, relative) | |
end | |
end | |
end | |
return outList | |
end | |
function Source:buildFile(path, relative) | |
return { | |
path = path, | |
relative = relative, | |
name = relative, | |
} | |
end | |
return Source | |
end | |
preload["howl.files.matcher"] = function(...) | |
--- Used to create matchers for particular patterns | |
-- @module howl.files.matcher | |
local utils = require "howl.lib.utils" | |
-- Matches with * and ? removed | |
local basicMatches = { | |
["^"] = "%^", ["$"] = "%$", ["("] = "%(", [")"] = "%)", | |
["%"] = "%%", ["."] = "%.", ["["] = "%[", ["]"] = "%]", | |
["+"] = "%+", ["-"] = "%-", ["\0"] = "%z", | |
} | |
local wildMatches = { | |
-- ["*"] = "([^\\]+)", | |
-- ["?"] = "([^\\])", | |
["*"] = "(.*)" | |
} | |
for k,v in pairs(basicMatches) do wildMatches[k] = v end | |
--- A resulting pattern | |
-- @table Pattern | |
-- @tfield string tag `pattern` or `normal` | |
-- @tfield (Pattern, string)->boolean match Predicate to check if this is a valid item | |
local function patternAction(self, text) return text:match(self.text) end | |
local function textAction(self, text) | |
return self.text == "" or self.text == text or text:sub(1, #self.text + 1) == self.text .. "/" | |
end | |
local function funcAction(self, text) return self.func(text) end | |
--- Create a matcher | |
-- @tparam string|function pattern Pattern to check against | |
-- @treturn Pattern | |
local function createMatcher(pattern) | |
local t = type(pattern) | |
if t == "string" then | |
local remainder = utils.startsWith(pattern, "pattern:") or utils.startsWith(pattern, "ptrn:") | |
if remainder then | |
return { tag = "pattern", text = remainder, match = patternAction } | |
end | |
if pattern:find("%*") then | |
local pattern = "^" .. pattern:gsub(".", wildMatches) .. "$" | |
return { tag = "pattern", text = pattern, match = patternAction } | |
end | |
return { tag = "text", text = pattern, match = textAction} | |
elseif t == "function" or (t == "table" and (getmetatable(pattern) or {}).__call) then | |
return { tag = "function", func = pattern, match = funcAction } | |
else | |
error("Expected string or function") | |
end | |
end | |
return { | |
createMatcher = createMatcher, | |
} | |
end | |
preload["howl.files.CopySource"] = function(...) | |
--- A source location for a series of files. | |
-- This holds a list of inclusion and exclusion filters. | |
-- @classmod howl.files.Source | |
local assert = require "howl.lib.assert" | |
local matcher = require "howl.files.matcher" | |
local mixin = require "howl.class.mixin" | |
local fs = require "howl.platform".fs | |
local Source = require "howl.files.Source" | |
local insert = table.insert | |
local CopySource = Source:subclass("howl.files.CopySource") | |
function CopySource:initialize(allowEmpty, parent) | |
Source.initialize(self, allowEmpty, parent) | |
self.renames = {} | |
self.modifiers = {} | |
end | |
function CopySource:configure(item) | |
assert.argType(item, "table", "configure", 1) | |
Source.configure(self, item) | |
if item.rename ~= nil then self:rename(item.rename) end | |
if item.modify ~= nil then self:modify(item.modify) end | |
end | |
function CopySource:rename(from, to) | |
local tyFrom, tyTo = type(from), type(to) | |
if tyFrom == "table" and to == nil then | |
for _, v in ipairs(from) do | |
self:rename(v) | |
end | |
elseif tyFrom == "function" and to == nil then | |
insert(self.renames, from) | |
elseif tyFrom == "string" and tyTo == "string" then | |
insert(self.renames, function(file) | |
return (file.name:gsub(from, to)) | |
end) | |
else | |
error("bad arguments for rename (expected table, function or string, string pair, got " .. tyFrom .. " and " .. tyTo .. ")", 2) | |
end | |
end | |
function CopySource:modify(modifier) | |
local ty = type(modifier) | |
if ty == "table" then | |
for _, v in ipairs(modifier) do | |
self:modify(v) | |
end | |
elseif ty == "function" then | |
insert(self.modifiers, modifier) | |
else | |
error("bad argument #1 for modify (expected table or function, got " .. ty .. ")", 2) | |
end | |
end | |
function CopySource:doMutate(file) | |
for _, modifier in ipairs(self.modifiers) do | |
local contents = modifier(file) | |
if contents then file.contents = contents end | |
end | |
for _, renamer in ipairs(self.renames) do | |
local name = renamer(file) | |
if name then file.name = name end | |
end | |
if self.parent then | |
return self.parent:doMutate(file) | |
else | |
return file | |
end | |
end | |
function CopySource:buildFile(path, relative) | |
return self:doMutate { | |
path = path, | |
relative = relative, | |
name = relative, | |
contents = fs.read(path), | |
} | |
end | |
return CopySource | |
end | |
preload["howl.context"] = function(...) | |
--- Handles the whole Howl instance | |
-- @classmod howl.Context | |
local assert = require "howl.lib.assert" | |
local class = require "howl.class" | |
local mixin = require "howl.class.mixin" | |
local mediator = require "howl.lib.mediator" | |
local argparse = require "howl.lib.argparse" | |
local Logger = require "howl.lib.Logger" | |
local Manager = require "howl.packages.Manager" | |
local Context = class("howl.Context"):include(mixin.sealed) | |
--- Setup the main context | |
-- @tparam string root The project root of the directory | |
-- @tparam howl.lib.argparse args The argument parser | |
function Context:initialize(root, args) | |
assert.type(root, "string", "bad argument #1 for Context expected string, got %s") | |
assert.type(args, "table", "bad argument #2 for Context expected table, got %s") | |
self.root = root | |
self.out = "build" | |
self.mediator = mediator | |
self.arguments = argparse.Options(self.mediator, args) | |
self.logger = Logger(self) | |
self.packageManager = Manager(self) | |
self.modules = {} | |
end | |
--- Include a module in this context | |
-- @tparam string|table The module to include | |
function Context:include(module) | |
if type(module) ~= "table" then | |
module = require(module) | |
end | |
if self.modules[module.name] then | |
self.logger:warn(module.name .. " already included, skipping") | |
return | |
end | |
local data = { module = module, } | |
self.modules[module.name] = data | |
self.logger:verbose("Including " .. module.name .. ": " .. module.description) | |
if not module.applied then | |
module.applied = true | |
if module.apply then module.apply() end | |
end | |
if module.setup then module.setup(self, data) end | |
end | |
function Context:getModuleData(name) | |
return self.modules[name] | |
end | |
return Context | |
end | |
preload["howl.cli"] = function(...) | |
--- Core script for Howl | |
-- @script howl.cli | |
local loader = require "howl.loader" | |
local colored = require "howl.lib.colored" | |
local fs = require "howl.platform".fs | |
local howlFile, currentDirectory = loader.FindHowl() | |
-- TODO: Don't pass the error message as the current directory: construct mediator/arg parser another time. | |
local context = require "howl.context"(currentDirectory or fs.currentDir(), {...}) | |
local options = context.arguments | |
options | |
:Option "verbose" | |
:Alias "v" | |
:Description "Print verbose output" | |
options | |
:Option "time" | |
:Alias "t" | |
:Description "Display the time taken for tasks" | |
options | |
:Option "trace" | |
:Description "Print a stack trace on errors" | |
options | |
:Option "help" | |
:Alias "?" | |
:Alias "h" | |
:Description "Print this help" | |
context:include "howl.modules.dependencies.file" | |
context:include "howl.modules.dependencies.task" | |
context:include "howl.modules.list" | |
context:include "howl.modules.plugins" | |
context:include "howl.modules.packages.file" | |
context:include "howl.modules.packages.gist" | |
context:include "howl.modules.packages.pastebin" | |
context:include "howl.modules.tasks.clean" | |
context:include "howl.modules.tasks.gist" | |
context:include "howl.modules.tasks.minify" | |
context:include "howl.modules.tasks.pack" | |
context:include "howl.modules.tasks.require" | |
-- Setup Tasks | |
local taskList = options:Arguments() | |
local function setHelp() | |
if options:Get "help" then | |
taskList = { "help" } | |
end | |
end | |
context.mediator:subscribe({ "ArgParse", "changed" }, setHelp) | |
setHelp() | |
-- Locate the howl file | |
if not howlFile then | |
if #taskList == 1 and taskList[1] == "help" then | |
colored.writeColor("yellow", "Howl") | |
colored.printColor("lightGrey", " is a simple build system for Lua") | |
colored.printColor("grey", "You can read the full documentation online: https://github.com/SquidDev-CC/Howl/wiki/") | |
colored.printColor("white", (([[ | |
The key thing you are missing is a HowlFile. This can be "Howlfile" or "Howlfile.lua". | |
Then you need to define some tasks. Maybe something like this: | |
]]):gsub("\t", ""):gsub("\n+$", ""))) | |
colored.printColor("magenta", 'Tasks:minify "minify" {') | |
colored.printColor("magenta", ' input = "build/Howl.lua",') | |
colored.printColor("magenta", ' output = "build/Howl.min.lua",') | |
colored.printColor("magenta", '}') | |
colored.printColor("white", "Now just run '" .. fs.getName(fs.currentProgram()) .. " minify'!") | |
colored.printColor("orange", "\nOptions:") | |
options:Help(" ") | |
elseif #taskList == 0 then | |
error(currentDirectory .. " Use " .. fs.getName(fs.currentProgram()) .. " --help to dislay usage.", 0) | |
else | |
error(currentDirectory, 0) | |
end | |
return | |
end | |
context.logger:verbose("Found HowlFile at " .. fs.combine(currentDirectory, howlFile)) | |
local tasks, environment = loader.SetupTasks(context, howlFile) | |
-- Basic list tasks | |
tasks:Task "list" (function() | |
tasks:listTasks() | |
end):description "Lists all the tasks" | |
tasks:Task "help" (function() | |
print("Howl [options] [task]") | |
colored.printColor("orange", "Tasks:") | |
tasks:listTasks(" ") | |
colored.printColor("orange", "\nOptions:") | |
options:Help(" ") | |
end):description "Print out a detailed usage for Howl" | |
-- If no other task exists run this | |
tasks:Default(function() | |
context.logger:error("No default task exists.") | |
context.logger:verbose("Use 'Tasks:Default' to define a default task") | |
colored.printColor("orange", "Choose from: ") | |
tasks:listTasks(" ") | |
end) | |
environment.dofile(fs.combine(currentDirectory, howlFile)) | |
if not tasks:setup() then | |
error("Error setting up tasks", 0) | |
end | |
-- Run the task | |
if not tasks:RunMany(taskList) then | |
error("Error running tasks", 0) | |
end | |
end | |
preload["howl.class.mixin"] = function(...) | |
--- Various mixins for the class library | |
-- @module howl.class.mixin | |
local assert = require "howl.lib.assert" | |
local rawset = rawset | |
local mixins = {} | |
--- Prevent subclassing a class | |
mixins.sealed = { | |
static = { | |
subclass = function(self, name) | |
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") | |
assert(type(name) == "string", "You must provide a name(string) for your class") | |
error("Cannot subclass '" .. tostring(self) .. "' (attempting to create '" .. name .. "')", 2) | |
end, | |
} | |
} | |
mixins.curry = { | |
curry = function(self, name) | |
assert.type(self, "table", "Bad argument #1 to class:curry (expected table, got %s)") | |
assert.type(name, "string", "Bad argument #2 to class:curry (expected string, got %s)") | |
local func = self[name] | |
assert.type(func, "function", "No such function " .. name) | |
return function(...) return func(self, ...) end | |
end, | |
__div = function(self, name) return self:curry(name) end, | |
} | |
mixins.configurable = { | |
configureWith = function(self, arg) | |
local t = type(arg) | |
if t == "table" then | |
self:configure(arg) | |
return self | |
elseif t == "function" then | |
arg(self) | |
return self | |
else | |
error("Expected table or function, got " .. type(arg), 2) | |
end | |
return self | |
end, | |
__call = function(self, ...) return self:configureWith(...) end, | |
} | |
mixins.filterable = { | |
__add = function(self, ...) return self:include(...) end, | |
__sub = function(self, ...) return self:exclude(...) end, | |
with = function(self, ...) return self:configure(...) end, | |
} | |
function mixins.delegate(name, keys) | |
local out = {} | |
for _, key in ipairs(keys) do | |
out[key] = function(self, ...) | |
local object = self[name] | |
return object[key](object, ...) | |
end | |
end | |
return out | |
end | |
mixins.optionGroup = { | |
static = { | |
addOption = function(self, key) | |
local func = function(self, value) | |
if value == nil then value = true end | |
self.options[key] = value | |
return self | |
end | |
self[key:gsub("^%l", string.upper)] = func | |
self[key] = func | |
if not rawget(self.static, "options") then | |
local options = {} | |
self.static.options = options | |
local parent = self.super and self.super.static.options | |
-- TODO: Copy instead. Also propagate to children below | |
if parent then setmetatable(options, { __index = parent } ) end | |
end | |
self.static.options[key] = true | |
return self | |
end, | |
addOptions = function(self, names) | |
for i = 1, #names do | |
self:addOption(names[i]) | |
end | |
return self | |
end, | |
}, | |
configure = function(self, item) | |
assert.argType(item, "table", "configure", 1) | |
local class = self.class | |
local options = class.options | |
while class and not options do | |
options = class.options | |
class = class.super | |
end | |
if not options then return end | |
for k, v in pairs(item) do | |
if options[k] then | |
self[k](self, v) | |
end | |
end | |
end, | |
__newindex = function(self, key, value) | |
if self.class.options and self.class.options[key] then -- TODO: This is being applied to superclasses | |
self[key](self, value) | |
else | |
rawset(self, key, value) | |
end | |
end | |
} | |
return mixins | |
end | |
preload["howl.class"] = function(...) | |
--- An OOP library for Lua | |
-- @module howl.class | |
local middleclass = { | |
_VERSION = 'middleclass v4.0.0', | |
_DESCRIPTION = 'Object Orientation for Lua', | |
_URL = 'https://github.com/kikito/middleclass', | |
_LICENSE = [[ | |
MIT LICENSE | |
Copyright (c) 2011 Enrique García Cota | |
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. | |
]] | |
} | |
local function _createIndexWrapper(aClass, f) | |
if f == nil then | |
return aClass.__instanceDict | |
else | |
return function(self, name) | |
local value = aClass.__instanceDict[name] | |
if value ~= nil then | |
return value | |
elseif type(f) == "function" then | |
return (f(self, name)) | |
else | |
return f[name] | |
end | |
end | |
end | |
end | |
local function _propagateInstanceMethod(aClass, name, f) | |
f = name == "__index" and _createIndexWrapper(aClass, f) or f | |
aClass.__instanceDict[name] = f | |
for subclass in pairs(aClass.subclasses) do | |
if rawget(subclass.__declaredMethods, name) == nil then | |
_propagateInstanceMethod(subclass, name, f) | |
end | |
end | |
end | |
local function _declareInstanceMethod(aClass, name, f) | |
aClass.__declaredMethods[name] = f | |
if f == nil and aClass.super then | |
f = aClass.super.__instanceDict[name] | |
end | |
_propagateInstanceMethod(aClass, name, f) | |
end | |
local function _tostring(self) return "class " .. self.name end | |
local function _call(self, ...) return self:new(...) end | |
local function _createClass(name, super) | |
local dict = {} | |
dict.__index = dict | |
local aClass = { | |
name = name, super = super, static = {}, | |
__instanceDict = dict, __declaredMethods = {}, | |
subclasses = setmetatable({}, {__mode='k'}) | |
} | |
if super then | |
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) or super.static[k] end }) | |
else | |
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) end }) | |
end | |
setmetatable(aClass, { | |
__index = aClass.static, __tostring = _tostring, | |
__call = _call, __newindex = _declareInstanceMethod | |
}) | |
return aClass | |
end | |
local function _includeMixin(aClass, mixin) | |
assert(type(mixin) == 'table', "mixin must be a table") | |
for name,method in pairs(mixin) do | |
if name ~= "included" and name ~= "static" then aClass[name] = method end | |
end | |
for name,method in pairs(mixin.static or {}) do | |
aClass.static[name] = method | |
end | |
if type(mixin.included)=="function" then mixin:included(aClass) end | |
return aClass | |
end | |
local DefaultMixin = { | |
__tostring = function(self) return "instance of " .. tostring(self.class) end, | |
initialize = function(self, ...) end, | |
isInstanceOf = function(self, aClass) | |
return | |
type(self) == 'table' and | |
type(self.class) == 'table' and | |
type(aClass) == 'table' and | |
( aClass == self.class or | |
type(aClass.isSubclassOf) == 'function' and | |
self.class:isSubclassOf(aClass) | |
) | |
end, | |
static = { | |
allocate = function(self) | |
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'") | |
return setmetatable({ class = self }, self.__instanceDict) | |
end, | |
new = function(self, ...) | |
assert(type(self) == 'table', "Make sure that you are using 'Class:new' instead of 'Class.new'") | |
local instance = self:allocate() | |
instance:initialize(...) | |
return instance | |
end, | |
subclass = function(self, name) | |
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") | |
assert(type(name) == "string", "You must provide a name(string) for your class") | |
local subclass = _createClass(name, self) | |
for methodName, f in pairs(self.__instanceDict) do | |
_propagateInstanceMethod(subclass, methodName, f) | |
end | |
subclass.initialize = function(instance, ...) return self.initialize(instance, ...) end | |
self.subclasses[subclass] = true | |
self:subclassed(subclass) | |
return subclass | |
end, | |
subclassed = function(self, other) end, | |
isSubclassOf = function(self, other) | |
return | |
type(other) == 'table' and | |
type(self) == 'table' and | |
type(self.super) == 'table' and | |
(self.super == other or | |
type(self.super.isSubclassOf) == 'function' and | |
self.super:isSubclassOf(other) ) | |
end, | |
include = function(self, ...) | |
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'") | |
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end | |
return self | |
end | |
} | |
} | |
return function(name, super) | |
assert(type(name) == 'string', "A name (string) is needed for the new class") | |
return super and super:subclass(name) or _includeMixin(_createClass(name), DefaultMixin) | |
end | |
end | |
if not shell or type(... or nil) == 'table' then | |
local tbl = ... or {} | |
tbl.require = require tbl.preload = preload | |
return tbl | |
else | |
return preload["howl.cli"](...) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local e={}local t,a,o=require,{},{startup=e} | |
local function i(n)local s=o[n] | |
if s~=nil then if s==e then | |
error("loop or previous error loading module '"..n.. | |
"'",2)end;return s end;o[n]=e;local h=a[n]if h then s=h(n)elseif t then s=t(n)else | |
error("cannot load '"..n.."'",2)end;if s==nil then s=true end;o[n]=s;return s end | |
a["howl.tasks.Task"]=function(...)local n=i"howl.lib.assert"local s=i"howl.class" | |
local h=i"howl.lib.colored"local r=i"howl.class.mixin"local d=i"howl.platform".os | |
local l=i"howl.lib.utils"local u=table.insert | |
local function c(f,w)local y=l.parsePattern(f,true) | |
local p=l.parsePattern(w)local v=y.Type | |
n(v==p.Type,"Both from and to must be the same type "..v.." and "..y.Type)return{Type=v,From=y.Text,To=p.Text}end | |
local m=s("howl.tasks.Task"):include(r.configurable):include(r.optionGroup):addOptions{"description"} | |
function m:initialize(f,w,y)n.argType(f,"string","Task",1)if type(w)=="function"then y=w | |
w={}end;self.options={}self.name=f;self.action=y | |
self.dependencies={}self.maps={}self.produces={}if w then self:depends(w)end end | |
function m.static:addDependency(s,f) | |
local function w(y,...) | |
if | |
select('#',...)==1 and type(...)=="table"and(# (...)>0 or next(...)==nil)then local p=...for v=1,#p do u(y.dependencies,s(y,p[v]))end else | |
u(y.dependencies,s(y,...))end;return y end;self[f]=w;self[f:gsub("^%l",string.upper)]=w;return | |
self end;function m:setup(f,w)end | |
function m:Produces(f) | |
if type(f)=="table"then local w=self.produces;for y,f in ipairs(f)do | |
table.insert(w,f)end else table.insert(self.produces,f)end;return self end | |
function m:Maps(f,w)table.insert(self.maps,c(f,w))return self end;function m:Action(f)self.action=f;return self end | |
function m:runAction(f,...)if self.action then return | |
self.action(self,f,...)else return true end end | |
function m:Run(f,...)local w=false | |
if#self.dependencies==0 then w=true else for k,q in ipairs(self.dependencies)do if | |
q:resolve(f.env,f)then w=true end end end;if not w then return false end;for k,q in ipairs(self.produces)do | |
f.filesProduced[q]=true end;local y={...}local p=""if#y>0 then local k={}for q,j in ipairs(y)do | |
table.insert(k,tostring(j))end | |
p=" ("..table.concat(k,", ")..")"end;f.env.logger:info("Running %s", | |
self.name..p)local v=d.clock() | |
local b,g=true,nil | |
if f.Traceback then | |
xpcall(function()self:runAction(f.env,unpack(y))end,function(k) | |
for q=5,15 | |
do local j,g=pcall(function()error("",q)end)if | |
k:match("Howlfile")then break end;k=k.."\n "..g end;g=k;b=false end)else b,g=pcall(self.runAction,self,f.env,...)end | |
if b then | |
f.env.logger:success("%s finished",self.name)else | |
f.env.logger:error("%s: %s",self.name,g or"no message")error("Error running tasks",0)end;if f.ShowTime then | |
print(" ","Took "..d.clock()-v.."s")end;return true end;return m end | |
a["howl.tasks.Runner"]=function(...)local n=i"howl.class"local s=i"howl.lib.colored" | |
local h=i"howl.tasks.Context"local r=i"howl.class.mixin"local d=i"howl.platform".os | |
local l=i"howl.tasks.Task" | |
local u=n("howl.tasks.Runner"):include(r.sealed) | |
function u:initialize(c)self.tasks={}self.default=nil;self.env=c end | |
function u:setup() | |
for c,m in pairs(self.tasks)do m:setup(self.env,self)end;if self.env.logger.hasError then return false end;for c,m in | |
pairs(self.tasks)do | |
for c,f in ipairs(m.dependencies)do f:setup(self.env,self)end end;if self.env.logger.hasError then return | |
false end;return true end;function u:Task(c) | |
return function(m,f)return self:addTask(c,m,f)end end;function u:addTask(c,m,f) | |
return self:injectTask(l(c,m,f))end;function u:injectTask(c,m) | |
self.tasks[m or c.name]=c;return c end | |
function u:Default(c)local m | |
if c==nil then self.default=nil elseif type(c)== | |
"string"then self.default=self.tasks[c]if not self.default then | |
error("Cannot find task "..c)end else self.default=l("<default>",{},c)end;return self end;function u:Run(c)return self:RunMany({c})end | |
function u:RunMany(c) | |
local m=d.clock()local f=true;local w=h(self)if#c==0 then w:Start()else | |
for y,p in ipairs(c)do f=w:Start(p)end end;if w.ShowTime then | |
s.printColor("orange","Took ".. | |
d.clock()-m.."s in total")end;return f end;return u end | |
a["howl.tasks.OptionTask"]=function(...)local n=i"howl.lib.assert"local s=i"howl.class.mixin" | |
local h=rawset;local r=i"howl.tasks.Task" | |
local d=r:subclass("howl.tasks.OptionTask"):include(s.configurable) | |
function d:initialize(l,u,c,m)r.initialize(self,l,u,m)self.options={} | |
self.optionKeys={}for f,w in ipairs(c or{})do self:addOption(w)end end | |
function d:addOption(l)local u=self.options | |
local c=function(m,f)if f==nil then f=true end;u[l]=f;return m end;self[l:gsub("^%l",string.upper)]=c;self[l]=c | |
self.optionKeys[l]=true end;function d:configure(l)n.argType(l,"table","configure",1) | |
for u,c in pairs(l)do if | |
self.optionKeys[u]then self.options[u]=c else end end end;return d end | |
a["howl.tasks.Dependency"]=function(...)local n=i"howl.class" | |
local s=n("howl.tasks.Dependency") | |
function s:initialize(h)if self.class==s then | |
error("Cannot create instance of abstract class "..tostring(s),2)end;self.task=h end;function s:setup(h,r) | |
error("setup has not been overridden in "..self.class,2)end;function s:resolve(h,r) | |
error("resolve has not been overridden in "..self.class,2)end;return s end | |
a["howl.tasks.Context"]=function(...)local n=i"howl.class"local s=i"howl.platform".fs | |
local h=i"howl.class.mixin"local r=i"howl.platform" | |
local d=n("howl.tasks.Context"):include(h.sealed) | |
function d:initialize(u)self.ran={}self.filesProduced={}self.tasks=u.tasks | |
self.default=u.default;self.Traceback=u.Traceback;self.ShowTime=u.ShowTime;self.env=u.env | |
self:BuildCache()end | |
function d:DoRequire(u,c)if self.filesProduced[u]then return true end | |
local m=self.producesCache[u] | |
if m then self.filesProduced[u]=true;return self:Run(m)end;m=self.normalMapsCache[u]local f,w;local y=u;if m then | |
self.filesProduced[u]=true;w=m.Name;f=m.Pattern.From end | |
for p,v in | |
pairs(self.patternMapsCache)do if u:match(p)then self.filesProduced[u]=true;w=v.Name | |
f=u:gsub(p,v.Pattern.From)break end end | |
if w then local p=self:DoRequire(f,true)if not p then | |
if not c then self.env.logger:error( | |
"Cannot find '"..f.."'")end;return false end | |
return self:Run(w,f,y)end;if s.exists(s.combine(self.env.root,u))then | |
self.filesProduced[u]=true;return true end;if not c then | |
self.env.logger:error( | |
"Cannot find a task matching '"..u.."'")end;return false end;local function l(u,c)local m=#u;if#u~=#c then return false end | |
for f=1,m do if u[f]~=c[f]then return false end end;return true end | |
function d:Run(u,...) | |
local c=u | |
if type(u)=="string"then c=self.tasks[u]if not c then | |
error("Cannot find a task called '"..u.."'")return false end elseif not c or not c.Run then | |
error( | |
"Cannot call task "..tostring(c).." as it has no 'Run' method")return false end;local m={...}local f=self.ran[c] | |
if not f then f={m}self.ran[c]=f else for w=1,#f do if l(m,f[w])then | |
return false end end;f[#f+1]=m end;r.refreshYield()return c:Run(self,...)end;d.run=d.Run | |
function d:Start(u)local c | |
if u then c=self.tasks[u]else c=self.default;u="<default>"end;if not c then | |
self.env.logger:error("Cannot find a task called '"..u.."'")return false end;return self:Run(c)end | |
function d:BuildCache()local u={}local c={}local m={}self.producesCache=u;self.patternMapsCache=c | |
self.normalMapsCache=m | |
for f,w in pairs(self.tasks)do local y=w.produces;if y then | |
for v,b in ipairs(y)do local g=u[b]if g then | |
error(string.format("Both '%s' and '%s' produces '%s'",g,f,b))end;u[b]=f end end | |
local p=w.maps | |
if p then | |
for v,b in ipairs(p)do | |
local g=(b.Type=="Pattern"and c or m)local k=b.To;local q=g[k]if q then | |
error(string.format("Both '%s' and '%s' match '%s'",q,f,k))end;g[k]={Name=f,Pattern=b}end end end;return self end;return d end | |
a["howl.scratchpad"]=function(...)local n=i"howl.lib.utils"local s=i"howl.lib.dump".dump | |
local h=i"howl.lib.colored".printColor;local r=n.parsePattern;local d=n.createLookup | |
local l={{name="input",provides=d{"foo.un.lua"}},{name="output",requires=d{"foo.min.lua"}},{name="minify",maps={{from=r("wild:*.lua",true),to=r("wild:*.min.lua")}}},{name="licence",maps={{from=r("wild:*.un.lua",true),to=r("wild:*.lua")}}}} | |
for f,w in pairs(l)do l[w.name]=w;if not w.maps then w.maps={}end | |
w.mapper=#w.maps>0;if not w.provides then w.provides={}end | |
if not w.requires then w.requires={}end end | |
local function u(f)local w={} | |
for y,p in ipairs(l)do | |
if p.provides[f]then w[#w+1]={task=p.name}end | |
for y,v in ipairs(p.maps)do | |
if v.to.Type=="Text"then if v.to.Text==f then | |
w[#w+1]={task=p.name,v.from.Text,f}end else if f:find(v.to.Text)then | |
w[#w+1]={task=p.name,f:gsub(v.to.Text,v.from.Text),f}end end end end;return w end | |
local function c(...)local f={}local w={}local y={} | |
local function p(b,g) | |
local k=b.task.."|"..table.concat(b,"|")local q=y[k] | |
if q then q.depth=math.min(q.depth,g)return q else b.depth=g;b.needed={} | |
b.solutions={} | |
b.name=b.task..": "..table.concat(b," \26 ")y[k]=b;w[#w+1]=b;return b end end | |
local function v(b,g)local b=p(b,g.depth+1)b.needed[#b.needed+1]=g;return b end | |
for b=1,select('#',...)do p({task=select(b,...)},1)end | |
while#w>0 do local b=table.remove(w,1)local g=l[b.task] | |
print("Task '"..b.name)if#b.needed>0 then print(" Needed for") | |
for k=1,#b.needed do h("lightGrey"," ".. | |
b.needed[k].name)end end | |
if b.depth>4 then | |
h("red"," Too deep")elseif#b.solutions>0 or | |
(#g.requires==0 and not g.mapper)then h("green"," Endpoint")f[#f+1]=b | |
for k=1,# | |
b.needed do local q=b.needed[k] | |
q.solutions[#q.solutions+1]=b;if#q.solutions==1 then w[#w+1]=q end end else | |
for k=1,#g.requires do local q=g.requires[k] | |
print(" Depends on '"..q.."'")local u=u(q)for k=1,#u do local j=v(u[k],b) | |
h("yellow"," Maybe: "..j.name)end end | |
if g.mapper then local k=b[1]print(" Depends on '"..k.."'") | |
local u=u(k)for q=1,#u do local j=v(u[q],b) | |
h("yellow"," Maybe: "..j.name)end end end end;return f end;local m=c("output")for f=1,#m do print(m[f].name)end end | |
a["howl.platform.oc"]=function(...)local n=i("filesystem")local s=i("term") | |
local h=i("component")local r=pcall(function()return h.internet end) | |
local d=i("internet")local l=h.gpu;local function u(b)local g=getSize(b)local k=n.open(b)local q=k:read(g)k:close() | |
return q end | |
local function c(b)local g=#b+2;local k,q={b},1;local j={}while q>0 do | |
local x=k[q]q=q-1 | |
if fs.isDir(x)then | |
for z,_ in ipairs(n.list(x))do q=q+1;k[q]=n.combine(x,_)end else j[x:sub(g)]=u(x)end end;return j end | |
local function m(b,g)for k,q in pairs(g)do write(n.combine(b,k),q)end end | |
local function f(b,g)local k=n.open(b,"w")local q,j=k:write(g)if not q then | |
io.stderr:write(j)end;k:close()end;local function w(b,g,k) | |
if not n.exists(b)then error("Cannot find "..g.." (looking for "..b..")", | |
k or 1)end end;local function y(b)local g=n.open(b) | |
local k=g:seek("end")g:close()return k end;local function p(b,g,k)if not r then | |
error("No internet card found",0)end;local q=""for j in d.request(b,g,k)do q=q..j end | |
return q end;local function v(b) | |
return function() | |
error(b.. | |
" has not been implemented for OpenComputers!",2)end end | |
return | |
{os={clock=os.clock,time=os.time,getEnv=os.getEnv},fs={combine=n.concat,normalise=n.canonical,getDir=n.path,getName=n.name,currentDir=shell.getWorkingDirectory,currentProgram=function()return | |
process.info().command end,read=u,write=f,readDir=c,writeDir=m,getSize=y,assertExists=w,exists=n.exists,isDir=n.isDir,list=n.list,makeDir=n.makeDir,delete=n.delete,move=n.move,copy=n.copy},term={setColor=l.setForeground,resetColor=function() | |
l.setForeground(colors.white)end,print=print,write=io.write},http={request=p},log=function() | |
return end,refreshYield=function()os.sleep(0)end}end | |
a["howl.platform.native"]=function(...)local n=string.char(27)..'[' | |
local s={white=97,orange=33,magenta=95,lightBlue=94,yellow=93,lime=92,pink=95,gray=90,grey=90,lightGray=37,lightGrey=37,cyan=96,purple=35,blue=36,brown=31,green=32,red=91,black=30}local function h(u)return | |
function()error(u.." is not implemented",2)end end | |
local r=i('pl.path')local d=i('pl.dir')local l=i('pl.file') | |
return | |
{fs={combine=r.join,normalise=r.normpath,getDir=r.dirname,getName=r.basename,currentDir=function() | |
return r.currentdir end,read=l.read,write=l.write,readDir=h("fs.readDir"),writeDir=h("fs.writeDir"),getSize=function(u) | |
local l=io:open(u,"r")local c=l:seek("end")l:close()return c end,assertExists=function(l) | |
if | |
not r.exists(l)then error("File does not exist")end end,exists=r.exists,isDir=r.isdir,list=function(d)local u={} | |
for r in r.dir(d)do u[#u+1]=r end;return u end,makeDir=d.makepath,delete=function(u)if r.isdir(u)then d.rmtree(u)else | |
l.delete(u)end end,move=l.move,copy=l.copy},http={request=h("http.request")},term={setColor=function(u) | |
local c=s[u]if not c then | |
error("Cannot find color "..tostring(u),2)end;io.write(n..c.."m") | |
io.flush()end,resetColor=function() | |
io.write(n.."0m")io.flush()end},refreshYield=function() | |
end}end | |
a["howl.platform"]=function(...) | |
if fs and term then return i"howl.platform.cc"elseif _G.component then | |
return i"howl.platform.oc"else return i"howl.platform.native"end end | |
a["howl.platform.cc"]=function(...)local n=term.getTextColor and term.getTextColor()or | |
colors.white | |
local function s(v) | |
local b=fs.open(v,"r")local g=b.readAll()b.close()return g end | |
local function h(v,b)local g=fs.open(v,"w")g.write(b)g.close()end;local function r(v,b,g) | |
if not fs.exists(v)then error("Cannot find "..b.." (Looking for "..v..")", | |
g or 1)end end | |
local d,l=os.queueEvent,coroutine.yield;local function u()d("sleep") | |
if l()=="terminate"then error("Terminated")end end | |
local function c(v)local b=#v+2;local g,k={v},1;local q={}while k>0 do | |
local j=g[k]k=k-1 | |
if fs.isDir(j)then | |
for x,z in ipairs(fs.list(j))do k=k+1;g[k]=fs.combine(j,z)end else q[j:sub(b)]=s(j)end end;return q end | |
local function m(v,b)for g,k in pairs(b)do h(fs.combine(v,g),k)end end;local f | |
if http.fetch then | |
f=function(v,b,g)local k,q=http.fetch(v,b,g) | |
if k then | |
while true do local j,x,z,_=os.pullEvent(e)if j== | |
"http_success"and x==v then return true,z elseif j=="http_failure"and x==v then | |
return false,_,z end end end;return false,nil,q end else | |
f=function(...)local v,b=http.post(...) | |
if v then return true,b else return false,nil,b end end end;local w;if settings and fs.exists(".settings")then | |
settings.load(".settings")end | |
if settings and shell.getEnv then | |
w=function(v,n) | |
local b=shell.getEnv(v)if b~=nil then return b end;return settings.get(v,n)end elseif settings then w=settings.get elseif shell.getEnv then | |
w=function(v,n)local b=shell.getEnv(v) | |
if b~=nil then return b end;return n end else w=function(v,n)return n end end;local y | |
if profiler and profiler.milliTime then y=function() | |
return profiler.milliTime()*1e-3 end else y=os.time end;local p;if howlci then p=howlci.log else p=function()end end | |
return | |
{os={clock=os.clock,time=y,getEnv=w},fs={combine=fs.combine,normalise=function(v)return | |
fs.combine(v,"")end,getDir=fs.getDir,getName=fs.getName,currentDir=shell.dir,currentProgram=shell.getRunningProgram,read=s,write=h,readDir=c,writeDir=m,getSize=fs.getSize,assertExists=r,exists=fs.exists,isDir=fs.isDir,list=fs.list,makeDir=fs.makeDir,delete=fs.delete,move=fs.move,copy=fs.copy},term={setColor=function(v)local b= | |
colours[v]or colors[v]if not b then | |
error("Unknown color "..v,2)end;term.setTextColor(b)end,resetColor=function() | |
term.setTextColor(n)end,print=print,write=io.write},http={request=f},log=p,refreshYield=u}end | |
a["howl.packages.Proxy"]=function(...)local n=i"howl.class"local s=i"howl.platform".fs | |
local h=i"howl.class.mixin"local r=n("howl.packages.Proxy")function r:initialize(d,l,u)self.name=l;self.manager=d | |
self.package=u end | |
function r:getName()return self.name end;function r:files()local d=self.manager:getCache(self.name)return | |
self.package:files(d)end;function r:require(d,l)return | |
self.manager:require(self.package,d,l)end;return r end | |
a["howl.packages.Package"]=function(...)local n=i"howl.class"local s=i"howl.platform".fs | |
local h=i"howl.class.mixin" | |
local r=n("howl.packages.Package"):include(h.configurable):include(h.optionGroup) | |
function r:initialize(d,l)if self.class==r then | |
error("Cannot create instance of abstract class "..tostring(r),2)end;self.context=d;self.root=l | |
self.options={}end;function r:setup() | |
error("setup has not been overridden in "..tostring(self.class),2)end;function r:getName() | |
error("name has not been overridden in ".. | |
tostring(self.class),2)end;function r:files(d) | |
error("files has not been overridden in ".. | |
tostring(self.class),2)end;function r:require(d,l) | |
error("require has not been overrriden in ".. | |
tostring(self.class),2)end;return r end | |
a["howl.packages.Manager"]=function(...)local n=i"howl.class"local s=i"howl.platform".fs | |
local h=i"howl.lib.dump"local r=i"howl.class.mixin"local d=i"howl.packages.Proxy"local l={} | |
local u=n("howl.packages.Manager")u.providers={} | |
function u:initialize(c)self.context=c;self.packages={} | |
self.packageLookup={}self.cache={}self.root=".howl/packages"self.alwaysRefresh=false end;function u.static:addProvider(n,c)self.providers[c]=n end | |
function u:addPackage(c,m) | |
local f=u.providers[c] | |
if not f then error("No such package provider "..c,2)end;local w=f(self.context,self.root)w:configure(m)local y=c.."-".. | |
w:getName() | |
w.installDir=s.combine(self.root,y)self.packages[y]=w;self.packageLookup[w]=y | |
w:setup(self.context)if self.context.logger.hasError then | |
error("Error setting up "..y,2)end;return d(self,y,w)end | |
function u:getCache(c)if not self.packages[c]then | |
error("No such package "..c,2)end;local m=self.cache[c] | |
local f=s.combine(self.root,c..".lua") | |
if m==nil and s.exists(f)then m=h.unserialise(s.read(f))end;if m==l then m=nil end;return m end | |
function u:require(c,m,f)local w=self.packageLookup[c]if not w then | |
error("No such package "..c:getName(),2)end;f=f or self.alwaysRefresh | |
local y=self:getCache(w)if y and m and not f then local b=c:files(y) | |
for g,k in ipairs(m)do if not b[k]then f=true;break end end end | |
local p=c:require(y,f) | |
if p~=y then | |
self.context.logger:verbose("Package "..w.." updated")if p==nil then self.cache[w]=l else self.cache[w]=p | |
s.write(s.combine(self.root,w..".lua"),h.serialise(p))end end;local v=c:files(p) | |
if m then for b,g in ipairs(m)do if not v[g]then | |
error("Cannot resolve "..g.." for "..w)end end end;return v end;return u end | |
a["howl.modules.tasks.require"]=function(...)local n=i"howl.lib.assert" | |
local s=i"howl.platform".fs;local h=i"howl.class.mixin"local r=i"howl.lib.Buffer"local d=i"howl.files.CopySource" | |
local l=i"howl.tasks.Runner"local u=i"howl.tasks.Task"local c=i"howl.modules.tasks.require.header" | |
local m="local env = setmetatable({ require = require, preload = preload, }, { __index = getfenv() })\n"local function f(b) | |
if b:find("%.lua$")then return | |
b:gsub("%.lua$",""):gsub("/","."):gsub("^(.*)%.init$","%1")end end | |
local function w(b)if | |
b.relative:find("%.res%.")then b.name=b.name:gsub("%.res%.",".")return | |
("return %q"):format(b.contents)end end | |
local y=u:subclass("howl.modules.require.RequireTask"):include(h.filterable):include(h.delegate("sources",{"from","include","exclude"})):addOptions{"link","startup","output","api"} | |
function y:initialize(b,g,k)u.initialize(self,g,k)self.sources=d()self.sources:rename(function(q)return | |
f(q.name)end) | |
self.sources:modify(w) | |
self:exclude{".git",".svn",".gitignore",b.out} | |
self:description("Packages files together to allow require")end;function y:configure(b)u.configure(self,b) | |
self.sources:configure(b)end | |
function y:output(b) | |
n.argType(b,"string","output",1)if self.options.output then | |
error("Cannot set output multiple times")end;self.options.output=b | |
self:Produces(b)end | |
function y:setup(b,g)u.setup(self,b,g)if not self.options.startup then | |
b.logger:error("Task '%s': No startup file",self.name)end | |
self:requires(self.options.startup)if not self.options.output then | |
b.logger:error("Task '%s': No output file",self.name)end end | |
function y:runAction(b)local g=self.sources:gatherFiles(b.root) | |
local k=s.combine(b.root,self.options.startup)local q=nil;local j=self.options.output;local x=self.options.link;local z=r() | |
z:append(c):append("\n")if x then z:append(m)end | |
for E,T in pairs(g)do | |
b.logger:verbose("Including "..T.relative) | |
z:append("preload[\""..T.name.."\"] = ") | |
if x then | |
n(s.exists(T.path),"Cannot find "..T.relative) | |
z:append("setfenv(assert(loadfile(\""..T.path.."\")), env)\n")else | |
z:append("function(...)\n"..T.contents.."\nend\n")end;if T.path==k then q=T.name end end;if not q then | |
error("Cannot find startup file "..self.options.startup.." in file list",0)end | |
if self.options.api then | |
z:append("if not shell or type(... or nil) == 'table' then\n")z:append("local tbl = ... or {}\n") | |
z:append("tbl.require = require tbl.preload = preload\n")z:append("return tbl\n")z:append("else\n")end | |
z:append("return preload[\""..q.."\"](...)\n")if self.options.api then z:append("end\n")end | |
s.write(s.combine(b.root,j),z:toString())end;local p={} | |
function p:require(b,g)return self:injectTask(y(self.env,b,g))end;local function v()l:include(p)end;return | |
{name="require task",description="A task that combines files that can be loaded using `require`.",apply=v,RequireTask=y}end | |
a["howl.modules.tasks.require.header"]=function(...) | |
return | |
"local loading = {}\ | |
local oldRequire, preload, loaded = require, {}, { startup = loading }\ | |
\ | |
local function require(name)\ | |
\009local result = loaded[name]\ | |
\ | |
\009if result ~= nil then\ | |
\009\009if result == loading then\ | |
\009\009\009error(\"loop or previous error loading module '\" .. name .. \"'\", 2)\ | |
\009\009end\ | |
\ | |
\009\009return result\ | |
\009end\ | |
\ | |
\009loaded[name] = loading\ | |
\009local contents = preload[name]\ | |
\009if contents then\ | |
\009\009result = contents(name)\ | |
\009elseif oldRequire then\ | |
\009\009result = oldRequire(name)\ | |
\009else\ | |
\009\009error(\"cannot load '\" .. name .. \"'\", 2)\ | |
\009end\ | |
\ | |
\009if result == nil then result = true end\ | |
\009loaded[name] = result\ | |
\009return result\ | |
end"end | |
a["howl.modules.tasks.pack.vfs"]=function(...) | |
return | |
"local fs = fs\ | |
\ | |
local matches = {\ | |
\009[\"^\"] = \"%^\",\ | |
\009[\"$\"] = \"%$\",\ | |
\009[\"(\"] = \"%(\",\ | |
\009[\")\"] = \"%)\",\ | |
\009[\"%\"] = \"%%\",\ | |
\009[\".\"] = \"%.\",\ | |
\009[\"[\"] = \"%[\",\ | |
\009[\"]\"] = \"%]\",\ | |
\009[\"*\"] = \"%*\",\ | |
\009[\"+\"] = \"%+\",\ | |
\009[\"-\"] = \"%-\",\ | |
\009[\"?\"] = \"%?\",\ | |
\009[\"\\0\"] = \"%z\",\ | |
}\ | |
\ | |
--- Escape a string for using in a pattern\ | |
-- @tparam string pattern The string to escape\ | |
-- @treturn string The escaped pattern\ | |
local function escapePattern(pattern)\ | |
\009return (pattern:gsub(\".\", matches))\ | |
end\ | |
\ | |
local function matchesLocal(root, path)\ | |
\009return root == \"\" or path == root or path:sub(1, #root + 1) == root .. \"/\"\ | |
end\ | |
\ | |
local function extractLocal(root, path)\ | |
\009if root == \"\" then\ | |
\009\009return path\ | |
\009else\ | |
\009\009return path:sub(#root + 2)\ | |
\009end\ | |
end\ | |
\ | |
\ | |
local function copy(old)\ | |
\009local new = {}\ | |
\009for k, v in pairs(old) do new[k] = v end\ | |
\009return new\ | |
end\ | |
\ | |
--[[\ | |
\009Emulates a basic file system.\ | |
\009This doesn't have to be too advanced as it is only for Howl's use\ | |
\009The files is a list of paths to file contents, or true if the file\ | |
\009is a directory.\ | |
\009TODO: Override IO\ | |
]]\ | |
local function makeEnv(root, files)\ | |
\009-- Emulated filesystem (partially based of Oeed's)\ | |
\009files = copy(files)\ | |
\009local env\ | |
\009env = {\ | |
\009\009fs = {\ | |
\009\009\009list = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009local list = fs.isDir(path) and fs.list(path) or {}\ | |
\ | |
\009\009\009\009if matchesLocal(root, path) then\ | |
\009\009\009\009\009local pattern = \"^\" .. escapePattern(extractLocal(root, path))\ | |
\009\009\009\009\009if pattern ~= \"^\" then pattern = pattern .. '/' end\ | |
\009\009\009\009\009pattern = pattern .. '([^/]+)$'\ | |
\ | |
\009\009\009\009\009for file, _ in pairs(files) do\ | |
\009\009\009\009\009\009local name = file:match(pattern)\ | |
\009\009\009\009\009\009if name then list[#list + 1] = name end\ | |
\009\009\009\009\009end\ | |
\009\009\009\009end\ | |
\ | |
\009\009\009\009return list\ | |
\009\009\009end,\ | |
\ | |
\009\009\009exists = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if fs.exists(path) then\ | |
\009\009\009\009\009return true\ | |
\009\009\009\009elseif matchesLocal(root, path) then\ | |
\009\009\009\009\009return files[extractLocal(root, path)] ~= nil\ | |
\009\009\009\009end\ | |
\009\009\009end,\ | |
\ | |
\009\009\009isDir = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if fs.isDir(path) then\ | |
\009\009\009\009\009return true\ | |
\009\009\009\009elseif matchesLocal(root, path) then\ | |
\009\009\009\009\009return files[extractLocal(root, path)] == true\ | |
\009\009\009\009end\ | |
\009\009\009end,\ | |
\ | |
\009\009\009isReadOnly = function(path)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if fs.exists(path) then\ | |
\009\009\009\009\009return fs.isReadOnly(path)\ | |
\009\009\009\009elseif matchesLocal(root, path) and files[extractLocal(root, path)] ~= nil then\ | |
\009\009\009\009\009return true\ | |
\009\009\009\009else\ | |
\009\009\009\009\009return false\ | |
\009\009\009\009end\ | |
\009\009\009end,\ | |
\ | |
\009\009\009getName = fs.getName,\ | |
\009\009\009getDir = fs.getDir,\ | |
\009\009\009getSize = fs.getSize,\ | |
\009\009\009getFreeSpace = fs.getFreeSpace,\ | |
\009\009\009combine = fs.combine,\ | |
\ | |
\009\009\009-- TODO: This should be implemented\ | |
\009\009\009move = fs.move,\ | |
\009\009\009copy = fs.copy,\ | |
\009\009\009makeDir = function(dir)\ | |
\ | |
\009\009\009end,\ | |
\009\009\009delete = fs.delete,\ | |
\ | |
\009\009\009open = function(path, mode)\ | |
\009\009\009\009path = fs.combine(path, \"\")\ | |
\009\009\009\009if matchesLocal(root, path) then\ | |
\009\009\009\009\009local localPath = extractLocal(root, path)\ | |
\009\009\009\009\009if type(files[localPath]) == 'string' then\ | |
\009\009\009\009\009\009local handle = {close = function()end}\ | |
\009\009\009\009\009\009if mode == 'r' then\ | |
\009\009\009\009\009\009\009local content = files[localPath]\ | |
\009\009\009\009\009\009\009handle.readAll = function()\ | |
\009\009\009\009\009\009\009\009return content\ | |
\009\009\009\009\009\009\009end\ | |
\ | |
\009\009\009\009\009\009\009local line = 1\ | |
\009\009\009\009\009\009\009local lines\ | |
\009\009\009\009\009\009\009handle.readLine = function()\ | |
\009\009\009\009\009\009\009\009if not lines then -- Lazy load lines\ | |
\009\009\009\009\009\009\009\009\009lines = {content:match((content:gsub(\"[^\\n]+\\n?\", \"([^\\n]+)\\n?\")))}\ | |
\009\009\009\009\009\009\009\009end\ | |
\009\009\009\009\009\009\009\009if line > #lines then\ | |
\009\009\009\009\009\009\009\009\009return nil\ | |
\009\009\009\009\009\009\009\009else\ | |
\009\009\009\009\009\009\009\009\009return lines[line]\ | |
\009\009\009\009\009\009\009\009end\ | |
\009\009\009\009\009\009\009\009line = line + 1\ | |
\009\009\009\009\009\009\009end\ | |
\ | |
\009\009\009\009\009\009\009return handle\ | |
\009\009\009\009\009\009else\ | |
\009\009\009\009\009\009\009error('Cannot write to read-only file.', 2)\ | |
\009\009\009\009\009\009end\ | |
\009\009\009\009\009end\ | |
\009\009\009\009end\ | |
\ | |
\009\009\009\009return fs.open(path, mode)\ | |
\009\009\009end\ | |
\009\009},\ | |
\ | |
\009\009loadfile = function(name)\ | |
\009\009\009local file = env.fs.open(name, \"r\")\ | |
\009\009\009if file then\ | |
\009\009\009\009local func, err = load(file.readAll(), fs.getName(name), nil, env)\ | |
\009\009\009\009file.close()\ | |
\009\009\009\009return func, err\ | |
\009\009\009end\ | |
\009\009\009return nil, \"File not found: \"..name\ | |
\009\009end,\ | |
\ | |
\009\009dofile = function(name)\ | |
\009\009\009local file, e = env.loadfile(name, env)\ | |
\009\009\009if file then\ | |
\009\009\009\009return file()\ | |
\009\009\009else\ | |
\009\009\009\009error(e, 2)\ | |
\009\009\009end\ | |
\009\009end,\ | |
\009}\ | |
\ | |
\009env._G = env\ | |
\009env._ENV = env\ | |
\009return setmetatable(env, {__index = _ENV or getfenv()})\ | |
end\ | |
\ | |
local function extract(root, files, from, to)\ | |
\009local pattern = \"^\" .. escapePattern(extractLocal(root, from))\ | |
\009if pattern ~= \"^\" then pattern = pattern .. '/' end\ | |
\009pattern = pattern .. '(.*)$'\ | |
\ | |
\009for file, contents in pairs(files) do\ | |
\009\009local name = file:match(pattern)\ | |
\009\009if name then\ | |
\009\009\009print(\"Extracting \" .. name)\ | |
\009\009\009local handle = fs.open(fs.combine(to, name), \"w\")\ | |
\009\009\009handle.write(contents)\ | |
\009\009\009handle.close()\ | |
\009\009end\ | |
\009end\ | |
end"end | |
a["howl.modules.tasks.pack.template"]=function(...) | |
return | |
"local files = ${files}\ | |
\ | |
${vfs}\ | |
\ | |
local root = \"\"\ | |
local args = {...}\ | |
if #args == 1 and args[1] == '--extract' then\ | |
\009extract(root, files, \"\", root)\ | |
else\ | |
\009local env = makeEnv(root, files)\ | |
\009local func, err = env.loadfile(${startup})\ | |
\009if not func then error(err, 0) end\ | |
\009return func(...)\ | |
end"end | |
a["howl.modules.tasks.pack"]=function(...)local n=i"howl.lib.assert"local s=i"howl.lib.dump" | |
local h=i"howl.platform".fs;local r=i"howl.class.mixin"local d=i"howl.lexer.rebuild" | |
local l=i"howl.files.CopySource"local u=i"howl.tasks.Runner"local c=i"howl.tasks.Task" | |
local m=i"howl.lib.utils".formatTemplate;local f=i"howl.modules.tasks.pack.template"local w=i"howl.modules.tasks.pack.vfs" | |
local y=c:subclass("howl.modules.tasks.pack.PackTask"):include(r.filterable):include(r.delegate("sources",{"from","include","exclude"})):addOptions{"minify","startup","output"} | |
function y:initialize(b,g,k)c.initialize(self,g,k)self.root=b.root;self.sources=l() | |
self.sources:modify(function(q) | |
local j=q.contents;if self.options.minify and loadstring(j)then | |
return d.minifyString(j)end end) | |
self:exclude{".git",".svn",".gitignore",b.out} | |
self:description("Combines multiple files using Pack")end;function y:configure(b)c.configure(self,b) | |
self.sources:configure(b)end | |
function y:output(b) | |
n.argType(b,"string","output",1)if self.options.output then | |
error("Cannot set output multiple times")end;self.options.output=b | |
self:Produces(b)end | |
function y:setup(b,g)c.setup(self,b,g)if not self.options.startup then | |
b.logger:error("Task '%s': No startup file",self.name)end | |
self:requires(self.options.startup)if not self.options.output then | |
b.logger:error("Task '%s': No output file",self.name)end end | |
function y:runAction(b)local g=self.sources:gatherFiles(self.root) | |
local k=self.options.startup;local q=self.options.output;local j=self.options.minify;local x={}for E,T in pairs(g)do b.logger:verbose( | |
"Including "..T.relative) | |
x[T.name]=T.contents end | |
local z=m(f,{files=s.serialise(x),startup=("%q"):format(k),vfs=w})if j then z=d.minifyString(z)end | |
h.write(h.combine(b.root,q),z)end;local p={} | |
function p:pack(b,g)return self:injectTask(y(self.env,b,g))end;local function v()u:include(p)end | |
return | |
{name="pack task",description="A task to combine multiple files into one which are then executed within a virtual file system.",apply=v,PackTask=y}end | |
a["howl.modules.tasks.minify"]=function(...)local n=i"howl.lib.assert" | |
local s=i"howl.lexer.rebuild"local h=i"howl.tasks.Runner"local r=i"howl.tasks.Task"local d=s.minifyFile;local l=function(w,y,p,v) | |
return d(y.root,p,v)end | |
local u=r:subclass("howl.modules.minify.tasks.MinifyTask"):addOptions{"input","output"}function u:initialize(w,y,p)r.initialize(self,y,p) | |
self:description"Minify a file"end | |
function u:input(w) | |
n.argType(w,"string","input",1)if self.options.input then | |
error("Cannot set input multiple times")end;self.options.input=w | |
self:requires(w)end | |
function u:output(w)n.argType(w,"string","output",1)if self.options.output then | |
error("Cannot set output multiple times")end;self.options.output=w | |
self:Produces(w)end | |
function u:setup(w,y)r.setup(self,w,y)if not self.options.input then | |
w.logger:error("Task '%s': No input file specified",self.name)end;if | |
not self.options.output then | |
w.logger:error("Task '%s': No output file specified",self.name)end end | |
function u:runAction(w) | |
local y,p=d(w.root,self.options.input,self.options.output)local v=(y-p)/y*100;v=math.floor(v*100)/100 | |
w.logger:verbose(("%.20f%% decrease in file size"):format(v))end;local c={} | |
function c:minify(w,y)return self:injectTask(u(self.env,w,y))end | |
function c:addMinifier(w,y,p)w=w or"_minify"return | |
self:addTask(w,{},l):Description("Minifies files"):Maps( | |
y or"wild:*.lua",p or"wild:*.min.lua")end;local function m()h:include(c)end;local function f(w) | |
w.mediator:subscribe({"HowlFile","env"},function(y) | |
y.minify=d end)end;return | |
{name="minify task",description="Adds various tasks to minify files.",apply=m,setup=f}end | |
a["howl.modules.tasks.gist"]=function(...)local n=i"howl.lib.assert"local s=i"howl.class.mixin" | |
local h=i"howl.lib.settings"local r=i"howl.lib.json"local d=i"howl.platform"local l=d.http;local u=i"howl.lib.Buffer" | |
local c=i"howl.tasks.Task"local m=i"howl.tasks.Runner"local f=i"howl.files.CopySource" | |
local w=c:subclass("howl.modules.tasks.gist.GistTask"):include(s.filterable):include(s.delegate("sources",{"from","include","exclude"})):addOptions{"gist","summary"} | |
function w:initialize(v,b,g)c.initialize(self,b,g)self.root=v.root;self.sources=f() | |
self:exclude{".git",".svn",".gitignore"}self:description"Uploads files to a gist"end;function w:configure(v)c.configure(self,context,runner) | |
self.sources:configure(v)end | |
function w:setup(v,b)c.setup(self,v,b)if not | |
self.options.gist then | |
v.logger:error("Task '%s': No gist ID specified",self.name)end | |
if not h.githubKey then | |
v.logger:error("Task '%s': No GitHub API key specified. Goto https://github.com/settings/tokens/new to create one.",self.name)end end | |
function w:runAction(v)local b=self.sources:gatherFiles(self.root) | |
local g=self.options.gist;local k=h.githubKey;local q={} | |
for A,O in pairs(b)do | |
v.logger:verbose("Including "..O.relative)q[O.name]={content=O.contents}end | |
local j="https://api.github.com/gists/"..g.."?access_token="..k | |
local x={Accept="application/vnd.github.v3+json",["X-HTTP-Method-Override"]="PATCH"} | |
local z=r.encodePretty({files=q,description=self.options.summary})local _,E,T=l.request(j,z,x) | |
if not _ then if E then | |
v.logger:error(E.readAll())end;error(result,0)end end;local y={} | |
function y:gist(v,b)return self:injectTask(w(self.env,v,b))end;local function p()m:include(y)end;return | |
{name="gist task",description="A task that uploads files to a Gist.",apply=p,GistTask=w}end | |
a["howl.modules.tasks.clean"]=function(...)local n=i"howl.class.mixin" | |
local s=i"howl.platform".fs;local h=i"howl.tasks.Task"local r=i"howl.tasks.Runner"local d=i"howl.files.Source" | |
local l=h:subclass("howl.modules.tasks.clean.CleanTask"):include(n.configurable):include(n.filterable):include(n.delegate("sources",{"from","include","exclude"})) | |
function l:initialize(m,f,w)h.initialize(self,f,w)self.root=m.root;self.sources=d() | |
self:exclude{".git",".svn",".gitignore"}self:description"Deletes all files matching a pattern"end | |
function l:configure(m)self.sources:configure(m)end;function l:setup(m,f)h.setup(self,m,f)local w=self.sources | |
if | |
w.allowEmpty and#w.includes==0 then w:include(s.combine(m.out,"*"))end end | |
function l:runAction(m)for f,w in | |
ipairs(self.sources:gatherFiles(self.root,true))do m.logger:verbose("Deleting "..w.path) | |
s.delete(w.path)end end;local u={}function u:clean(m,f) | |
return self:injectTask(l(self.env,m or"clean",f))end;local function c()r:include(u)end;return | |
{name="clean task",description="A task that deletes all specified files.",apply=c,CleanTask=l}end | |
a["howl.modules.plugins"]=function(...)local n=i"howl.class"local s=i"howl.class.mixin" | |
local h=i"howl.platform".fs | |
local r=n("howl.modules.plugins"):include(s.configurable)function r:initialize(l)self.context=l end;function r:configure(l) | |
if#l==0 then | |
self:addPlugin(l,l)else for u=1,#l do self:addPlugin(l[u])end end end | |
local function d(l,u) | |
local c=u:gsub("%.lua$",""):gsub("/","."):gsub("^(.*)%.init$","%1")if c==""or c=="init"then return l else return l.."."..c end end | |
function r:addPlugin(l) | |
if not l.type then error("No plugin type specified")end;local u=l.type;l.type=nil;local c;if l.file then c=l.file;l.file=nil end | |
local m=self.context.packageManager;local f=m:addPackage(u,l) | |
self.context.logger:verbose("Using plugin from package ".. | |
f:getName())local w=f:require(c and{c}) | |
local y="external."..f:getName()local p=0 | |
for c,b in pairs(w)do | |
if c:find("%.lua$")then p=p+1;local g,k=loadfile(w[c],_ENV) | |
if g then | |
local q=d(y,c)a[q]=g | |
self.context.logger:verbose("Including plugin file "..c.." as "..q)else | |
self.context.logger:warning("Cannot load plugin file "..c..": "..k)end end end | |
if not c then | |
if w["init.lua"]then c="init.lua"elseif p==1 then c=next(w)elseif p==0 then | |
self.context.logger:error( | |
f:getName().." does not export any files")error("Error adding plugin")else | |
self.context.logger:error( | |
"Cannot guess a file for "..f:getName())error("Error adding plugin")end end | |
self.context.logger:verbose("Using package "..f:getName().." with "..c)local v=d(y,c) | |
if not a[v]then | |
self.context.logger:error("Cannot load plugin as ".. | |
v.." could not be loaded")error("Error adding plugin")end;self.context:include(i(v))return self end | |
return | |
{name="plugins",description="Inject plugins into Howl at runtime.",setup=function(l) | |
l.mediator:subscribe({"HowlFile","env"},function(u)u.plugins=r(l)end)end}end | |
a["howl.modules.packages.pastebin"]=function(...)local n=i"howl.class"local s=i"howl.platform" | |
local h=i"howl.packages.Manager"local r=i"howl.packages.Package" | |
local d=r:subclass("howl.modules.packages.pastebin.PastebinPackage"):addOptions{"id"} | |
function d:setup(l)if not self.options.id then | |
self.context.logger:error("Pastebin has no ID")end end;function d:getName()return self.options.id end | |
function d:files(l)if l then return{}else return | |
{["init.lua"]=s.fs.combine(self.installDir,"init.lua")}end end | |
function d:require(l,u)local c=self.options.id;local m=self.installDir | |
if not u and l then return l end | |
local f,w=s.http.request("http://pastebin.com/raw/"..c)if not f or not w then | |
self.context.logger:error("Cannot find pastebin "..c)return l end;local y=w.readAll() | |
w.close()s.fs.write(s.fs.combine(m,"init.lua"),y)return | |
{}end | |
return | |
{name="pastebin package",description="Allows downloading a pastebin dependency.",apply=function()h:addProvider(d,"pastebin")end,PastebinPackage=d}end | |
a["howl.modules.packages.gist"]=function(...)local n=i"howl.class"local s=i"howl.lib.json" | |
local h=i"howl.platform"local r=i"howl.packages.Manager"local d=i"howl.packages.Package" | |
local l=d:subclass("howl.modules.packages.gist.GistPackage"):addOptions{"id"} | |
function l:setup(u)if not self.options.id then | |
self.context.logger:error("Gist has no ID")end end;function l:getName()return self.options.id end | |
function l:files(u)if u then local c={} | |
for m,f in | |
pairs(u.files)do c[m]=h.fs.combine(self.installDir,m)end;return c else return{}end end | |
function l:require(u,c)local m=self.options.id;local f=self.installDir | |
if not c and u then return u end | |
local w,y=h.http.request("https://api.github.com/gists/"..m)if not w or not y then | |
self.context.logger:error("Cannot find gist "..m)return false end;local p=y.readAll() | |
y.close()local v=s.decode(p)local b=v.history[1].version;local g | |
if | |
u and b==u.hash then g=u else g={hash=b,files={}} | |
for k,q in pairs(v.files)do if q.truncated then | |
self.context.logger:error( | |
"Skipping "..k.." as it is truncated")else h.fs.write(h.fs.combine(f,k),q.content) | |
g.files[k]=true end end end;return g end;return | |
{name="gist package",description="Allows downloading a gist dependency.",apply=function()r:addProvider(l,"gist")end,GistPackage=l}end | |
a["howl.modules.packages.file"]=function(...)local n=i"howl.class"local s=i"howl.class.mixin" | |
local h=i"howl.platform".fs;local r=i"howl.packages.Manager"local d=i"howl.packages.Package" | |
local l=i"howl.files.Source" | |
local u=d:subclass("howl.modules.packages.file.FilePackage"):include(s.filterable):include(s.delegate("sources",{"from","include","exclude"}))function u:initialize(c,m)d.initialize(self,c,m)self.sources=l(false) | |
self.name=tostring({}):sub(8) | |
self:exclude{".git",".svn",".gitignore",c.out}end | |
function u:setup(c)if | |
not self.sources:hasFiles()then | |
self.context.logger:error("No files specified")end end;function u:configure(c)d.configure(self,c) | |
self.sources:configure(c)end;function u:getName()return self.name end | |
function u:files(c) | |
local m={}for f,w in | |
pairs(self.sources:gatherFiles(self.context.root))do m[w.name]=w.path end;return m end;function u:require(c,m)end;return | |
{name="file package",description="Allows using a local file as a dependency",apply=function() | |
r:addProvider(u,"file")end,FilePackage=u}end | |
a["howl.modules.list"]=function(...)local n=i"howl.lib.assert"local s=i"howl.lib.colored" | |
local h=i"howl.tasks.Runner"local r={} | |
function r:listTasks(l,u)local c={}local m=0 | |
for f,w in pairs(self.tasks)do local y=f:sub(1,1)if u or | |
(y~="_"and y~=".")then local p=w.options.description or""local v=#f | |
if v>m then m=v end;c[f]=p end end;m=m+2;l=l or""for f,w in pairs(c)do s.writeColor("white",l..f) | |
s.printColor("lightGray",string.rep(" ", | |
m-#f)..w)end;return self end;local function d()h:include(r)end;return | |
{name="list",description="List all tasks on a runner.",apply=d}end | |
a["howl.modules.dependencies.task"]=function(...)local n=i"howl.lib.assert" | |
local s=i"howl.tasks.Task"local h=i"howl.tasks.Dependency" | |
local r=h:subclass("howl.modules.dependencies.task.TaskDependency")function r:initialize(d,l)h.initialize(self,d) | |
n.argType(l,"string","initialize",1)self.name=l end | |
function r:setup(d,l)if not | |
l.tasks[self.name]then | |
d.logger:error("Task '%s': cannot resolve dependency '%s'",self.task.name,self.name)end end;function r:resolve(d,l)return l:run(self.name)end;return | |
{name="task dependency",description="Allows depending on a task.",apply=function() | |
s:addDependency(r,"depends")end,TaskDependency=r}end | |
a["howl.modules.dependencies.file"]=function(...)local n=i"howl.lib.assert" | |
local s=i"howl.tasks.Task"local h=i"howl.tasks.Dependency" | |
local r=h:subclass("howl.modules.dependencies.file.FileDependency")function r:initialize(d,l)h.initialize(self,d) | |
n.argType(l,"string","initialize",1)self.path=l end | |
function r:setup(d,l)end;function r:resolve(d,l)return l:DoRequire(self.path)end;return | |
{name="file dependency",description="Allows depending on a file.",apply=function() | |
s:addDependency(r,"requires")end,FileDependency=r}end | |
a["howl.loader"]=function(...)local n=i"howl.platform".fs;local s=i"howl.tasks.Runner" | |
local h=i"howl.lib.utils"local r={"Howlfile","Howlfile.lua"} | |
local function d()local c=n.currentDir() | |
while true do for m,f in | |
ipairs(r)do local w=n.combine(c,f) | |
if n.exists(w)and not n.isDir(w)then return f,c end end | |
if c=="/"or c==""then break end;c=n.getDir(c)end;return nil, | |
"Cannot find HowlFile. Looking for '"..table.concat(r,"', '").."'."end | |
local function l(c)local m=setmetatable(c or{},{__index=_ENV})function m.loadfile(f)return | |
assert(loadfile(f,m))end;function m.dofile(f) | |
return m.loadfile(f)()end;return m end | |
local function u(c,m)local f=s(c) | |
c.mediator:subscribe({"ArgParse","changed"},function(y)f.ShowTime=y:Get"time" | |
f.Traceback=y:Get"trace"end) | |
local w=l({require=i,CurrentDirectory=c.root,Tasks=f,Options=c.arguments,Verbose=c.logger/"verbose",Log=c.logger/"dump",File=function(...) | |
return n.combine(c.root,...)end})c.mediator:publish({"HowlFile","env"},w,c) | |
return f,w end;return{FindHowl=d,SetupEnvironment=l,SetupTasks=u,Names=r}end | |
a["howl.lib.utils"]=function(...)local n=i"howl.lib.assert" | |
local s={["^"]="%^",["$"]="%$",["("]="%(",[")"]="%)",["%"]="%%",["."]="%.",["["]="%[",["]"]="%]",["*"]="%*",["+"]="%+",["-"]="%-",["?"]="%?",["\0"]="%z"}local function h(w)return(w:gsub(".",s))end | |
local r={["^"]="%^",["$"]="%$",["("]="%(",[")"]="%)",["%"]="%%",["."]="%.",["["]="%[",["]"]="%]",["+"]="%+",["-"]="%-",["?"]="%?",["\0"]="%z"} | |
local function d(w,y)local p=w:sub(1,5) | |
if p=="ptrn:"or p=="wild:"then local w=w:sub(6) | |
if p=="wild:"then | |
if y then | |
local v=0 | |
w=((w:gsub(".",r)):gsub("(%*)",function()v=v+1;return"%"..v end))else w="^".. | |
((w:gsub(".",r)):gsub("(%*)","(.*)")).."$"end end;return{Type="Pattern",Text=w}else return{Type="Normal",Text=w}end end;local function l(w)for y,p in ipairs(w)do w[p]=true end;return w end;local function u(w,y) | |
local p=#w;if p~=#y then return false end | |
for v=1,p do if w[v]~=y[v]then return false end end;return true end;local function c(w,y) | |
if w:sub(1, | |
#y)==y then return w:sub(#y+1)else return false end end | |
local function m(w,y)return | |
(w:gsub("${([^}]+)}",function(p)local v=y[p]if v==nil then return | |
"${"..p.."}"else return tostring(v)end end))end | |
local function f(w,y,p)n.argType(w,"string","deprecated",1) | |
n.argType(y,"function","deprecated",2) | |
if p~=nil then n.argType(p,"string","msg",4)p=" "..p else p=""end;local v=false | |
return | |
function(...)if not v then local b,g=pcall(error,"",3)g=g:gsub(":%s*$","") | |
print(w.. | |
" is deprecated (called at "..g..")."..p)v=true end | |
return y(...)end end | |
return{escapePattern=h,parsePattern=d,createLookup=l,matchTables=u,startsWith=c,formatTemplate=m,deprecated=f}end | |
a["howl.lib.settings"]=function(...)local n=i"howl.platform"local s=n.fs;local h=i"howl.lib.dump" | |
local r={} | |
if s.exists(".howl.settings.lua")then local d=s.read(".howl.settings.lua")for l,u in | |
pairs(h.unserialise(d))do r[l]=u end end | |
if s.exists(".howl/settings.lua")then local d=s.read(".howl/settings.lua")for l,u in | |
pairs(h.unserialise(d))do r[l]=u end end | |
for d,l in pairs(r)do r[d]=n.os.getEnv("howl."..d,l)end;return r end | |
a["howl.lib.mediator"]=function(...)local n=i"howl.class"local s=i"howl.class.mixin"local function h()return | |
tonumber(tostring({}):match(':%s*[0xX]*(%x+)'),16)end | |
local r=n("howl.lib.mediator.Subscriber"):include(s.sealed) | |
function r:initialize(u,c)self.id=h()self.options=c or{}self.fn=u end;function r:update(u)self.fn=u.fn or self.fn | |
self.options=u.options or self.options end | |
local d=n("howl.lib.mediator.Channel"):include(s.sealed)function d:initialize(u,c)self.stopped=false;self.namespace=u;self.callbacks={} | |
self.channels={}self.parent=c end | |
function d:addSubscriber(u,c) | |
local m=r(u,c)local f=(#self.callbacks+1)c=c or{} | |
if c.priority and | |
c.priority>=0 and c.priority<f then f=c.priority end;table.insert(self.callbacks,f,m)return m end | |
function d:getSubscriber(u)for m=1,#self.callbacks do local f=self.callbacks[m]if f.id==u then | |
return{index=m,value=f}end end;local c | |
for m,f in | |
pairs(self.channels)do c=f:getSubscriber(u)if c then break end end;return c end | |
function d:setPriority(u,c)local m=self:getSubscriber(u) | |
if m.value then | |
table.remove(self.callbacks,m.index)table.insert(self.callbacks,c,m.value)end end | |
function d:addChannel(u)self.channels[u]=d(u,self)return self.channels[u]end | |
function d:hasChannel(u)return u and self.channels[u]and true end;function d:getChannel(u) | |
return self.channels[u]or self:addChannel(u)end | |
function d:removeSubscriber(u) | |
local c=self:getSubscriber(u) | |
if c and c.value then | |
for m,f in pairs(self.channels)do f:removeSubscriber(u)end;return table.remove(self.callbacks,c.index)end end | |
function d:publish(u,...) | |
for c=1,#self.callbacks do local m=self.callbacks[c] | |
if | |
not m.options.predicate or m.options.predicate(...)then local f,w=m.fn(...)if | |
w~=nil then table.insert(u,w)end | |
if f==false then return false,u end end end | |
if self.parent then return self.parent:publish(u,...)else return true,u end end | |
local l=setmetatable({Channel=d,Subscriber=r},{__call=function(u,c) | |
return | |
{channel=d('root'),getChannel=function(m,f)local w=m.channel | |
for y=1,#f do w=w:getChannel(f[y])end;return w end,subscribe=function(m,f,u,c)return | |
m:getChannel(f):addSubscriber(u,c)end,getSubscriber=function(m,f,w)return | |
m:getChannel(w):getSubscriber(f)end,removeSubscriber=function(m,f,w)return | |
m:getChannel(w):removeSubscriber(f)end,publish=function(m,f,...)return | |
m:getChannel(f):publish({},...)end}end})return l()end | |
a["howl.lib.Logger"]=function(...)local n=i"howl.class"local s=i"howl.class.mixin" | |
local h=i"howl.lib.dump".dump;local r=i"howl.lib.colored"local d=i"howl.platform".log | |
local l,u=select,tostring;local function c(...)local y={}for p=1,l('#',...)do y[p]=u(l(p,...))end;return | |
table.concat(y," ")end | |
local m=n("howl.lib.Logger"):include(s.sealed):include(s.curry)function m:initialize(y)self.isVerbose=false | |
y.mediator:subscribe({"ArgParse","changed"},function(p)self.isVerbose= | |
p:Get"verbose"or false end)end | |
function m:verbose(...)if | |
self.isVerbose then local y,p=pcall(function()error("",4)end) | |
r.writeColor("gray",p)r.printColor("lightGray",...) | |
d("verbose",p..c(...))end end | |
function m:dump(...) | |
if self.isVerbose then | |
local y,p=pcall(function()error("",4)end)r.writeColor("gray",p)local v=l('#',...)local b={...}for g=1,v do local k=b[g] | |
local q=type(k)if q=="table"then k=h(k)else k=u(k)end;if g>1 then k=" "..k end | |
r.writeColor("lightGray",k)end | |
print()end end | |
local f={{"success","ok","green"},{"error","error","red"},{"info","info","cyan"},{"warning","warn","yellow"}}local w=0;for y,p in ipairs(f)do w=math.max(w,#p[2])end | |
for y,p in ipairs(f)do | |
local v=p[3] | |
local b='['..p[2]..']'.. (' '):rep(w-#p[2]+1) | |
local g="has"..p[2]:gsub("^%l",string.upper)local k=p[1] | |
m[k]=function(q,j,...)q[g]=true;r.writeColor(v,b)local x;if type(j)=="string"then | |
x=j:format(...)else end;r.printColor(v,x)d(k,x)end end;return m end | |
a["howl.lib.json"]=function(...) | |
local n={["\n"]="\\n",["\r"]="\\r",["\t"]="\\t",["\b"]="\\b",["\f"]="\\f",["\""]="\\\"",["\\"]="\\\\"} | |
local function s(j)local x=0 | |
for z,_ in pairs(j)do if type(z)~="number"then return false elseif z>x then x=z end end;return x==#j end | |
local function h(j,x,z,_,E)local T="" | |
local function A(I)T=T.. ("\t"):rep(z)..I end | |
local function O(j,I,N,S,H)T=T..I;if x then T=T.."\n"z=z+1 end;for R,D in S(j)do A("")H(R,D)T=T..","if x then | |
T=T.."\n"end end;if x then z=z-1 end | |
if | |
T:sub(-2)==",\n"then T=T:sub(1,-3).."\n"elseif T:sub(-1)==","then T=T:sub(1,-2)end;A(N)end | |
if type(j)=="table"then | |
assert(not _[j],"Cannot encode a table holding itself recursively")_[j]=true | |
if s(j)then | |
O(j,"[","]",ipairs,function(I,N)T=T..h(N,x,z,_)end)else | |
O(j,"{","}",pairs,function(I,N) | |
assert(type(I)=="string","JSON object keys must be strings",2)T=T..h(I,x,z,_) | |
T=T.. (x and": "or":")..h(N,x,z,_,I)end)end elseif type(j)=="string"then | |
T='"'..j:gsub("[%c\"\\]",n)..'"'elseif type(j)=="number"or type(j)=="boolean"then T=tostring(j)else | |
error( | |
"JSON only supports arrays, objects, numbers, booleans, and strings, got "..type(j).." in "..tostring(E),2)end;return T end;local function r(j)return h(j,false,0,{})end | |
local function d(j)return h(j,true,0,{})end | |
local l={['\n']=true,['\r']=true,['\t']=true,[' ']=true,[',']=true,[':']=true} | |
local function u(j)while l[j:sub(1,1)]do j=j:sub(2)end;return j end;local c={}for j,x in pairs(n)do c[x]=j end | |
local function m(j)if j:sub(1,4)=="true"then | |
return true,u(j:sub(5))else return false,u(j:sub(6))end end;local function f(j)return nil,u(j:sub(5))end | |
local w={['e']=true,['E']=true,['+']=true,['-']=true,['.']=true} | |
local function y(j)local x=1 | |
while w[j:sub(x,x)]or tonumber(j:sub(x,x))do x=x+1 end;local z=tonumber(j:sub(1,x-1))j=u(j:sub(x))return z,j end | |
local function p(j)j=j:sub(2)local x="" | |
while j:sub(1,1)~="\""do local z=j:sub(1,1)j=j:sub(2) | |
assert(z~="\n","Unclosed string")if z=="\\"then local _=j:sub(1,1)j=j:sub(2) | |
z=assert(c[z.._],"Invalid escape character")end;x=x..z end;return x,u(j:sub(2))end;local v | |
local function b(j)j=u(j:sub(2))local x={}local z=1;while j:sub(1,1)~="]"do local _=nil;_,j=v(j)x[z]=_;z= | |
z+1;j=u(j)end;j=u(j:sub(2))return x,j end;local function g(j)local x=nil;x,j=v(j)local z=nil;z,j=v(j)return x,z,j end | |
local function k(j) | |
j=u(j:sub(2))local x={} | |
while j:sub(1,1)~="}"do local z,_=nil,nil;z,_,j=g(j)x[z]=_;j=u(j)end;j=u(j:sub(2))return x,j end | |
function v(j)local x=j:sub(1,1) | |
if x=="{"then return k(j)elseif x=="["then return b(j)elseif | |
tonumber(x)~=nil or w[x]then return y(j)elseif j:sub(1,4)=="true"or j:sub(1,5)=="false"then | |
return m(j)elseif x=="\""then return p(j)elseif j:sub(1,4)=="null"then return f(j)end;return nil end;local function q(j)j=u(j)return v(j)end;return{encode=r,encodePretty=d,decode=q}end | |
a["howl.lib.dump"]=function(...)local n=i("howl.lib.Buffer") | |
local s=i("howl.lib.utils").createLookup;local h,r,d=type,tostring,string.format;local l,u=getmetatable,error | |
local function c(v,b,g,k)local q=h(v) | |
if | |
q=="table"and not b[v]then b[v]=true | |
if next(v)==nil then return"{}"else local j=false;local x=#v;local z=0 | |
for S,H in | |
pairs(v)do | |
if h(S)=="table"or h(H)=="table"then j=true;break elseif | |
h(S)=="number"and S>=1 and S<=x and S%1 ==0 then z=z+#r(H)+2 else z= | |
z+#r(H)+#r(S)+2 end;if z>40 then j=true;break end end;local _,E,T="",", "," "if j then _="\n"E=",\n"T=g.." "end;local A,O={ | |
(k and"("or"{").._},1;local I={}local N=true | |
for S=1,x do I[S]=true;O=O+1;local H=T.. | |
c(v[S],b,T)if not N then H=E..H else N=false end;A[O]=H end | |
for S,H in pairs(v)do | |
if not I[S]then local R;if | |
h(S)=="string"and string.match(S,"^[%a_][%a%d_]*$")then R=S.." = "..c(H,b,T)else | |
R="["..c(S,b,T).."] = "..c(H,b,T)end;R=T..R;if not N then | |
R=E..R else N=false end;O=O+1;A[O]=R end end;O=O+1;A[O]=_..g.. (k and")"or"}")return | |
table.concat(A)end elseif q=="string"then return | |
(string.format("%q",v):gsub("\\\n","\\n"))else return r(v)end end;local function m(v,b)return c(v,{},"",b)end | |
local f=s{"and","break","do","else","elseif","end","false","for","function","if","in","local","nil","not","or","repeat","return","then","true","until","while"} | |
local function w(v,b,g)local k=h(v) | |
if k=="table"then if b[v]then | |
u("Cannot serialise table with recursive entries",1)end;b[v]=true | |
if next(v)==nil then | |
g:append("{}")else g:append("{")local q={} | |
for j,x in ipairs(v)do q[j]=true;w(x,b,g)g:append(",")end | |
for j,x in pairs(v)do | |
if not q[j]then | |
if | |
h(j)=="string"and not f[j]and j:match("^[%a_][%a%d_]*$")then g:append(j.."=")else g:append("[")w(j,b,g)g:append("]=")end;w(x,b,g)g:append(",")end end;g:append("}")end elseif k=="string"then g:append(d("%q",v))elseif k=="number"or k=="boolean"or | |
k=="nil"then g:append(r(v))else | |
u("Cannot serialise type "..k)end;return g end;local function y(v)return w(v,{},n()):toString()end | |
local function p(v)local b=loadstring( | |
"return "..v,"unserialise-temp",nil,{})if | |
not b then return nil end;local g,k=pcall(b)return g and k end;return{serialise=y,unserialise=p,deserialise=p,dump=m}end | |
a["howl.lib.colored"]=function(...)local n=i"howl.platform".term;local function s(r,...)n.setColor(r) | |
n.print(...)n.resetColor(r)end;local function h(r,d)n.setColor(r) | |
n.write(d)n.resetColor(r)end | |
return{printColor=s,writeColor=h}end | |
a["howl.lib.Buffer"]=function(...)local n=table.concat | |
local function s(r,d)local l=r.n+1;r[l]=d;r.n=l;return r end;local function h(r)return n(r)end;return | |
function()return{n=0,append=s,toString=h}end end | |
a["howl.lib.assert"]=function(...)local n,s,h,r=type,error,select,math.floor;local d=assert | |
local l=setmetatable({assert=d},{__call=function(m,...)return | |
d(...)end}) | |
local function u(n,m,f)if f then return s(f:format(n))else | |
return s(m.." expected, got "..n)end end | |
function l.type(m,f,w)local y=n(m)if y~=f then return u(y,f,w)end end | |
local function c(n,m,f,w)return | |
s("bad argument #"..w.." for ".. | |
f.." (expected "..m..", got "..n..")")end | |
function l.argType(m,f,w,y)local p=n(m)if p~=f then return c(p,f,w,y)end end | |
function l.args(m,...)local f=r('#',...)local w={...} | |
for y=1,f,2 do local p=n(w[i])local v=w[i+1]if p~=v then return | |
c(p,v,m,math.floor(y/2))end end end;l.typeError=u;l.argError=c | |
function l.class(m,f,w)local y=n(m) | |
if | |
y~="table"or not m.isInstanceOf then return u(y,f,w)elseif not m:isInstanceOf(f)then return u(m.class.name,f,w)end end;return l end | |
a["howl.lib.argparse"]=function(...)local n=i"howl.lib.colored" | |
local s={__index=function(d,l) | |
return function(d,...)local u=d.parser | |
local c=u[l](u,d.name,...)if c==u then return d end;return c end end}local h={} | |
function h:Get(d,l)local u=self.options;local c=u[d]if c~=nil then return c end | |
local m=self.settings[d] | |
if m then local f=m.aliases;if f then | |
for w,y in ipairs(f)do c=u[y]if c~=nil then return c end end end;c=m.default;if c~=nil then return c end end;return l end;function h:Ensure(d)local l=self:Get(d) | |
if l==nil then error(d.." must be set")end;return l end;function h:Default(d,l) | |
if l==nil then l=true end;self:_SetSetting(d,"default",l)self:_Changed() | |
return self end | |
function h:Alias(d,l) | |
local u=self.settings;local c=u[d]if c then local m=c.aliases | |
if m==nil then c.aliases={l}else table.insert(m,l)end else u[d]={aliases={l}}end | |
self:_Changed()return self end | |
function h:Description(d,l)return self:_SetSetting(d,"description",l)end;function h:TakesValue(d,l)if l==nil then l=true end | |
return self:_SetSetting(d,"takesValue",l)end | |
function h:_SetSetting(d,l,u)local c=self.settings | |
local m=c[d]if m then m[l]=u else c[d]={[l]=u}end;return self end | |
function h:Option(d)return setmetatable({name=d,parser=self},s)end;function h:Arguments()return self.arguments end;function h:_Changed() | |
self.mediator:publish({"ArgParse","changed"},self)end | |
function h:Help(d) | |
for l,u in pairs(self.settings)do local c='-'if | |
u.takesValue then c="--"l=l.."=value"end;if#l>1 then c='--'end;n.writeColor("white",d..c.. | |
l)local m=""local f=u.aliases | |
if f and#f>0 then local y=#f | |
m=m.." ("for p=1,y do local v="-"..f[p]if#v>2 then v="-"..v end;if p<y then v=v..', 'end | |
m=m..v end;m=m..")"end;n.writeColor("brown",m)local w=u.description;if w and w~=""then | |
n.printColor("lightGray"," "..w)end end end | |
function h:Parse(d)local l=self.options;local u=self.arguments | |
for c,m in ipairs(d)do | |
if m:sub(1,1)=="-"then | |
if | |
m:sub(2,2)=="-"then local f,w=m:match("([%w_%-]+)=([%w_%-]+)",3)if f then l[f]=w else | |
m=m:sub(3)local y=m:sub(1,4)local w=true | |
if y=="not-"or y=="not_"then w=false;m=m:sub(5)end;l[m]=w end else for f=2,#m do | |
l[m:sub(f,f)]=true end end else table.insert(u,m)end end;return self end | |
local function r(d,l)return | |
setmetatable({options={},arguments={},mediator=d,settings={}},{__index=h}):Parse(l)end;return{Parser=h,Options=r}end | |
a["howl.lexer.walk"]=function(...)local function n()end;local function s(l,u)u(l.Base) | |
for c,m in ipairs(l.Arguments)do u(m)end end | |
local function h(l,u)u(l.Base)u(l.Index)end;local r | |
local function d(l,u)local c=r[l.AstType]if not c then | |
error("No visitor for "..l.AstType)end;c(l,u)end | |
r={VarExpr=n,NumberExpr=n,StringExpr=n,BooleanExpr=n,NilExpr=n,DotsExpr=n,Eof=n,BinopExpr=function(l,u)u(l.Lhs)u(l.Rhs)end,UnopExpr=function(l,u)u(l.Rhs)end,CallExpr=s,TableCallExpr=s,StringCallExpr=s,IndexExpr=h,MemberExpr=h,Function=function(l,u)if l.Name and not | |
l.IsLocal then u(l.Name)end;u(l.Body)end,ConstructorExpr=function(l,u) | |
for c,m in | |
ipairs(l.EntryList)do if m.Type=="Key"then u(m.Key)end;u(m.Value)end end,Parentheses=function(l,u)u(v.Inner)end,Statlist=function(l,u)for c,m in | |
ipairs(l.Body)do u(m)end end,ReturnStatement=function(l,u) | |
for c,m in ipairs(l.Arguments)do u(m)end end,AssignmentStatement=function(l,u)for c,m in ipairs(l.Lhs)do u(m)end;for c,m in | |
ipairs(l.Rhs)do u(m)end end,LocalStatement=function(l,u)for c,m in | |
ipairs(l.InitList)do u(m)end end,CallStatement=function(l,u)u(v.Expression)end,IfStatement=function(l,u) | |
for c,m in | |
ipairs(l.Clauses)do if m.Condition then u(m.Condition)end;u(m.Body)end end,WhileStatement=function(l,u)u(l.Condition) | |
u(l.Body)end,DoStatement=function(l,u)u(l.Body)end,BreakStatement=n,LabelStatement=n,GotoStatement=n,RepeatStatement=function(l,u)u(l.Body) | |
u(l.Condition)end,GenericForStatement=function(l,u)for c,m in ipairs(l.Generators)do u(m)end | |
u(l.Body)end,NumericForStatement=function(l,u)u(l.Start)u(l.End)if l.Step then | |
u(l.Step)end;u(l.Body)end}return d end | |
a["howl.lexer.TokenList"]=function(...)local n=math.min;local s=table.insert | |
return | |
function(h)local r=#h;local d=1;local function l(b)return h[n(r,d+ | |
(b or 0))]end;local function u(b)local g=h[d] | |
d=n(d+1,r)if b then s(b,g)end;return g end;local function c(b) | |
return l().Type==b end | |
local function m(b,g)local k=l() | |
if k.Type=='Symbol'then if b then if k.Data==b then if g then s(g,k)end | |
d=d+1;return true else return nil end else if g then s(g,k)end;d=d+1 | |
return k end else return nil end end | |
local function f(b,g)local k=l()if k.Type=='Keyword'and k.Data==b then if g then s(g,k)end;d=d+1 | |
return true else return nil end end | |
local function w(b)local g=l()return g.Type=='Keyword'and g.Data==b end | |
local function y(b)local g=l()return g.Type=='Symbol'and g.Data==b end;local function p()return l().Type=='Eof'end | |
local function v(b) | |
b=(b==nil and true or b)local g="" | |
for k,q in ipairs(h)do if b then | |
for k,j in ipairs(q.LeadingWhite)do g=g..j:Print().."\n"end end;g=g..q:Print().."\n"end;return g end | |
return{Peek=l,Get=u,Is=c,ConsumeSymbol=m,ConsumeKeyword=f,IsKeyword=w,IsSymbol=y,IsEof=p,Print=v,Tokens=h}end end | |
a["howl.lexer.Scope"]=function(...)local n=i"howl.lexer.constants".Keywords;local s={}function s:AddLocal(r,d) | |
table.insert(self.Locals,d)self.LocalMap[r]=d end | |
function s:CreateLocal(r) | |
local d=self:GetLocal(r)if d then return d end | |
d={Scope=self,Name=r,IsGlobal=false,CanRename=true,References=1}self:AddLocal(r,d)return d end;function s:GetLocal(r) | |
repeat local d=self.LocalMap[r]if d then return d end;self=self.Parent until not self end;function s:GetOldLocal(r)if | |
self.oldLocalNamesMap[r]then return self.oldLocalNamesMap[r]end;return | |
self:GetLocal(r)end | |
function s:RenameLocal(r,d)r= | |
type(r)=='string'and r or r.Name | |
repeat | |
local l=self.LocalMap[r] | |
if l then l.Name=d;self.oldLocalNamesMap[r]=l | |
self.LocalMap[r]=nil;self.LocalMap[d]=l;break end;self=self.Parent until not self end;function s:AddGlobal(r,d)table.insert(self.Globals,d) | |
self.GlobalMap[r]=d end | |
function s:CreateGlobal(r) | |
local d=self:GetGlobal(r)if d then return d end | |
d={Scope=self,Name=r,IsGlobal=true,CanRename=true,References=1}self:AddGlobal(r,d)return d end | |
function s:GetGlobal(r)repeat local d=self.GlobalMap[r]if d then return d end;self=self.Parent until | |
not self end;function s:GetVariable(r) | |
return self:GetLocal(r)or self:GetGlobal(r)end;function s:GetAllVariables()return | |
self:getVars(true,self:getVars(true))end | |
function s:getVars(r,d) | |
local d=d or{} | |
if r then for l,u in pairs(self.Children)do u:getVars(true,d)end else for l,u in | |
pairs(self.Locals)do table.insert(d,u)end;for l,u in pairs(self.Globals)do | |
table.insert(d,u)end;if self.Parent then | |
self.Parent:getVars(false,d)end end;return d end | |
function s:ObfuscateLocals(r) | |
local d=r or"etaoinshrdlucmfwypvbgkqjxz_ETAOINSHRDLUCMFWYPVBGKQJXZ" | |
local l=r or"etaoinshrdlucmfwypvbgkqjxz_0123456789ETAOINSHRDLUCMFWYPVBGKQJXZ"local u,c=#d,#l;local m=0;local f=math.floor | |
for w,y in pairs(self.Locals)do local p | |
repeat | |
if m<u then m=m+1 | |
p=d:sub(m,m)else | |
if m<u then m=m+1;p=d:sub(m,m)else local v=f(m/u)local b=m%u;p=d:sub(b,b)while v>0 do b=v%c;p= | |
l:sub(b,b)..p;v=f(v/c)end;m=m+1 end end until not(n[p]or self:GetVariable(p))self:RenameLocal(y.Name,p)end end;function s:ToString()return'<Scope>'end | |
local function h(r) | |
local d=setmetatable({Parent=r,Locals={},LocalMap={},Globals={},GlobalMap={},oldLocalNamesMap={},Children={}},{__index=s})if r then table.insert(r.Children,d)end;return d end;return h end | |
a["howl.lexer.rebuild"]=function(...)local n=i"howl.lexer.constants"local s=i"howl.lexer.parse" | |
local h=i"howl.platform"local r=n.LowerChars;local d=n.UpperChars;local l=n.Digits;local u=n.Symbols | |
local function c(y,p,v)v=v or' ' | |
local b,g=y:sub(-1,-1),p:sub(1,1) | |
if d[b]or r[b]or b=='_'then | |
if not | |
(g=='_'or d[g]or r[g]or l[g])then return y..p else return y..v..p end elseif l[b]then | |
if g=='('then return y..p elseif u[g]then return y..p else return y..v..p end elseif b==''then return y..p else if g=='('then return y..v..p else return y..p end end end | |
local function m(y)local p,v;local b=0;local function g(q,j,x) | |
if b>150 then b=0;return q.."\n"..j else return c(q,j,x)end end | |
v=function(q,j)local j=j or 0;local x=0;local z=false;local _="" | |
if | |
q.AstType=='VarExpr'then | |
if q.Variable then _=_..q.Variable.Name else _=_..q.Name end elseif q.AstType=='NumberExpr'then _=_..q.Value.Data elseif q.AstType=='StringExpr'then _=_.. | |
q.Value.Data elseif q.AstType=='BooleanExpr'then | |
_=_..tostring(q.Value)elseif q.AstType=='NilExpr'then _=g(_,"nil")elseif q.AstType=='BinopExpr'then | |
x=q.OperatorPrecedence;_=g(_,v(q.Lhs,x))_=g(_,q.Op)_=g(_,v(q.Rhs))if q.Op=='^'or q.Op== | |
'..'then x=x-1 end;if x<j then z=false else z=true end elseif | |
q.AstType=='UnopExpr'then _=g(_,q.Op)_=g(_,v(q.Rhs))elseif q.AstType=='DotsExpr'then _=_.. | |
"..."elseif q.AstType=='CallExpr'then _=_..v(q.Base)_=_.."("for E=1,#q.Arguments do _= | |
_..v(q.Arguments[E]) | |
if E~=#q.Arguments then _=_..","end end;_=_..")"elseif | |
q.AstType=='TableCallExpr'then _=_..v(q.Base)_=_..v(q.Arguments[1])elseif q.AstType== | |
'StringCallExpr'then _=_..v(q.Base) | |
_=_..q.Arguments[1].Data elseif q.AstType=='IndexExpr'then _=_.. | |
v(q.Base).."["..v(q.Index).."]"elseif q.AstType=='MemberExpr'then _=_..v(q.Base)..q.Indexer.. | |
q.Ident.Data elseif q.AstType== | |
'Function'then q.Scope:ObfuscateLocals()_=_.."function(" | |
if# | |
q.Arguments>0 then for E=1,#q.Arguments do _=_..q.Arguments[E].Name | |
if E~=# | |
q.Arguments then _=_..","elseif q.VarArg then _=_..",..."end end elseif q.VarArg then | |
_=_.."..."end;_=_..")"_=g(_,p(q.Body))_=g(_,"end")elseif | |
q.AstType=='ConstructorExpr'then _=_.."{" | |
for E=1,#q.EntryList do local T=q.EntryList[E] | |
if T.Type=='Key'then _=_.."["..v(T.Key).."]=".. | |
v(T.Value)elseif | |
T.Type=='Value'then _=_..v(T.Value)elseif T.Type=='KeyString'then _=_..T.Key.. | |
"="..v(T.Value)end;if E~=#q.EntryList then _=_..","end end;_=_.."}"elseif q.AstType=='Parentheses'then | |
_=_.."("..v(q.Inner)..")"end;if not z then | |
_=string.rep('(',q.ParenCount or 0).._ | |
_=_..string.rep(')',q.ParenCount or 0)end;b=b+#_;return _ end | |
local k=function(q)local j='' | |
if q.AstType=='AssignmentStatement'then for x=1,#q.Lhs do j=j..v(q.Lhs[x])if x~=# | |
q.Lhs then j=j..","end end | |
if | |
#q.Rhs>0 then j=j.."="for x=1,#q.Rhs do j=j..v(q.Rhs[x]) | |
if x~=#q.Rhs then j=j..","end end end elseif q.AstType=='CallStatement'then j=v(q.Expression)elseif | |
q.AstType=='LocalStatement'then j=j.."local " | |
for x=1,#q.LocalList do | |
j=j..q.LocalList[x].Name;if x~=#q.LocalList then j=j..","end end;if#q.InitList>0 then j=j.."=" | |
for x=1,#q.InitList do | |
j=j..v(q.InitList[x])if x~=#q.InitList then j=j..","end end end elseif | |
q.AstType=='IfStatement'then j=g("if",v(q.Clauses[1].Condition)) | |
j=g(j,"then")j=g(j,p(q.Clauses[1].Body)) | |
for x=2,#q.Clauses do | |
local z=q.Clauses[x]if z.Condition then j=g(j,"elseif")j=g(j,v(z.Condition)) | |
j=g(j,"then")else j=g(j,"else")end | |
j=g(j,p(z.Body))end;j=g(j,"end")elseif q.AstType=='WhileStatement'then | |
j=g("while",v(q.Condition))j=g(j,"do")j=g(j,p(q.Body))j=g(j,"end")elseif | |
q.AstType=='DoStatement'then j=g(j,"do")j=g(j,p(q.Body))j=g(j,"end")elseif | |
q.AstType=='ReturnStatement'then j="return" | |
for x=1,#q.Arguments do j=g(j,v(q.Arguments[x]))if x~=# | |
q.Arguments then j=j..","end end elseif q.AstType=='BreakStatement'then j="break"elseif q.AstType=='RepeatStatement'then j="repeat" | |
j=g(j,p(q.Body))j=g(j,"until")j=g(j,v(q.Condition))elseif q.AstType=='Function'then | |
q.Scope:ObfuscateLocals()if q.IsLocal then j="local"end;j=g(j,"function ")if q.IsLocal then | |
j=j..q.Name.Name else j=j..v(q.Name)end;j=j.."(" | |
if | |
#q.Arguments>0 then | |
for x=1,#q.Arguments do j=j..q.Arguments[x].Name;if | |
x~=#q.Arguments then j=j..","elseif q.VarArg then j=j..",..."end end elseif q.VarArg then j=j.."..."end;j=j..")"j=g(j,p(q.Body))j=g(j,"end")elseif | |
q.AstType=='GenericForStatement'then q.Scope:ObfuscateLocals()j="for "for x=1,#q.VariableList do j=j.. | |
q.VariableList[x].Name | |
if x~=#q.VariableList then j=j..","end end;j=j.." in" | |
for x=1,#q.Generators do | |
j=g(j,v(q.Generators[x]))if x~=#q.Generators then j=g(j,',')end end;j=g(j,"do")j=g(j,p(q.Body))j=g(j,"end")elseif | |
q.AstType=='NumericForStatement'then q.Scope:ObfuscateLocals()j="for " | |
j=j..q.Variable.Name.."="j=j..v(q.Start)..","..v(q.End)if q.Step then j=j..",".. | |
v(q.Step)end;j=g(j,"do") | |
j=g(j,p(q.Body))j=g(j,"end")elseif q.AstType=='LabelStatement'then | |
j="::"..q.Label.."::"elseif q.AstType=='GotoStatement'then j="goto "..q.Label elseif q.AstType=='Comment'then elseif q.AstType== | |
'Eof'then else | |
error("Unknown AST Type: "..q.AstType)end;b=b+#j;return j end | |
p=function(q)local j=''q.Scope:ObfuscateLocals()for x,z in pairs(q.Body)do | |
j=g(j,k(z),';')end;return j end;return p(y)end | |
local function f(y)local p=s.LexLua(y)h.refreshYield()local v=s.ParseLua(p) | |
h.refreshYield()local b=m(v)h.refreshYield()return b end;local function w(y,p,v)v=v or p;local b=h.fs.read(h.fs.combine(y,p))local g=f(b) | |
h.fs.write(h.fs.combine(y,v),g)return#b,#g end;return | |
{minify=m,minifyString=f,minifyFile=w}end | |
a["howl.lexer.parse"]=function(...)local n=i"howl.lexer.constants"local s=i"howl.lexer.Scope" | |
local h=i"howl.lexer.TokenList"local r=n.LowerChars;local d=n.UpperChars;local l=n.Digits;local u=n.Symbols;local c=n.HexDigits | |
local m=n.Keywords;local f=n.StatListCloseKeywords;local w=n.UnOps;local y,p=table.insert,setmetatable | |
local v={} | |
function v:Print()return | |
"<".. (self.Type.. | |
string.rep(' ',math.max(3,12-#self.Type))).." ".. | |
(self.Data or'').." >"end;local b={__index=v} | |
local function g(q)local j={} | |
do local z=string.sub;local _=1;local E=1;local T=1 | |
local function A()local R=z(q,_,_)if R=='\n'then | |
T=1;E=E+1 else T=T+1 end;_=_+1;return R end;local function O(R)R=R or 0;return z(q,_+R,_+R)end | |
local function I(R)local D=O()for L=1,#R do if | |
D==R:sub(L,L)then return A()end end end;local function N(R) | |
error(">> :"..E..":"..T..": "..R,0)end | |
local function S()local R=_ | |
if O()=='['then local D=0;local L=1 | |
while O(D+1)=='='do D=D+1 end | |
if O(D+1)=='['then for F=0,D+1 do A()end;local U=_ | |
while true do if O()==''then | |
N("Expected `]"..string.rep('=',D).. | |
"]` near <eof>.",3)end;local F=true | |
if O()==']'then for W=1,D do | |
if O(W)~='='then F=false end end;if O(D+1)~=']'then F=false end else | |
if O()=='['then local W=true;for Y=1,D do if | |
O(Y)~='='then W=false;break end end;if | |
O(D+1)=='['and W then L=L+1;for Y=1,(D+2)do A()end end end;F=false end | |
if F then L=L-1;if L==0 then break else for W=1,D+2 do A()end end else A()end end;local C=q:sub(U,_-1)for F=0,D+1 do A()end;local M=q:sub(R,_-1)return C,M else return nil end else return nil end end;local function H(R)return R>='0'and R<='9'end | |
while true do local R,D | |
while true do local F=z(q,_,_) | |
if F== | |
'#'and O(1)=='!'and E==1 then A()A()leadingWhite="#!"while O()~='\n'and | |
O()~=''do A()end end | |
if F==' 'or F=='\t'then T=T+1;_=_+1 elseif F=='\n'or F=='\r'then T=1;E=E+1;_=_+1 elseif F=='-'and | |
O(1)=='-'then A()A()local W,Y,P=E,T,_;local V,B=S() | |
if not V then local G=z(q,_,_)while G~='\n'and G~=''do A() | |
G=z(q,_,_)end;V=z(q,P,_-1)end;if not R then R={}D=0 end;D=D+1;R[D]={Data=V,Line=W,Char=Y}else break end end;local L=E;local U=T;local C=z(q,_,_)local M=nil | |
if C==''then M={Type='Eof'}elseif | |
(C>='A'and C<='Z')or(C>='a'and C<='z')or C=='_'then local F=_ | |
repeat A()C=z(q,_,_)until not | |
( | |
(C>='A'and C<='Z')or(C>='a'and C<='z')or C=='_'or(C>='0'and C<='9'))local W=q:sub(F,_-1)if m[W]then M={Type='Keyword',Data=W}else | |
M={Type='Ident',Data=W}end elseif(C>='0'and C<='9')or | |
(C=='.'and l[O(1)])then local F=_ | |
if C=='0'and O(1)=='x'then A()A()while c[O()]do A()end;if | |
I('Pp')then I('+-')while l[O()]do A()end end else while l[O()]do A()end;if | |
I('.')then while l[O()]do A()end end | |
if I('Ee')then I('+-')if not l[O()]then | |
N("Expected exponent")end;repeat A()until not l[O()]end;local W=O():lower()if(W>='a'and W<='z')or W=='_'then | |
N("Invalid number format")end end;M={Type='Number',Data=q:sub(F,_-1)}elseif C=='\''or C=='\"'then | |
local F=_;local W=A()local Y=_ | |
while true do local C=A()if C=='\\'then A()elseif C==W then break elseif C==''then | |
N("Unfinished string near <eof>")end end;local P=q:sub(Y,_-2)local V=q:sub(F,_-1) | |
M={Type='String',Data=V,Constant=P}elseif C=='['then local F,W=S()if W then M={Type='String',Data=W,Constant=F}else A() | |
M={Type='Symbol',Data='['}end elseif | |
C=='>'or C=='<'or C=='='then A() | |
if I('=')then M={Type='Symbol',Data=C..'='}else M={Type='Symbol',Data=C}end elseif C=='~'then A()if I('=')then M={Type='Symbol',Data='~='}else | |
N("Unexpected symbol `~` in source.",2)end elseif C=='.'then A()if I('.')then if I('.')then | |
M={Type='Symbol',Data='...'}else M={Type='Symbol',Data='..'}end else | |
M={Type='Symbol',Data='.'}end elseif C==':'then A() | |
if | |
I(':')then M={Type='Symbol',Data='::'}else M={Type='Symbol',Data=':'}end elseif u[C]then A()M={Type='Symbol',Data=C}else local F,W=S()if F then | |
M={Type='String',Data=W,Constant=F}else | |
N("Unexpected Symbol `"..C.."` in source.",2)end end;M.Line=L;M.Char=U;if R then M.Comments=R end;j[#j+1]=M | |
if M.Type=='Eof'then break end end end;local x=h(j)return x end | |
local function k(q,j) | |
local function x(H) | |
local R=q.Peek().Line..":"..q.Peek().Char..": "..H.."\n"local D=q.Peek()R=R.. | |
" got "..D.Type..": "..D.Data.."\n"local L=0 | |
if type(j)=='string'then | |
for U in | |
j:gmatch("[^\n]*\n?")do if U:sub(-1,-1)=='\n'then U=U:sub(1,-2)end;L=L+1;if L== | |
q.Peek().Line then | |
R=R..""..U:gsub('\t',' ').."\n"for C=1,q.Peek().Char do local M=U:sub(C,C)R=R..' 'end | |
R=R.."^"break end end end;error(R)end;local z,_,E,T,A | |
local function O(H,R)local D=s(H)if not q.ConsumeSymbol('(',R)then | |
x("`(` expected.")end;local L={}local U=false | |
while | |
not q.ConsumeSymbol(')',R)do | |
if q.Is('Ident')then local M=D:CreateLocal(q.Get(R).Data) | |
L[#L+1]=M | |
if not q.ConsumeSymbol(',',R)then if q.ConsumeSymbol(')',R)then break else | |
x("`)` expected.")end end elseif q.ConsumeSymbol('...',R)then U=true;if not q.ConsumeSymbol(')',R)then | |
x("`...` must be the last argument of a function.")end;break else | |
x("Argument name or `...` expected")end end;local C=_(D)if not q.ConsumeKeyword('end',R)then | |
x("`end` expected after function body")end;return | |
{AstType='Function',Scope=D,Arguments=L,Body=C,VarArg=U,Tokens=R}end | |
function T(H)local R={} | |
if q.ConsumeSymbol('(',R)then local D=z(H)if not q.ConsumeSymbol(')',R)then | |
x("`)` Expected.")end | |
return{AstType='Parentheses',Inner=D,Tokens=R}elseif q.Is('Ident')then local D=q.Get(R)local L=H:GetLocal(D.Data) | |
if not L then | |
L=H:GetGlobal(D.Data) | |
if not L then L=H:CreateGlobal(D.Data)else L.References=L.References+1 end else L.References=L.References+1 end;return{AstType='VarExpr',Name=D.Data,Variable=L,Tokens=R}else | |
x("primary expression expected")end end | |
function A(H,R)local D=T(H) | |
while true do local L={} | |
if q.IsSymbol('.')or q.IsSymbol(':')then | |
local U=q.Get(L).Data | |
if not q.Is('Ident')then x("<Ident> expected.")end;local C=q.Get(L) | |
D={AstType='MemberExpr',Base=D,Indexer=U,Ident=C,Tokens=L}elseif not R and q.ConsumeSymbol('[',L)then local U=z(H)if | |
not q.ConsumeSymbol(']',L)then x("`]` expected.")end | |
D={AstType='IndexExpr',Base=D,Index=U,Tokens=L}elseif not R and q.ConsumeSymbol('(',L)then local U={} | |
while | |
not q.ConsumeSymbol(')',L)do U[#U+1]=z(H) | |
if not q.ConsumeSymbol(',',L)then if q.ConsumeSymbol(')',L)then break else | |
x("`)` Expected.")end end end;D={AstType='CallExpr',Base=D,Arguments=U,Tokens=L}elseif | |
not R and q.Is('String')then | |
D={AstType='StringCallExpr',Base=D,Arguments={q.Get(L)},Tokens=L}elseif not R and q.IsSymbol('{')then local U=E(H) | |
D={AstType='TableCallExpr',Base=D,Arguments={U},Tokens=L}else break end end;return D end | |
function E(H)local R={}local D=q.Peek()local L=D.Type | |
if L=='Number'then return | |
{AstType='NumberExpr',Value=q.Get(R),Tokens=R}elseif L=='String'then | |
return{AstType='StringExpr',Value=q.Get(R),Tokens=R}elseif L=='Keyword'then local U=D.Data | |
if U=='nil'then q.Get(R) | |
return{AstType='NilExpr',Tokens=R}elseif U=='false'or U=='true'then return | |
{AstType='BooleanExpr',Value=(q.Get(R).Data=='true'),Tokens=R}elseif U=='function'then q.Get(R)local C=O(H,R) | |
C.IsLocal=true;return C end elseif L=='Symbol'then local U=D.Data | |
if U=='...'then q.Get(R) | |
return{AstType='DotsExpr',Tokens=R}elseif U=='{'then q.Get(R)local C={} | |
local M={AstType='ConstructorExpr',EntryList=C,Tokens=R} | |
while true do | |
if q.IsSymbol('[',R)then q.Get(R)local F=z(H)if not q.ConsumeSymbol(']',R)then | |
x("`]` Expected")end;if not q.ConsumeSymbol('=',R)then | |
x("`=` Expected")end;local W=z(H) | |
C[#C+1]={Type='Key',Key=F,Value=W}elseif q.Is('Ident')then local F=q.Peek(1) | |
if F.Type=='Symbol'and F.Data=='='then | |
local W=q.Get(R) | |
if not q.ConsumeSymbol('=',R)then x("`=` Expected")end;local Y=z(H)C[#C+1]={Type='KeyString',Key=W.Data,Value=Y}else | |
local W=z(H)C[#C+1]={Type='Value',Value=W}end elseif q.ConsumeSymbol('}',R)then break else local F=z(H)C[#C+1]={Type='Value',Value=F}end | |
if q.ConsumeSymbol(';',R)or q.ConsumeSymbol(',',R)then elseif | |
q.ConsumeSymbol('}',R)then break else x("`}` or table entry Expected")end end;return M end end;return A(H)end;local I=8 | |
local N={['+']={6,6},['-']={6,6},['%']={7,7},['/']={7,7},['*']={7,7},['^']={10,9},['..']={5,4},['==']={3,3},['<']={3,3},['<=']={3,3},['~=']={3,3},['>']={3,3},['>=']={3,3},['and']={2,2},['or']={1,1}} | |
function z(H,R)R=R or 0;local D | |
if w[q.Peek().Data]then local L={}local U=q.Get(L).Data;D=z(H,I) | |
local C={AstType='UnopExpr',Rhs=D,Op=U,OperatorPrecedence=I,Tokens=L}D=C else D=E(H)end | |
while true do local L=N[q.Peek().Data] | |
if L and L[1]>R then local U={} | |
local C=q.Get(U).Data;local M=z(H,L[2]) | |
local F={AstType='BinopExpr',Lhs=D,Op=C,OperatorPrecedence=L[1],Rhs=M,Tokens=U}D=F else break end end;return D end | |
local function S(H)local R=nil;local D={}local L=q.Peek() | |
if L.Type=="Keyword"then local U=L.Data | |
if U=='if'then q.Get(D) | |
local C={}local M={AstType='IfStatement',Clauses=C} | |
repeat local F=z(H) | |
if not | |
q.ConsumeKeyword('then',D)then x("`then` expected.")end;local W=_(H)C[#C+1]={Condition=F,Body=W}until | |
not q.ConsumeKeyword('elseif',D) | |
if q.ConsumeKeyword('else',D)then local F=_(H)C[#C+1]={Body=F}end | |
if not q.ConsumeKeyword('end',D)then x("`end` expected.")end;M.Tokens=D;R=M elseif U=='while'then q.Get(D)local C=z(H)if | |
not q.ConsumeKeyword('do',D)then return x("`do` expected.")end;local M=_(H)if not | |
q.ConsumeKeyword('end',D)then x("`end` expected.")end | |
R={AstType='WhileStatement',Condition=C,Body=M,Tokens=D}elseif U=='do'then q.Get(D)local C=_(H)if not q.ConsumeKeyword('end',D)then | |
x("`end` expected.")end | |
R={AstType='DoStatement',Body=C,Tokens=D}elseif U=='for'then q.Get(D) | |
if not q.Is('Ident')then x("<ident> expected.")end;local C=q.Get(D) | |
if q.ConsumeSymbol('=',D)then local M=s(H) | |
local F=M:CreateLocal(C.Data)local W=z(H) | |
if not q.ConsumeSymbol(',',D)then x("`,` Expected")end;local Y=z(H)local P;if q.ConsumeSymbol(',',D)then P=z(H)end;if not | |
q.ConsumeKeyword('do',D)then x("`do` expected")end | |
local V=_(M) | |
if not q.ConsumeKeyword('end',D)then x("`end` expected")end | |
R={AstType='NumericForStatement',Scope=M,Variable=F,Start=W,End=Y,Step=P,Body=V,Tokens=D}else local M=s(H)local F={M:CreateLocal(C.Data)} | |
while | |
q.ConsumeSymbol(',',D)do | |
if not q.Is('Ident')then x("for variable expected.")end;F[#F+1]=M:CreateLocal(q.Get(D).Data)end | |
if not q.ConsumeKeyword('in',D)then x("`in` expected.")end;local W={z(H)}while q.ConsumeSymbol(',',D)do W[#W+1]=z(H)end;if not | |
q.ConsumeKeyword('do',D)then x("`do` expected.")end | |
local Y=_(M) | |
if not q.ConsumeKeyword('end',D)then x("`end` expected.")end | |
R={AstType='GenericForStatement',Scope=M,VariableList=F,Generators=W,Body=Y,Tokens=D}end elseif U=='repeat'then q.Get(D)local C=_(H)if not q.ConsumeKeyword('until',D)then | |
x("`until` expected.")end;local M=z(C.Scope) | |
R={AstType='RepeatStatement',Condition=M,Body=C,Tokens=D}elseif U=='function'then q.Get(D)if not q.Is('Ident')then | |
x("Function name expected")end;local C=A(H,true)local M=O(H,D)M.IsLocal=false | |
M.Name=C;R=M elseif U=='local'then q.Get(D) | |
if q.Is('Ident')then local C={q.Get(D).Data} | |
while | |
q.ConsumeSymbol(',',D)do | |
if not q.Is('Ident')then x("local var name expected")end;C[#C+1]=q.Get(D).Data end;local M={}if q.ConsumeSymbol('=',D)then repeat M[#M+1]=z(H)until | |
not q.ConsumeSymbol(',',D)end;for F,W in pairs(C)do | |
C[F]=H:CreateLocal(W)end | |
R={AstType='LocalStatement',LocalList=C,InitList=M,Tokens=D}elseif q.ConsumeKeyword('function',D)then if not q.Is('Ident')then | |
x("Function name expected")end;local C=q.Get(D).Data | |
local M=H:CreateLocal(C)local F=O(H,D)F.Name=M;F.IsLocal=true;R=F else | |
x("local var or function def expected")end elseif U=='::'then q.Get(D) | |
if not q.Is('Ident')then x('Label name expected')end;local C=q.Get(D).Data;if not q.ConsumeSymbol('::',D)then | |
x("`::` expected")end | |
R={AstType='LabelStatement',Label=C,Tokens=D}elseif U=='return'then q.Get(D)local C={}if not q.IsKeyword('end')then | |
local M,F=pcall(function()return z(H)end) | |
if M then C[1]=F;while q.ConsumeSymbol(',',D)do C[#C+1]=z(H)end end end | |
R={AstType='ReturnStatement',Arguments=C,Tokens=D}elseif U=='break'then q.Get(D)R={AstType='BreakStatement',Tokens=D}elseif U=='goto'then | |
q.Get(D)if not q.Is('Ident')then x("Label expected")end | |
local C=q.Get(D).Data;R={AstType='GotoStatement',Label=C,Tokens=D}end end | |
if not R then local U=A(H) | |
if q.IsSymbol(',')or q.IsSymbol('=')then if | |
(U.ParenCount or 0)>0 then | |
x("Can not assign to parenthesized expression, is not an lvalue")end;local C={U}while | |
q.ConsumeSymbol(',',D)do C[#C+1]=A(H)end;if not q.ConsumeSymbol('=',D)then | |
x("`=` Expected.")end;local M={z(H)}while q.ConsumeSymbol(',',D)do | |
M[#M+1]=z(H)end | |
R={AstType='AssignmentStatement',Lhs=C,Rhs=M,Tokens=D}elseif U.AstType=='CallExpr'or U.AstType=='TableCallExpr'or | |
U.AstType=='StringCallExpr'then | |
R={AstType='CallStatement',Expression=U,Tokens=D}else x("Assignment Statement Expected")end end | |
if q.IsSymbol(';')then R.Semicolon=q.Get(R.Tokens)end;return R end | |
function _(H)local R={}local D={Scope=s(H),AstType='Statlist',Body=R,Tokens={}} | |
while not | |
f[q.Peek().Data]and not q.IsEof()do local L=S(D.Scope)R[#R+1]=L end | |
if q.IsEof()then local L={}L.AstType='Eof'L.Tokens={q.Get()}R[#R+1]=L end;return D end;return _(s())end;return{LexLua=g,ParseLua=k}end | |
a["howl.lexer.constants"]=function(...) | |
local function n(s)for h,r in ipairs(s)do s[r]=h end;return s end | |
return | |
{WhiteChars=n{' ','\n','\t','\r'},EscapeLookup={['\r']='\\r',['\n']='\\n',['\t']='\\t',['"']='\\"',["'"]="\\'"},LowerChars=n{'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'},UpperChars=n{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'},Digits=n{'0','1','2','3','4','5','6','7','8','9'},HexDigits=n{'0','1','2','3','4','5','6','7','8','9','A','a','B','b','C','c','D','d','E','e','F','f'},Symbols=n{'+','-','*','/','^','%',',','{','}','[',']','(',')',';','#'},Keywords=n{'and','break','do','else','elseif','end','false','for','function','goto','if','in','local','nil','not','or','repeat','return','then','true','until','while'},StatListCloseKeywords=n{'end','else','elseif','until'},UnOps=n{'-','not','#'}}end | |
a["howl.files.Source"]=function(...)local n=i"howl.lib.assert"local s=i"howl.class" | |
local h=i"howl.files.matcher"local r=i"howl.class.mixin"local d=i"howl.platform".fs;local l=table.insert | |
local u=s("howl.files.Source"):include(r.configurable):include(r.filterable) | |
local function c(w)local y=type(w) | |
if y=="function"or y=="string"then | |
return h.createMatcher(w)elseif y=="table"and w.tag and w.predicate then return w elseif | |
y=="table"and w.isInstanceOf and w:isInstanceOf(u)then return | |
h.createMatcher(function(p)return w:matches(p)end)else return nil end end | |
local function m(w,y,p,v)local b=c(y)local g=type(y) | |
if b then l(w,b)elseif g=="table"then | |
for v,k in ipairs(y)do local b=c(k)if b then l(w,b)else | |
error("bad item #"..v.. | |
" for "..p.." (expected pattern, got "..type(k)..")")end end else | |
error("bad argument #"..v.. | |
" for "..p.." (expected pattern, got "..g..")")end end;local function f(w,y)for p,v in pairs(w)do if v:match(y)then return true end end | |
return false end;function u:initialize(w,y) | |
if w==nil then w=true end;self.parent=y;self.children={}self.includes={}self.excludes={} | |
self.allowEmpty=w end | |
function u:from(w,y) | |
n.argType(w,"string","from",1)w=d.normalise(w)local p=self.children[w] | |
if not p then | |
p=self.class(true,self)self.children[w]=p;self.allowEmpty=false end;if y~=nil then return p:configureWith(y)else return p end end | |
function u:include(...)local w=select('#',...)local y={...}for p=1,w do | |
m(self.includes,y[p],"include",p)end;return self end | |
function u:exclude(...)local w=select('#',...)local y={...}for p=1,w do | |
m(self.excludes,y[p],"exclude",p)end;return self end | |
function u:excluded(w)if f(self.excludes,w)then return true elseif self.parent then | |
return self.parent:excluded(w)else return false end end | |
function u:included(w)if#self.includes==0 then return self.allowEmpty else | |
return f(self.includes,w)end end | |
function u:configure(w)n.argType(w,"table","configure",1)if w.include~=nil then | |
self:include(w.include)end | |
if w.exclude~=nil then self:exclude(w.exclude)end | |
if w.with~=nil then | |
n.type(w.with,"table","expected table for with, got %s")for y,p in ipairs(w.with)do self:with(p)end end end;function u:matches(w) | |
return self:included(w)and not self:excluded(w)end | |
function u:hasFiles()if | |
self.allowEmpty or#self.includes>0 then return true end;for w,y in pairs(self.children)do if y:hasFiles()then | |
return true end end;return false end | |
function u:gatherFiles(w,y,p)if not p then p={}end;for v,b in pairs(self.children)do local g=d.combine(w,v) | |
b:gatherFiles(g,y,p)end | |
if | |
self.allowEmpty or#self.includes>0 then local v,b={w},1;local g=#p | |
while b>0 do local k=v[b]local q=k;if w~=""then q=q:sub(#w+2)end | |
b=b-1 | |
if d.isDir(k)then | |
if not self:excluded(q)then if y and self:included(q)then g=g+1 | |
p[g]=self:buildFile(k,q)end;for j,x in ipairs(d.list(k))do b=b+1 | |
v[b]=d.combine(k,x)end end elseif self:included(q)and not self:excluded(q)then g=g+1 | |
p[g]=self:buildFile(k,q)end end end;return p end;function u:buildFile(w,y)return{path=w,relative=y,name=y}end;return u end | |
a["howl.files.matcher"]=function(...)local n=i"howl.lib.utils" | |
local s={["^"]="%^",["$"]="%$",["("]="%(",[")"]="%)",["%"]="%%",["."]="%.",["["]="%[",["]"]="%]",["+"]="%+",["-"]="%-",["\0"]="%z"}local h={["*"]="(.*)"}for c,m in pairs(s)do h[c]=m end;local function r(c,m) | |
return m:match(c.text)end;local function d(c,m) | |
return c.text==""or c.text==m or | |
m:sub(1,#c.text+1)==c.text.."/"end | |
local function l(c,m)return c.func(m)end | |
local function u(c)local m=type(c) | |
if m=="string"then | |
local f=n.startsWith(c,"pattern:")or n.startsWith(c,"ptrn:")if f then return{tag="pattern",text=f,match=r}end;if c:find("%*")then local c= | |
"^"..c:gsub(".",h).."$" | |
return{tag="pattern",text=c,match=r}end | |
return{tag="text",text=c,match=d}elseif m=="function"or | |
(m=="table"and(getmetatable(c)or{}).__call)then return{tag="function",func=c,match=l}else | |
error("Expected string or function")end end;return{createMatcher=u}end | |
a["howl.files.CopySource"]=function(...)local n=i"howl.lib.assert"local s=i"howl.files.matcher" | |
local h=i"howl.class.mixin"local r=i"howl.platform".fs;local d=i"howl.files.Source"local l=table.insert | |
local u=d:subclass("howl.files.CopySource") | |
function u:initialize(c,m)d.initialize(self,c,m)self.renames={}self.modifiers={}end | |
function u:configure(c)n.argType(c,"table","configure",1) | |
d.configure(self,c)if c.rename~=nil then self:rename(c.rename)end;if | |
c.modify~=nil then self:modify(c.modify)end end | |
function u:rename(c,m)local f,w=type(c),type(m) | |
if f=="table"and m==nil then for y,p in ipairs(c)do | |
self:rename(p)end elseif f=="function"and m==nil then l(self.renames,c)elseif f== | |
"string"and w=="string"then l(self.renames,function(y) | |
return(y.name:gsub(c,m))end)else | |
error( | |
"bad arguments for rename (expected table, function or string, string pair, got "..f.." and "..w..")",2)end end | |
function u:modify(c)local m=type(c) | |
if m=="table"then | |
for f,w in ipairs(c)do self:modify(w)end elseif m=="function"then l(self.modifiers,c)else | |
error("bad argument #1 for modify (expected table or function, got "..m.. | |
")",2)end end | |
function u:doMutate(c) | |
for m,f in ipairs(self.modifiers)do local w=f(c)if w then c.contents=w end end | |
for m,f in ipairs(self.renames)do local w=f(c)if w then c.name=w end end | |
if self.parent then return self.parent:doMutate(c)else return c end end | |
function u:buildFile(c,m)return | |
self:doMutate{path=c,relative=m,name=m,contents=r.read(c)}end;return u end | |
a["howl.context"]=function(...)local n=i"howl.lib.assert"local s=i"howl.class" | |
local h=i"howl.class.mixin"local r=i"howl.lib.mediator"local d=i"howl.lib.argparse"local l=i"howl.lib.Logger" | |
local u=i"howl.packages.Manager"local c=s("howl.Context"):include(h.sealed) | |
function c:initialize(m,f) | |
n.type(m,"string","bad argument #1 for Context expected string, got %s") | |
n.type(f,"table","bad argument #2 for Context expected table, got %s")self.root=m;self.out="build"self.mediator=r | |
self.arguments=d.Options(self.mediator,f)self.logger=l(self)self.packageManager=u(self)self.modules={}end | |
function c:include(m)if type(m)~="table"then m=i(m)end;if self.modules[m.name]then | |
self.logger:warn( | |
m.name.." already included, skipping")return end;local f={module=m} | |
self.modules[m.name]=f | |
self.logger:verbose("Including "..m.name..": "..m.description) | |
if not m.applied then m.applied=true;if m.apply then m.apply()end end;if m.setup then m.setup(self,f)end end;function c:getModuleData(m)return self.modules[m]end;return c end | |
a["howl.cli"]=function(...)local n=i"howl.loader"local s=i"howl.lib.colored" | |
local h=i"howl.platform".fs;local r,d=n.FindHowl() | |
local l=i"howl.context"(d or h.currentDir(),{...})local u=l.arguments | |
u:Option"verbose":Alias"v":Description"Print verbose output" | |
u:Option"time":Alias"t":Description"Display the time taken for tasks" | |
u:Option"trace":Description"Print a stack trace on errors" | |
u:Option"help":Alias"?":Alias"h":Description"Print this help"l:include"howl.modules.dependencies.file" | |
l:include"howl.modules.dependencies.task"l:include"howl.modules.list"l:include"howl.modules.plugins" | |
l:include"howl.modules.packages.file"l:include"howl.modules.packages.gist" | |
l:include"howl.modules.packages.pastebin"l:include"howl.modules.tasks.clean" | |
l:include"howl.modules.tasks.gist"l:include"howl.modules.tasks.minify" | |
l:include"howl.modules.tasks.pack"l:include"howl.modules.tasks.require"local c=u:Arguments()local function m()if u:Get"help"then | |
c={"help"}end end | |
l.mediator:subscribe({"ArgParse","changed"},m)m() | |
if not r then | |
if#c==1 and c[1]=="help"then | |
s.writeColor("yellow","Howl") | |
s.printColor("lightGrey"," is a simple build system for Lua") | |
s.printColor("grey","You can read the full documentation online: https://github.com/SquidDev-CC/Howl/wiki/") | |
s.printColor("white",(([[ | |
The key thing you are missing is a HowlFile. This can be "Howlfile" or "Howlfile.lua". | |
Then you need to define some tasks. Maybe something like this: | |
]]):gsub("\t",""):gsub("\n+$","")))s.printColor("magenta",'Tasks:minify "minify" {') | |
s.printColor("magenta",' input = "build/Howl.lua",') | |
s.printColor("magenta",' output = "build/Howl.min.lua",')s.printColor("magenta",'}') | |
s.printColor("white","Now just run '".. | |
h.getName(h.currentProgram()).." minify'!")s.printColor("orange","\nOptions:")u:Help(" ")elseif#c==0 then | |
error( | |
d.." Use ".. | |
h.getName(h.currentProgram()).." --help to dislay usage.",0)else error(d,0)end;return end | |
l.logger:verbose("Found HowlFile at "..h.combine(d,r))local f,w=n.SetupTasks(l,r) | |
f:Task"list"(function()f:listTasks()end):description"Lists all the tasks" | |
f:Task"help"(function()print("Howl [options] [task]") | |
s.printColor("orange","Tasks:")f:listTasks(" ") | |
s.printColor("orange","\nOptions:")u:Help(" ")end):description"Print out a detailed usage for Howl" | |
f:Default(function()l.logger:error("No default task exists.") | |
l.logger:verbose("Use 'Tasks:Default' to define a default task")s.printColor("orange","Choose from: ") | |
f:listTasks(" ")end)w.dofile(h.combine(d,r))if not f:setup()then | |
error("Error setting up tasks",0)end;if not f:RunMany(c)then | |
error("Error running tasks",0)end end | |
a["howl.class.mixin"]=function(...)local n=i"howl.lib.assert"local s=rawset;local h={} | |
h.sealed={static={subclass=function(r,d) | |
n(type(r)=='table',"Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") | |
n(type(d)=="string","You must provide a name(string) for your class") | |
error("Cannot subclass '".. | |
tostring(r).."' (attempting to create '"..d.."')",2)end}} | |
h.curry={curry=function(r,d) | |
n.type(r,"table","Bad argument #1 to class:curry (expected table, got %s)") | |
n.type(d,"string","Bad argument #2 to class:curry (expected string, got %s)")local l=r[d] | |
n.type(l,"function","No such function "..d)return function(...)return l(r,...)end end,__div=function(r,d)return | |
r:curry(d)end} | |
h.configurable={configureWith=function(r,d)local l=type(d)if l=="table"then r:configure(d)return r elseif l=="function"then d(r) | |
return r else | |
error("Expected table or function, got "..type(d),2)end;return r end,__call=function(r,...)return | |
r:configureWith(...)end} | |
h.filterable={__add=function(r,...)return r:include(...)end,__sub=function(r,...)return r:exclude(...)end,with=function(r,...)return | |
r:configure(...)end} | |
function h.delegate(r,d)local l={}for u,c in ipairs(d)do | |
l[c]=function(m,...)local f=m[r]return f[c](f,...)end end;return l end | |
h.optionGroup={static={addOption=function(r,d) | |
local l=function(r,u)if u==nil then u=true end;r.options[d]=u;return r end;r[d:gsub("^%l",string.upper)]=l;r[d]=l | |
if not | |
rawget(r.static,"options")then local u={}r.static.options=u | |
local c=r.super and r.super.static.options;if c then setmetatable(u,{__index=c})end end;r.static.options[d]=true;return r end,addOptions=function(r,d)for l=1, | |
#d do r:addOption(d[l])end;return r end},configure=function(r,d) | |
n.argType(d,"table","configure",1)local l=r.class;local u=l.options | |
while l and not u do u=l.options;l=l.super end;if not u then return end | |
for c,m in pairs(d)do if u[c]then r[c](r,m)end end end,__newindex=function(r,d,l) | |
if | |
r.class.options and r.class.options[d]then r[d](r,l)else s(r,d,l)end end}return h end | |
a["howl.class"]=function(...) | |
local n={_VERSION='middleclass v4.0.0',_DESCRIPTION='Object Orientation for Lua',_URL='https://github.com/kikito/middleclass',_LICENSE=[[ | |
MIT LICENSE | |
Copyright (c) 2011 Enrique García Cota | |
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. | |
]]} | |
local function s(w,y) | |
if y==nil then return w.__instanceDict else | |
return function(f,p)local v=w.__instanceDict[p] | |
if v~=nil then return v elseif type(y)== | |
"function"then return(y(f,p))else return y[p]end end end end | |
local function h(w,y,p)p=y=="__index"and s(w,p)or p | |
w.__instanceDict[y]=p;for f in pairs(w.subclasses)do | |
if rawget(f.__declaredMethods,y)==nil then h(f,y,p)end end end | |
local function r(w,y,p)w.__declaredMethods[y]=p;if p==nil and w.super then | |
p=w.super.__instanceDict[y]end;h(w,y,p)end;local function d(f)return"class "..f.name end | |
local function l(f,...)return f:new(...)end | |
local function u(f,w)local y={}y.__index=y | |
local p={name=f,super=w,static={},__instanceDict=y,__declaredMethods={},subclasses=setmetatable({},{__mode='k'})} | |
if w then | |
setmetatable(p.static,{__index=function(v,b)return rawget(y,b)or w.static[b]end})else | |
setmetatable(p.static,{__index=function(v,b)return rawget(y,b)end})end | |
setmetatable(p,{__index=p.static,__tostring=d,__call=l,__newindex=r})return p end | |
local function c(f,w) | |
assert(type(w)=='table',"mixin must be a table") | |
for y,p in pairs(w)do if y~="included"and y~="static"then f[y]=p end end;for y,p in pairs(w.static or{})do f.static[y]=p end;if | |
type(w.included)=="function"then w:included(f)end;return f end | |
local m={__tostring=function(f)return"instance of "..tostring(f.class)end,initialize=function(f,...) | |
end,isInstanceOf=function(f,w) | |
return type(f)=='table'and type(f.class)=='table'and | |
type(w)=='table'and | |
(w==f.class or type(w.isSubclassOf)== | |
'function'and f.class:isSubclassOf(w))end,static={allocate=function(f) | |
assert( | |
type(f)=='table',"Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")return setmetatable({class=f},f.__instanceDict)end,new=function(f,...) | |
assert( | |
type(f)=='table',"Make sure that you are using 'Class:new' instead of 'Class.new'")local w=f:allocate()w:initialize(...)return w end,subclass=function(f,w) | |
assert( | |
type(f)=='table',"Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") | |
assert(type(w)=="string","You must provide a name(string) for your class")local y=u(w,f)for p,v in pairs(f.__instanceDict)do h(y,p,v)end;y.initialize=function(p,...)return | |
f.initialize(p,...)end | |
f.subclasses[y]=true;f:subclassed(y)return y end,subclassed=function(f,w) | |
end,isSubclassOf=function(f,w) | |
return type(w)=='table'and type(f)=='table'and type(f.super)== | |
'table'and | |
(f.super==w or | |
type(f.super.isSubclassOf)=='function'and f.super:isSubclassOf(w))end,include=function(f,...) | |
assert( | |
type(f)=='table',"Make sure you that you are using 'Class:include' instead of 'Class.include'")for w,y in ipairs({...})do c(f,y)end;return f end}}return | |
function(f,w) | |
assert(type(f)=='string',"A name (string) is needed for the new class")return w and w:subclass(f)or c(u(f),m)end end | |
if not shell or type(...or nil)=='table'then local n=...or{} | |
n.require=i;n.preload=a;return n else return a["howl.cli"](...)end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment