[aegisub] bunch of scripts

This commit is contained in:
odrling 2020-06-29 18:45:05 +02:00
parent cca2f1f756
commit 758e2410f0
69 changed files with 24749 additions and 1 deletions

View file

@ -0,0 +1,75 @@
export script_name = 'Selegator'
export script_description = 'Select/navigate in the subtitle grid'
export script_author = 'tophf'
export script_version = '1.1.5'
export script_namespace = 'Flux.Selegator'
DependencyControl = require('l0.DependencyControl') {
url: 'https://github.com/TypesettingTools/CoffeeFlux-Aegisub-Scripts/blob/master/macros/Flux.Selegator.moon'
feed: 'https://raw.githubusercontent.com/TypesettingTools/CoffeeFlux-Aegisub-Scripts/master/DependencyControl.json'
{}
}
selectAll = (subs, sel, act) ->
lookforstyle = subs[act].style
if #sel>1
[i for i in *sel when subs[i].style==lookforstyle]
else
[k for k,s in ipairs subs when s.style==lookforstyle]
findPrevious = (subs, sel, act) ->
lookforstyle = subs[act].style
for i = act-1,1,-1
return if subs[i].class!='dialogue'
if subs[i].style==lookforstyle
return {i}
findNext = (subs, sel, act) ->
lookforstyle = subs[act].style
for i = act+1,#subs
if subs[i].style==lookforstyle
return {i}
firstInBlock = (subs, sel, act) ->
lookforstyle = subs[act].style
for i = act-1,1,-1
if subs[i].class!='dialogue' or subs[i].style!=lookforstyle
return {i+1}
lastInBlock = (subs, sel, act) ->
lookforstyle = subs[act].style
for i = act+1,#subs
if subs[i].style!=lookforstyle
return {i-1}
{#subs}
selectBlock = (subs, sel, act) ->
lookforstyle = subs[act].style
first, last = act, #subs
for i = act-1,1,-1
if subs[i].class!='dialogue' or subs[i].style!=lookforstyle
first = i + 1
break
for i = act+1,#subs
if subs[i].class!='dialogue' or subs[i].style!=lookforstyle
last = i - 1
break
[i for i=first,last]
untilStart = (subs, sel, act) ->
[i for i = 1,act when subs[i].class=='dialogue']
untilEnd = (subs, sel, act) ->
[i for i = act,#subs when subs[i].class=='dialogue']
DependencyControl\registerMacros {
{ 'Current Style/Select All', '', selectAll }
{ 'Current Style/Previous', '', findPrevious }
{ 'Current Style/Next', '', findNext }
{ 'Current Style/First In Block', '', firstInBlock }
{ 'Current Style/Last In Block', '', lastInBlock }
{ 'Current Style/Select Block', '', selectBlock }
{ 'Select Until Start', '', untilStart }
{ 'Select Until End', '', untilEnd }
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
local tr = aegisub.gettext
script_name = tr"Clean k tags"
script_description = tr"Remove double k tags"
script_author = "amoethyst"
script_version = "1.0"
function special_k(subs, sel)
-- if the first tag is K/kf this would break the timing for the previous timing
local expr = "^(.-){\\(ko?)([0-9.]*)[^}]-}([^{]-){\\[kK][fo]?([0-9.]*)[^}]-}( -{(\\[kK][fo]?)[0-9.]*[^}]-}.*)$"
for _, i in ipairs(sel) do
line = subs[i]
before, tag, k1, between, k2, after = line.text:match(expr)
while after ~= nil do
line.text = before .. "{\\" .. tag .. tonumber(k1) + tonumber(k2) .. "}" .. between .. after
subs[i] = line
before, tag, k1, between, k2, after = line.text:match(expr)
end
end
end
aegisub.register_macro(script_name, script_description, special_k)

View file

@ -0,0 +1,178 @@
local tr = aegisub.gettext
script_name = tr"Duetto Meika"
script_description = tr"The ultimate tool for karaoke duets"
script_author = "amoethyst"
include("utils.lua")
function replace_style(line, style_name, style_string)
before_style, after_style = line.text:match("^(.-{[^}]-)\\?s:".. style_name .."(.*)$")
return before_style .. style_string .. after_style
end
function duetto(subs, sel)
styles = {}
-- create the style map
for _, line in ipairs(subs) do
if line.class == "style" then
styles[line.name] = line
end
end
-- duetto~
for _, i in ipairs(sel) do
line = subs[i]
current_style = styles[line.style]
-- match every `s:` marker
for style_name in line.text:gmatch("{[^}]*s:([^}\\]*)[^}]*}") do
if style_name ~= current_style.name then
style = styles[style_name]
-- build the tags to use the new style
style_string = ""
if current_style.color1 ~= style.color1 then
style_string = style_string .. "\\c" .. style.color1
end
if current_style.color2 ~= style.color2 then
style_string = style_string .. "\\2c" .. style.color2
end
if current_style.color3 ~= style.color3 then
style_string = style_string .. "\\3c" .. style.color3
end
if current_style.color4 ~= style.color4 then
style_string = style_string .. "\\4c" .. style.color4
end
-- set style
line.text = replace_style(line, style_name, style_string)
current_style = style
else
-- remove marker to not break everything
line.text = replace_style(line, style_name, "")
end
end
subs[i] = line
end
aegisub.set_undo_point(script_name)
end
function test_colors(c1, c2)
return color_from_style(c1) == color_from_style(c2)
end
function get_script_style(style, styles)
for key, script_style in pairs(styles) do
if (test_colors(style.color1, script_style.color1)
and test_colors(style.color2, script_style.color2)
and test_colors(style.color3, script_style.color3)
and test_colors(style.color4, script_style.color4)
and tonumber(style.fontsize) == tonumber(script_style.fontsize)
and style.fontname == script_style.fontname) then
return script_style
end
end
return nil
end
function deduetto_meika(subs, sel)
local styles = {}
local last_style = -1
-- create the style map
for i, line in ipairs(subs) do
if line.class == "style" then
styles[line.name] = line
last_style = i
end
end
local new_styles = {}
for _, i in ipairs(sel) do
local line = subs[i]
local current_style = table.copy(styles[line.style])
local search_index = 1
while search_index < #line.text do
local match_start, match_end = line.text:find("{[^}]*}", search_index)
if match_start == nil then
break
end
local bracketed = line.text:sub(match_start, match_end)
local new_style = false
-- change style's colors
for tag, value in bracketed:gmatch("\\([1-4]?c)([^}\\]*)") do
new_style = true
if tag == "c" or tag == "1c" then
current_style.color1 = value
elseif tag == "2c" then
current_style.color2 = value
elseif tag == "3c" then
current_style.color3 = value
elseif tag == "4c" then
current_style.color4 = value
end
end
-- change style's font
for tag, value in bracketed:gmatch("\\(f[sn])([^}\\]*)") do
new_style = true
if tag == "fs" then
current_style.fontsize = value
elseif tag == "fn" then
current_style.fontname = value
end
end
if new_style then
aegisub.log("oof\n")
local script_style = get_script_style(current_style, styles)
if script_style == nil then
aegisub.log("foo\n")
if get_script_style(current_style, new_styles) == nil then
new_styles[#new_styles+1] = table.copy(current_style)
end
else
aegisub.log("ofo\n")
-- remove inline colors
bracketed = bracketed:gsub("\\[1-4]?c[^\\}]*", "")
bracketed = bracketed:gsub("\\[1-4]?a[^\\}]*", "")
-- remove inline fonts
bracketed = bracketed:gsub("\\f[sn][^\\}]*", "")
-- add style marker
bracketed = "{s:" .. script_style.name .. bracketed:sub(2, #bracketed)
line.text = line.text:sub(1, match_start-1) .. bracketed .. line.text:sub(match_end + 1, #line.text)
end
end
search_index = match_start + 1
end
subs[i] = line
end
if #new_styles > 0 then
for i, new_style in ipairs(new_styles) do
new_style.name = "Deduetto style " .. i
subs.insert(last_style, new_style)
last_style = last_style + 1
aegisub.log("Created new style: " .. new_style.name .. "\n")
end
end
end
aegisub.register_macro(script_name, script_description, duetto)
aegisub.register_macro(tr"Deduetto Meika", tr"Create styles from inline color tags", deduetto_meika)

View file

@ -0,0 +1,398 @@
[[
==README==
Frame-by-Frame Transform Automation Script
Smoothly transforms various parameters across multi-line, frame-by-frame typesets.
Useful for adding smooth transitions to frame-by-frame typesets that cannot be tracked with mocha,
or as a substitute for the stepped \t transforms generated by the Aegisub-Motion.lua script, which
may cause more lag than hard-coded values.
First generate the frame-by-frame typeset that you will be adding the effect to. Find the lines where
you want the effect to begin and the effect to end, and visually typeset them until they look the way
you want them to.
These lines will act as "keyframes", and the automation will modify all the lines in between so that
the appearance of the first line smoothly transitions into the appearance of the last line. Simply
highlight the first line, the last line, and all the lines in between, and run the automation.
It will only affect the tags that are checked in the popup menu when you run the automation. If you wish
to save specific sets of parameters that you would like to run together, you can use the presets manager.
For example, you can go to the presets manager, check all the color tags, and save a preset named "Colors".
The next time you want to transform all the colors, just select "Colors" from the preset dropdown menu.
The "All" preset is included by default and cannot be deleted. If you want a specific preset to be loaded
when you start the script, name it "Default" when you define the preset.
This may be obvious, but this automation only works on one layer or one component of a frame-by-frame
typeset at a time. If you have a frame-by-frame typeset that has two lines per frame, which looks like:
A1
B1
A2
B2
A3
B3
etc.
Then this automation will not work. The lines must be organized as:
A1
A2
A3
etc.
B1
B2
B3
etc.
And you would have to run the automation twice, once on A and once on B. Furthermore, the text of each
line must be exactly the same once all tags are removed. You can have as many tag blocks as you want
in whatever positions you want for the "keyframe" lines (the first and the last). But once the tags are
taken out, the text of the lines must be identical, down to the last space. If you are using ctrl-D or
copy-pasting, this should be a given, but it's worth a warning.
The lines in between can have any tags you want in them. So long as the automation is not transforming
those particular tags, they will be left untouched. If you need the typeset to suddenly turn huge for one
frame, simply uncheck "fscx" and "fscy" when you run the automation, and the size of the line won't be
touched.
If you are transforming rotations, there is something to watch out for. If you want a line to start
with \frz10 and rotate to \frz350, then with default options, the line will rotate 340 degrees around the
circle until it gets to 350. You probably wanted it to rotate only 20 degrees, passing through 0. The
solution is to check the "Rotate in shortest direction" checkbox from the popup window. This will cause
the line to always pick the rotation direction that has a total rotation of less than 180 degrees.
New feature: ignore text. Requires you to only have one tag block in each line, at the beginning.
Comes with an extra automation "Remove tags" that utilizes functions that were written for the main
automation. You can comment out (two dashes) the line at the bottom that adds this automation if you don't
want it.
TODO:
Check that all lines text match
iclip support
]]
export script_name = "Frame-by-frame transform"
export script_description = "Smoothly transforms between the first and last selected lines."
export script_version = "2.0.1"
export script_namespace = "lyger.FbfTransform"
DependencyControl = require "l0.DependencyControl"
rec = DependencyControl{
feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
{
{"lyger.LibLyger", version: "2.0.0", url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
{"l0.Functional", version: "0.3.0", url: "https://github.com/TypesettingTools/ASSFoundation",
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"},
}
}
LibLyger, Functional = rec\requireModules!
import list, math, string, table, unicode, util, re from Functional
logger, libLyger = rec\getLogger!, LibLyger!
-- tag list, grouped by dialog layout
tags_grouped = {
{"c", "2c", "3c", "4c"},
{"alpha", "1a", "2a", "3a", "4a"},
{"fscx", "fscy", "fax", "fay"},
{"frx", "fry", "frz"},
{"bord", "shad", "fs", "fsp"},
{"xbord", "ybord", "xshad", "yshad"},
{"blur", "be"},
{"pos", "org", "clip"}
}
tags_flat = list.join unpack tags_grouped
-- default settings for every preset
preset_defaults = { skiptext: false, flip_rot: false, accel: 1.0,
tags: {tag, false for tag in *tags_flat }
}
-- the default preset must always be available and cannot be deleted
config = rec\getConfigHandler {
presets: {
Default: {}
"[Last Settings]": {description: "Repeats the last #{script_name} operation"}
}
startupPreset: "Default"
}
unless config\load!
-- write example preset on first time load
config.c.presets["All"] = tags: {tag, true for tag in *tags_flat}
config\write!
create_dialog = (preset) ->
config\load!
preset_names = [preset for preset, _ in pairs config.c.presets]
table.sort preset_names
dlg = {
-- Flip rotation
{ name: "flip_rot", class: "checkbox", x: 0, y: 9, width: 3, height: 1,
label: "Rotate in shortest direction", value: preset.c.flip_rot },
{ name: "skiptext", class: "checkbox", x: 3, y: 9, width: 2, height: 1,
label: "Ignore text", value: preset.c.skiptext },
-- Acceleration
{ class: "label", x: 0, y: 10, width: 2, height: 1,
label: "Acceleration: ", },
{ name: "accel", class:"floatedit", x: 2, y: 10, width: 3, height: 1,
value: preset.c.accel, hint: "1 means no acceleration, >1 starts slow and ends fast, <1 starts fast and ends slow" },
{ class: "label", x: 0, y: 11, width: 2, height: 1,
label: "Preset: " },
{ name: "preset_select", class: "dropdown", x: 2, y: 11, width: 2, height: 1,
items: preset_names, value: preset.section[#preset.section] },
{ name: "preset_modify", class: "dropdown", x: 4, y: 11, width: 2, height: 1,
items: {"Load", "Save", "Delete", "Rename"}, value: "Load" }
}
-- generate tag checkboxes
for y, group in ipairs tags_grouped
dlg[#dlg+1] = { name: tag, class: "checkbox", x: x-1, y: y, width: 1, height: 1,
label: "\\#{tag}", value: preset.c.tags[tag] } for x, tag in ipairs group
btn, res = aegisub.dialog.display dlg, {"OK", "Cancel", "Mod Preset", "Create Preset"}
return btn, res, preset
save_preset = (preset, res) ->
preset\import res, nil, true
if res.__class != DependencyControl.ConfigHandler
preset.c.tags[k] = res[k] for k in *tags_flat
preset\write!
create_preset = (settings, name) ->
msg = if not name
"Onii-chan, what name would you like your preset to listen to?"
elseif name == ""
"Onii-chan, did you forget to name the preset?"
elseif config.c.presets[name]
"Onii-chan, it's not good to name a preset the same thing as another one~"
if msg
btn, res = aegisub.dialog.display {
{ class: "label", x: 0, y: 0, width: 2, height: 1, label: msg }
{ class: "label", x: 0, y: 1, width: 1, height: 1, label: "Preset Name: " },
{ class: "edit", x: 1, y: 1, width: 1, height: 1, name: "name", text: name }
}
return btn and create_preset settings, res.name
preset = config\getSectionHandler {"presets", name}, preset_defaults
save_preset preset, settings
return name
prepare_line = (i, preset) ->
line = libLyger.lines[i]
-- Figure out the correct position and origin values
posx, posy = libLyger\get_pos line
orgx, orgy = libLyger\get_org line
-- Look for clips
clip = {line.text\match "\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)"}
-- Make sure each line starts with tags
line.text = "{}#{line.text}" unless line.text\find "^{"
-- Turn all \1c tags into \c tags, just for convenience
line.text = line.text\gsub "\\1c", "\\c"
--Separate line into a table of tags and text
line_table = if preset.c.skiptext
while not line.text\match "^{[^}]+}[^{]"
line.text = line.text\gsub "}{", "", 1
tag, text = line.text\match "^({[^}]+})(.+)$"
{{:tag, :text}}
else [{:tag, :text} for tag, text in line.text\gmatch "({[^}]*})([^{]*)"]
return line, line_table, posx, posy, orgx, orgy, #clip > 0 and clip
--The main body of code that runs the frame transform
frame_transform = (sub, sel, res) ->
-- save last settings
preset = config\getSectionHandler {"presets", "[Last Settings]"}, preset_defaults
save_preset preset, res
libLyger\set_sub sub
-- Set the first and last lines in the selection
first_line, start_table, sposx, sposy, sorgx, sorgy, sclip = prepare_line sel[1], preset
last_line, end_table, eposx, eposy, eorgx, eorgy, eclip = prepare_line sel[#sel], preset
-- If either the first or last line do not contain a rectangular clip,
-- you will not be clipping today
preset.c.tags.clip = false unless sclip and eclip
-- These are the tags to transform
transform_tags = [tag for tag in *tags_flat when preset.c.tags[tag]]
-- Make sure both lines have the same splits
LibLyger.match_splits start_table, end_table
-- Tables that store tables for each tag block, consisting of the state of all relevant tags
-- that are in the transform_tags table
start_state_table = LibLyger.make_state_table start_table, transform_tags
end_state_table = LibLyger.make_state_table end_table, transform_tags
-- Insert default values when not included for the state of each tag block,
-- or inherit values from previous tag block
start_style = libLyger\style_lookup first_line
end_style = libLyger\style_lookup last_line
current_end_state, current_start_state = {}, {}
for k, sval in ipairs start_state_table
-- build current state tables
for skey, sparam in pairs sval
current_start_state[skey] = sparam
for ekey, eparam in pairs end_state_table[k]
current_end_state[ekey] = eparam
-- check if end is missing any tags that start has
for skey, sparam in pairs sval
end_state_table[k][skey] or= current_end_state[skey] or end_style[skey]
-- check if start is missing any tags that end has
for ekey, eparam in pairs end_state_table[ k]
start_state_table[k][ekey] or= current_start_state[ekey] or start_style[ekey]
-- Insert proper state into each intervening line
for i = 2, #sel-1
aegisub.progress.set 100 * (i-1) / (#sel-1)
this_line = libLyger.lines[sel[i]]
-- Turn all \1c tags into \c tags, just for convenience
this_line.text = this_line.text\gsub "\\1c","\\c"
-- Remove all the relevant tags so they can be replaced with their proper interpolated values
this_line.text = LibLyger.time_exclude this_line.text, transform_tags
this_line.text = LibLyger.line_exclude this_line.text, transform_tags
this_line.text = this_line.text\gsub "{}",""
-- Make sure this line starts with tags
this_line.text = "{}#{this_line.text}" unless this_line.text\find "^{"
-- The interpolation factor for this particular line
factor = (i-1)^preset.c.accel / (#sel-1)^preset.c.accel
-- Handle pos transform
if preset.c.tags.pos then
x = LibLyger.float2str util.interpolate factor, sposx, eposx
y = LibLyger.float2str util.interpolate factor, sposy, eposy
this_line.text = this_line.text\gsub "^{", "{\\pos(#{x},#{y})"
-- Handle org transform
if preset.c.tags.org then
x = LibLyger.float2str util.interpolate factor, sorgx, eorgx
y = LibLyger.float2str util.interpolate factor, sorgy, eorgy
this_line.text = this_line.text\gsub "^{", "{\\org(#{x},#{y})"
-- Handle clip transform
if preset.c.tags.clip then
clip = [util.interpolate factor, ord, eclip[i] for i, ord in ipairs sclip]
logger\dump{clip, sclip, eclip}
this_line.text = this_line.text\gsub "^{", "{\\clip(%d,%d,%d,%d)"\format unpack clip
-- Break the line into a table
local this_table
if preset.c.skiptext
while not this_line.text\match "^{[^}]+}[^{]"
this_line.text = this_line.text\gsub "}{", "", 1
tag, text = this_line.text\match "^({[^}]+})(.+)$"
this_table = {{:tag, :text}}
else
this_table = [{:tag, :text} for tag, text in this_line.text\gmatch "({[^}]*})([^{]*)"]
-- Make sure it has the same splits
j = 1
while j <= #start_table
stext = start_table[j].text
ttext, ttag = this_table[j].text, this_table[j].tag
-- ttext might contain miscellaneous tags that are not being checked for,
-- so remove them temporarily
ttext_temp = ttext\gsub "{[^{}]*}", ""
-- If this table item has longer text, break it in two based on
-- the text of the start table
if #ttext_temp > #stext
newtext = ttext_temp\match "#{LibLyger.esc stext}(.*)"
for i = #this_table, j+1,-1
this_table[i+1] = this_table[i]
this_table[j] = tag: ttag, text: ttext\gsub "#{LibLyger.esc newtext}$",""
this_table[j+1] = tag: "{}", text: newtext
-- If the start table has longer text, then perhaps ttext was split
-- at a tag that's not being transformed
if #ttext < #stext
-- It should be impossible for this to happen at the end, but check anyway
assert this_table[j+1], "You fucked up big time somewhere. Sorry."
this_table[j].text = table.concat {ttext, this_table[j+1].tag, this_table[j+1].text}
if this_table[j+2]
this_table[i] = this_table[i+1] for i = j+1, #this_table-1
this_table[#this_table] = nil
j -= 1
j += 1
--Interpolate all the relevant parameters and insert
this_line.text = LibLyger.interpolate this_table, start_state_table, end_state_table,
factor, preset
sub[sel[i]] = this_line
validate_fbf = (sub, sel) -> #sel >= 3
load_tags_remove = (sub, sel) ->
pressed, res = aegisub.dialog.display {
{ class: "label", label: "Enter the tags you would like to remove: ",
x: 0, y: 0, width: 1,height: 1 },
{ class: "textbox", name: "tag_list", text: "",
x: 0, y: 1,width: 1, height: 1 },
{ class: "checkbox", label: "Remove all EXCEPT", name: "do_except", value: false,
x: 0,y: 2, width: 1, height: 1 }
}, {"Remove","Cancel"}, {ok: "Remove", cancel: "Cancel"}
return if pressed == "Cancel"
tag_list = [tag for tag in res.tag_list\gmatch "\\?(%w+)[%s\\n,;]*"]
--Remove or remove except the tags in the table
for li in *sel
line = sub[li]
f = res.do_except and LibLyger.line_exclude_except or LibLyger.line_exclude
line.text = f(line.text, tag_list)\gsub "{}", ""
sub[li] = line
fbf_gui = (sub, sel, _, preset_name = config.c.startupPreset) ->
preset = config\getSectionHandler {"presets", preset_name}, preset_defaults
btn, res = create_dialog preset
switch btn
when "OK" do frame_transform sub, sel, res
when "Create Preset" do fbf_gui sub, sel, nil, create_preset res
when "Mod Preset"
if preset_name != res.preset_select
preset = config\getSectionHandler {"presets", res.preset_select}, preset_defaults
preset_name = res.preset_select
switch res.preset_modify
when "Delete"
preset\delete!
preset_name = nil
when "Save" do save_preset preset, res
when "Rename"
preset_name = create_preset preset.userConfig, preset_name
preset\delete!
fbf_gui sub, sel, nil, preset_name
-- register macros
rec\registerMacros {
{script_name, nil, fbf_gui, validate_fbf},
{"Remove tags", "Remove or remove all except the input tags.", load_tags_remove}
}
for name, preset in pairs config.c.presets
f = (sub, sel) -> frame_transform sub, sel, config\getSectionHandler {"presets", name}
rec\registerMacro "Presets/#{name}", preset.description, f, validate_fbf, nil, true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,175 @@
--[[
README
Karaoke Helper
Does simple karaoke tasks. Adds blank padding syllables to the beginning of lines,
and also adjusts final syllable so it matches the line length.
Will add more features as ktimers suggest them to me.
]]--
script_name = "Karaoke helper"
script_description = "Miscellaneous tools for assisting in karaoke timing."
script_version = "0.2.0"
script_author = "lyger"
script_namespace = "lyger.KaraHelper"
local DependencyControl = require("l0.DependencyControl")
local rec = DependencyControl{
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
{
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
}
}
local LibLyger = rec:requireModules()
local libLyger = LibLyger()
function make_config(styles)
local stopts={"selected lines"}
for i=1,styles.n,1 do
stopts[i+1] = ("style: %q").format(styles[i].name)
end
local config=
{
--What to apply the automation on
{
class="label",
label="Apply to:",
x=0,y=0,width=1,height=1
},
{
class="dropdown",
name="sselect",items=stopts,
x=1,y=0,width=1,height=1,
value="selected lines"
},
--Match syls to line length
{
class="checkbox",
name="match",label="Match syllable lengths to line length",
x=0,y=1,width=2,height=1,
value=true
},
--Add blank syl at the start
{
class="checkbox",
name="leadin",label="Add start padding:",
x=0,y=2,width=1,height=1,
value=false
},
{
class="intedit",
name="leadindur",
x=1,y=2,width=1,height=1,
min=0,
value=0
},
--Add blank syl at the end
{
class="checkbox",
name="leadout",label="Add end padding:",
x=0,y=3,width=1,height=1,
value=false
},
{
class="intedit",
name="leadoutdur",
x=1,y=3,width=1,height=1,
min=0,
value=0
}
}
return config
end
--Match syllable and line durations
function match_durs(line)
local ldur=line.end_time-line.start_time
local cum_sdur=0
for sdur in line.text:gmatch("\\[Kk][fo]?(%d+)") do
cum_sdur=cum_sdur+tonumber(sdur)
end
local delta=math.floor(ldur/10)-cum_sdur
line.text=line.text:gsub("({[^{}]*\\[Kk][fo]?)(%d+)([^{}]*}[^{}]*)$",
function(pre,val,post)
return ("%s%d%s"):format(pre, tonumber(val)+delta, post)
end)
return line
end
--Add padding at the start
function add_prepad(line,pdur)
line.text=line.text:gsub("^({[^{}]*\\[Kk][fo]?)(%d+)",
function(pre,val)
return ("{\\k%d}%s%d"):format(pdur, pre, tonumber(val)-pdur)
end)
line.text=line.text:gsub("^{\\k(%d+)}({[^{}]*\\[Kk][fo]?)(%-?%d+)([^{}]*}{)",
function(val1,mid,val2,post)
return ("%s%d%s"):format(mid, tonumber(val1)+tonumber(val2), post)
end)
return line
end
--Add padding at the end
function add_postpad(line,pdur)
line.text=line.text:gsub("(\\[Kk][fo]?)(%d+)([^{}]*}[^{}]*)$",
function(pre,val,post)
return ("%s%d%s{\\k%d}"):format(pre, tonumber(val)-pdur, post, pdur)
end)
line.text=line.text:gsub("(\\[Kk][fo]?)(%-?%d+)([^{}]*}){\\k(%d+)}$",
function(pre,val1,mid,val2)
return ("%s%d%s"):format(pre, tonumber(val1)+tonumber(val2), mid)
end)
return line
end
--Load config and display
function load_kh(sub,sel)
libLyger:set_sub(sub, sel)
-- Basic header collection, config, dialog display
local config = make_config(libLyger.styles)
local pressed,results=aegisub.dialog.display(config)
if pressed=="Cancel" then aegisub.cancel() end
--Determine how to retrieve the next line, based on the dropdown selection
local tstyle, line_cnt, get_next = results["sselect"], #sub
if tstyle:match("^style: ") then
tstyle=tstyle:match("^style: \"(.+)\"$")
get_next = function(uindex)
for i = uindex, line_cnt do
local line = libLyger.dialogue[uindex]
if line.style == tstyle and (not line.comment or line.effect == "karaoke") then
return line, i
end
end
end
else
get_next = function(uindex)
if uindex <= #sel then
return libLyger.lines[sel[uindex]], uindex+1
end
end
end
--Control loop
local line, uindex = get_next(1)
while line do
if results["match"] then match_durs(line) end
if results["leadin"] then add_prepad(line, results["leadindur"]) end
if results["leadout"] then add_postpad(line, results["leadoutdur"]) end
sub[line.i] = line
line, uindex = get_next(uindex)
end
aegisub.set_undo_point(script_name)
end
rec:registerMacro(load_kh)

View file

@ -0,0 +1,224 @@
script_name = "Karaoke replacer"
script_description = "Replaces the syllables of a verse."
script_version = "0.3.0"
script_author = "lyger"
script_namespace = "lyger.KaraReplacer"
local DependencyControl = require("l0.DependencyControl")
local rec = DependencyControl{
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json"
}
--Fuck it, I should comment this code. Her goes
function kara_replace(sub,sel)
for si,li in ipairs(sel) do
--Read in line
local line = sub[li]
--Split at karaoke tags and create table of tags and text
local line_table = {}
for tag,text in line.text:gmatch("({[^{}]*\\[kK][^{}]*})([^{}]*)") do
table.insert(line_table,{["tag"]=tag,["text"]=text})
end
--Put the line back together with spaces between syllables
local rebuilt_parts = {}
for i,val in ipairs(line_table) do
rebuilt_parts[i] = val.text
end
--Add some padding so it displays better
local rebuilt_original = table.concat(rebuilt_parts, " ")
rebuilt_original = rebuilt_original .. string.rep(" ", math.floor(rebuilt_original:len()/2) -1)
--Dialog display
local config = {
{
class="label",
label=rebuilt_original,
x=0,y=0,width=1,height=1
}
,
{
class="edit",
name="replace",
x=0,y=1,width=1,height=1
}
}
--Instructions
local help_config = {
{
class="label",
label=
"The syllables of the original line will be displayed.\n"..
"Type the syllables you would like to replace them with,\n"..
"with spaces between each syllable.\n\n"..
"If you want a space in the lyrics, type a double space.\n"..
"Two join a syllable with the one after it, put a + after\n"..
"the syllable.\n"..
"To split a syllable (you'll have to adjust it yourself),\n"..
"put a | where you want the split.\n"..
"To insert a blank syllable (for padding), type _\n"..
"You can ignore any blank syllables in the original line.\n\n"..
"Example:\n"..
"_ ko re wa+ re|i de su",
x=0,y=0,width=1,height=1
}
}
--Show dialog and get input for each line
local pressed
repeat
pressed,result=aegisub.dialog.display(config,{"Next line","Help"})
if pressed=="Help" then
aegisub.dialog.display(help_config,{"OK"})
end
until pressed~="Help"
--Split input at spaces and store in table
local replace = {}
for newsyl in result["replace"]:gsub(" ","\t "):gmatch("[^ ]+") do
newsyl=newsyl:gsub("\t"," ")
table.insert(replace,newsyl)
end
local rebuilt_text, r = {}, 1
--Indices of original and replacement tables
local oi, ri = 1, 1
while oi<=#line_table do
--Skip if it's a blank syl (used for padding) or we're out of replacements
if line_table[oi].text:len()>0 and replace[ri]~=nil then
--Handle splitting syls
if replace[ri]:find("|")~=nil then
--Split the replacement line at | characters
subtab={}
for subsyl in replace[ri]:gmatch("[^|]+") do
table.insert(subtab,subsyl)
end
--Find the original time of the karaoke syllable
local otime = tonumber(line_table[oi].tag:match("\\[kK][fo]?(%d+)"))
--The remaining time (for last syl, to ensure they add up to the original time)
local ltime = otime
--Add all but the last syl
for x=1,#subtab-1,1 do
--To minimize truncation error, alternate between ceil and floor
local ttime = 0
if x%2==1 then
ttime = math.floor(otime/#subtab)
else
ttime = math.ceil(otime/#subtab)
end
rebuilt_text[r] = line_table[oi].tag:gsub("(\\[kK][fo]?)%d+","\1"..tostring(ttime))
rebuilt_text[r+1], r = subtab[x], r+2
ltime=ltime-ttime
end
--Add the last syl
rebuilt_text[r] = line_table[oi].tag:gsub("(\\[kK][fo]?)%d+","\1"..tostring(ltime))
rebuilt_text[r+1], r = subtab[#subtab], r+2
--Handle merging syls
--Only merge if it's not the last syl
elseif replace[ri]:find("+")~=nil and oi<#line_table then
local temp_tag = line_table[oi].tag
oi=oi+1
stime=tonumber(line_table[oi].tag:match("\\[kK][fo]?(%d+)"))
temp_tag=temp_tag:gsub("(\\[kK][fo]?)(%d+)",function(a,b)
return a..tostring(tonumber(b)+stime)
end)
rebuilt_text[r], rebuilt_text[r+1] = temp_tag, replace[ri]:gsub("+","")
r = r+2
--The usual replacement
else
rebuilt_text[r], rebuilt_text[r+1] = line_table[oi].tag, replace[ri]:gsub("_","")
r = r+2
end
--Increment indices
oi=oi+1
ri=ri+1
else
rebuilt_text[r], r = line_table[oi].tag, r+1
oi=oi+1
end
end
line.text = table.concat(rebuilt_text)
sub[li]=line
if finished then break end
end
end
--Old behavior. If automations are ever modified so that hitting "enter" from a text box
--will execute the "OK" button, then this behavior is probably better.
--For now, this function doesn't do anything
function kara_replace_old(sub,sel)
for si,li in ipairs(sel) do
line=sub[li]
line_table={}
for tag,text in line.text:gmatch("({[^{}]*\\[kK][^{}]*})([^{}]*)") do
table.insert(line_table,{["tag"]=tag,["text"]=text})
end
rebuilt_text=""
finished=false
for i,val in ipairs(line_table) do
local function hl_syl(lt,idx)
result=""
for k,a in ipairs(lt) do
if k==idx then result=result.." ["..a.text:upper().."] "
else result=result..a.text end
end
return result
end
if val.text:len()<1 or finished then
rebuilt_text=rebuilt_text..val.tag..val.text
else
config=
{
{
class="label",
label="Enter the syllable to replace with, or nothing to close.",
x=0,y=0,width=1,height=1
},
{
class="label",
label=hl_syl(line_table,i),
x=0,y=2,width=1,height=1
},
{
class="edit",
name="replace",
x=0,y=3,width=1,height=1
}
}
_,res=aegisub.dialog.display(config,{"OK"})
if res["replace"]:len()<1 then
rebuilt_text=rebuilt_text..val.tag..val.text
finished=true
else
rebuilt_text=rebuilt_text..val.tag..res["replace"]
end
end
end
line.text=rebuilt_text
sub[li]=line
if finished then break end
end
end
rec:registerMacro(kara_replace)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,185 @@
-- Times a sign with {TS 3:24} to 3:24-3:25. Can convert and use a few other formats, like {3:24}, {TS,3:24}, {3,24}, etc.
-- supported timecodes: {TS 1:23}, {TS 1:23 words}, {TS words 1:23}, {TS,1:23}, {1:23}, {1;23}, {1,23}, {1.23}, [1:23], and variations
-- Manual: http://unanimated.xtreemhost.com/ts/scripts-manuals.htm#timesigns
script_name="Time Signs"
script_description="Rough-times signs from TS timecodes"
script_author="unanimated"
script_version="2.8"
script_namespace="ua.TimeSigns"
local haveDepCtrl,DependencyControl,depRec=pcall(require,"l0.DependencyControl")
if haveDepCtrl then
script_version="2.8.0"
depRec=DependencyControl{feed="https://raw.githubusercontent.com/TypesettingTools/unanimated-Aegisub-Scripts/master/DependencyControl.json"}
end
function signtime(subs,sel)
for z=#sel,1,-1 do
i=sel[z]
line=subs[i]
text=line.text
-- format timecodes
text=text
:gsub("({.-})({TS.-})","%2%1")
:gsub("(%d+):(%d%d):(%d%d)",
function(a,b,c) return a*60+b..":"..c end) -- hours
:gsub("^(%d%d%d%d)%s%s*","{TS %1}") -- ^1234 text
:gsub("(%d%d)ish","%1") -- 1:23ish
:gsub("^([%d%s:,%-]+)","{%1}") -- ^1:23, 2:34, 4:56
:gsub("^(%d+:%d%d)%s%-%s","{TS %1}") -- ^12?:34 -
:gsub("^(%d+:%d%d%s*)","{TS %1}") -- ^12?:34
:gsub("^{(%d+:%d%d)","{TS %1") -- ^{12?:34
:gsub("^%[(%d+:%d%d)%]:?%s*","{TS %1}") -- ^[12?:34]:?
:gsub("{TS[%s%p]+(%d)","{TS %1") -- {TS ~12:34
:gsub("({[^}]-)(%d+)[%;%.,]?(%d%d)([^:%d][^}]-})","%1%2:%3%4") -- {1;23 / 1.23 / 1,23 123}
:gsub("{TS%s([^%d\\}]+)%s(%d+:%d%d)","{TS %2 %1") -- {TS comment 12:34}
:gsub(":%s?}","}") -- {TS 12:34: }
:gsub("|","\\N")
tc=text:match("^{[^}]-}") or ""
tc=tc:gsub("(%d+)(%d%d)([^:])","%1:%2%3")
text=text:gsub("^{[^}]-}%s*",tc)
if res.blur then text=text:gsub("\\blur[%d%.]+",""):gsub("{}",""):gsub("^","{\\blur"..res.bl.."}") end
line.text=text
tstags=text:match("{TS[^}]-}") or ""
times={} -- collect times if there are more
for tag in tstags:gmatch("%d+:%d+") do table.insert(times,tag) end
for t=#times,1,-1 do
tim=times[t]
-- convert to start time
tstid1,tstid2=tim:match("(%d+):(%d%d)")
if tstid1 then tid=(tstid1*60000+tstid2*1000-500) end
-- shifting times
if tid then
if res.shift then tid=tid+res.secs*1000 end
-- set start and end time [500ms before and after the timecode]
line.start_time=tid line.end_time=(tid+1000)
end
-- snapping to keyframes
if res.snap then
start=line.start_time
endt=line.end_time
startf=ms2fr(start)
endf=ms2fr(endt)
diff=250
diffe=250
startkf=keyframes[1]
endkf=keyframes[#keyframes]
-- check for nearby keyframes
for k,kf in ipairs(keyframes) do
-- startframe snap up to 24 frames back [scroll down to change default] and 5 frames forward
if kf>=startf-res.kfs and kf<startf+5 then
tdiff=math.abs(startf-kf)
if tdiff<=diff then diff=tdiff startkf=kf end
start=fr2ms(startkf)
line.start_time=start
end
-- endframe snap up to 24 frames forward [scroll down to change default] and 10 frames back
if kf>=endf-10 and kf<=endf+res.kfe then
tdiff=math.abs(endf-kf)
if tdiff<diffe then diffe=tdiff endkf=kf end
endt=fr2ms(endkf)
line.end_time=endt
end
end
end
line.text=line.text:gsub("{TS[^}]-}","{TS "..tim.."}")
if res.nots then line.text=line.text:gsub("{TS[^}]-}",""):gsub("{(\\[^}]-)}{(\\[^}]-)}","{%1%2}"):gsub("^({[^}]-})%s*","%1") end
subs.insert(i+1,line)
if t>1 then table.insert(sel,sel[#sel]+1) end
end
if #times>0 then subs.delete(i) end
end
if res.copy then taim=0 tame=0
for z,i in ipairs(sel) do
l=subs[i]
if l.start_time==0 then l.start_time=taim l.end_time=tame end
taim=l.start_time
tame=l.end_time
subs[i]=l
end
end
return sel
end
-- Config Stuff --
function saveconfig()
savecfg="Time Signs Settings\n\n"
for key,val in ipairs(GUI) do
if val.class=="floatedit" or val.class=="intedit" or val.class=="checkbox" and val.name~="save" then
savecfg=savecfg..val.name..":"..tf(res[val.name]).."\n"
end
end
file=io.open(cfgpath,"w")
file:write(savecfg)
file:close()
ADD({{class="label",label="Config saved to:\n"..cfgpath}},{"OK"},{close='OK'})
end
function loadconfig()
cfgpath=aegisub.decode_path("?user").."\\timesigns.conf"
file=io.open(cfgpath)
if file~=nil then
konf=file:read("*all")
file:close()
for key,val in ipairs(GUI) do
if val.class=="floatedit" or val.class=="checkbox" or val.class=="intedit" then
if konf:match(val.name) then val.value=detf(konf:match(val.name..":(.-)\n")) end
end
end
end
end
function tf(val)
if val==true then ret="true"
elseif val==false then ret="false"
else ret=val end
return ret
end
function detf(txt)
if txt=="true" then ret=true
elseif txt=="false" then ret=false
else ret=txt end
return ret
end
function timesigns(subs,sel)
ADD=aegisub.dialog.display
GUI={
{x=0,y=0,width=4,class="label",label="Check this if all your timecodes are too late or early:"},
{x=0,y=1,class="checkbox",name="shift",label="Shift timecodes by "},
{x=1,y=1,width=2,class="floatedit",name="secs",value=-10,hint="Negative=backward / positive=forward",step=0.5},
{x=3,y=1,class="label",label=" sec."},
{x=0,y=2,width=4,class="checkbox",name="copy",label="For lines without timecodes, copy from the previous line"},
{x=0,y=3,width=4,class="checkbox",name="nots",label="Automatically remove {TS ...} comments"},
{x=0,y=4,width=2,class="checkbox",name="blur",label="Automatically add blur:"},
{x=2,y=4,width=2,class="floatedit",name="bl",value="0.6"},
{x=0,y=5,width=2,class="checkbox",name="snap",label="Snapping to keyframes:",value=true},
{x=0,y=6,width=2,class="label",label="Frames to search back:"},
{x=0,y=7,width=2,class="label",label="Frames to search forward:"},
{x=2,y=6,width=2,class="intedit",name="kfs",value="24",step=1,min=1,max=250},
{x=2,y=7,width=2,class="intedit",name="kfe",value="24",step=1,min=1,max=250},
{x=0,y=8,width=2,class="checkbox",name="save",label="Save current settings"},
{x=2,y=8,width=2,class="label",label=" [Time Signs v"..script_version.."]"},
}
loadconfig()
buttons={"No more suffering with SHAFT signs!","Exit"}
P,res=ADD(GUI,buttons,{ok='No more suffering with SHAFT signs!',cancel='Exit'})
if P=="Exit" then aegisub.cancel() end
ms2fr=aegisub.frame_from_ms
fr2ms=aegisub.ms_from_frame
keyframes=aegisub.keyframes()
if res.save then saveconfig() end
if P=="No more suffering with SHAFT signs!" then sel=signtime(subs,sel) end
aegisub.set_undo_point(script_name)
return sel
end
if haveDepCtrl then depRec:registerMacro(timesigns) else aegisub.register_macro(script_name,script_description,timesigns) end

View file

@ -0,0 +1,142 @@
local json, log
version = '1.1.4'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'ConfigHandler',
:version,
description: 'A class for mapping dialogs to persistent configuration.',
author: 'torque',
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.ConfigHandler'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'json' }
{ 'a-mo.Log', version: '1.0.0' }
}
}
json, log = version\requireModules!
else
json = require 'json'
log = require 'a-mo.Log'
class ConfigHandler
@version: version
-- The minimum required format for `optionTables` is
-- { section: { optionname: { value: optionvalue, config: (true|false) } } }
-- the config key exists because this is designed to be embedded in dialog
-- tables. Some dialog elements may not be intended to be saved to a
-- config file, or are labels that do not return a value.
-- Constructor
new: ( @optionTables, fileName, hasSections, @version = "0.0.1", filePath = "?user" ) =>
@fileName = aegisub.decode_path "#{filePath}/#{fileName}"
@fileHandle = nil
loadDefault @
-- Private methods (I probably shouldn't have bothered to do this!)
loadDefault = =>
@configuration = { }
for sectionName, configEntries in pairs @optionTables
@configuration[sectionName] = { }
for optionName, configEntry in pairs configEntries
if configEntry.name != optionName and configEntry.class != "label"
configEntry.name = optionName
if configEntry.config
@configuration[sectionName][optionName] = configEntry.value
parse = =>
rawConfigText = @fileHandle\read '*a'
-- Avoid clobbering the things loaded by loadDefault. I need to
-- decide how I want to handle version changes between a script's
-- built-in defaults and the serialized configuration on disk. This
-- is currently biased towards a script's built-in defaults.
parsedConfig = json.decode rawConfigText
if parsedConfig
for sectionName, configEntries in pairs parsedConfig
if configSection = @configuration[sectionName]
for optionName, optionValue in pairs configEntries
if configSection[optionName] != nil
configSection[optionName] = optionValue
doInterfaceUpdate = ( interfaceSection, sectionName ) =>
for tableKey, tableValue in pairs interfaceSection
if tableValue.config and @configuration[sectionName][tableKey] != nil
tableValue.value = @configuration[sectionName][tableKey]
doConfigUpdate = ( newValues, sectionName ) =>
-- have to loop across @configuration because not all of the
-- fields in the result table are going to be serialized, and it
-- contains no information about which ones should be and which
-- ones should not be.
for configKey, configValue in pairs @configuration[sectionName]
if newValues[configKey] != nil
@configuration[sectionName][configKey] = newValues[configKey]
-- Public methods
read: =>
if @fileHandle = io.open @fileName, 'r'
parse @
@fileHandle\close!
return true
else
log.debug "Configuration file \"#{@fileName}\" can't be read. Writing defaults."
@write!
return false
-- todo: find keys missing from either @conf or interface, and warn
-- (maybe error?) about mismatching config versions.
updateInterface: ( sectionNames ) =>
if sectionNames
if "table" == type sectionNames
for sectionName in *sectionNames
if @configuration[sectionName]
doInterfaceUpdate @, @optionTables[sectionName], sectionName
else
log.debug "Cannot update section %s, as it doesn't exist.", sectionName
else
if @configuration[sectionNames]
doInterfaceUpdate @, @optionTables[sectionNames], sectionNames
else
log.debug "Cannot update section %s, as it doesn't exist.", sectionNames
else
for sectionName, section in pairs @optionTables
if @configuration[sectionName] != nil
doInterfaceUpdate @, section, sectionName
-- maybe updateConfigurationFromDialog (but then we're getting into
-- obj-c identifier verbosity territory, and I'd rather not go there)
updateConfiguration: ( resultTable, sectionNames ) =>
-- do nothing if sectionNames isn't defined.
if sectionNames
if "table" == type sectionNames
for section in *sectionNames
doConfigUpdate @, resultTable[section], section
else
doConfigUpdate @, resultTable, sectionNames
else
log.debug "Section Name not provided. You are doing it wrong."
write: =>
-- Make sure @configuration is not an empty table.
unless next( @configuration ) == nil
@configuration.__version = @version
serializedConfig = json.encode @configuration
@configuration.__version = nil
if @fileHandle = io.open @fileName, 'w'
@fileHandle\write serializedConfig
@fileHandle\close!
else
log.warn "Could not write the configuration file \"#{@fileName}\"."
delete: =>
os.remove @fileName
if haveDepCtrl
return version\register ConfigHandler
else
return ConfigHandler

View file

@ -0,0 +1,132 @@
local log
version = '1.0.5'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'DataHandler',
:version,
description: 'A class for parsing After Effects motion data.',
author: 'torque',
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.DataHandler'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
}
}
log = version\requireModules!
else
log = require 'a-mo.Log'
class DataHandler
@version: version
new: ( input, @scriptResX, @scriptResY ) =>
-- (length-22)/4
if input
unless @parseRawDataString input
@parseFile input
parseRawDataString: ( rawDataString ) =>
@tableize rawDataString
if next @rawData
unless @rawData[1]\match "Adobe After Effects 6.0 Keyframe Data"
return false
width = @rawData[3]\match "Source Width[\t ]+([0-9]+)"
height = @rawData[4]\match "Source Height[\t ]+([0-9]+)"
unless width and height
return false
@xPosScale = @scriptResX/tonumber width
@yPosScale = @scriptResY/tonumber height
parse @
if #@xPosition != @length or #@yPosition != @length or #@xScale != @length or #@yScale != @length or #@zRotation != @length or 0 == @length
return false
return true
parseFile: ( fileName ) =>
if file = io.open fileName, 'r'
return @parseRawDataString file\read '*a'
return false
tableize: ( rawDataString ) =>
@rawData = { }
rawDataString\gsub "([^\r\n]+)", ( line ) ->
table.insert @rawData, line
parse = =>
-- Initialize these here so they don't get appended if
-- parseRawDataString is called twice.
@xPosition = { }
@yPosition = { }
@xScale = { }
@yScale = @xScale
@zRotation = { }
length = 0
section = 0
for _index, line in ipairs @rawData
unless line\match("^[\t ]+")
if line == "Position"
section = 1
elseif line == "Scale"
section = 2
elseif line == "Rotation"
section = 3
else
section = 0
else
line\gsub "^[\t ]+([%d%.%-]+)[\t ]+([%d%.%-e%+]+)(.*)", ( value1, value2, remainder ) ->
switch section
when 1
table.insert @xPosition, @xPosScale*tonumber value2
table.insert @yPosition, @yPosScale*tonumber remainder\match "^[\t ]+([%d%.%-e%+]+)"
length += 1
when 2
-- Sort of future proof against having different scale
-- values for different axes.
table.insert @xScale, tonumber value2
-- table.insert @yScale, tonumber value2
when 3
-- Sort of future proof having rotation around different
-- axes.
table.insert @zRotation, -tonumber value2
@length = length
-- Arguments: just your friendly neighborhood options table.
stripFields: ( options ) =>
defaults = { xPosition: @xStartPosition, yPosition: @yStartPosition, xScale: @xStartScale, zRotation: @zStartRotation }
for field, defaultValue in pairs defaults
unless options[field]
for index, value in ipairs @[field]
@[field][index] = defaultValue
checkLength: ( totalFrames ) =>
if totalFrames == @length
true
else
false
addReferenceFrame: ( frame ) =>
@startFrame = frame
@xStartPosition = @xPosition[frame]
@yStartPosition = @yPosition[frame]
@zStartRotation = @zRotation[frame]
@xStartScale = @xScale[frame]
@yStartScale = @yScale[frame]
calculateCurrentState: ( frame ) =>
@xCurrentPosition = @xPosition[frame]
@yCurrentPosition = @yPosition[frame]
@xRatio = @xScale[frame]/@xStartScale
@yRatio = @yScale[frame]/@yStartScale
@zRotationDiff = @zRotation[frame] - @zStartRotation
if haveDepCtrl
return version\register DataHandler
else
return DataHandler

View file

@ -0,0 +1,72 @@
local log, DataHandler, ShakeShapeHandler
version = '1.0.2'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'DataWrapper',
:version,
description: 'A class for wrapping motion data.',
author: 'torque',
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.DataWrapper'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
{ 'a-mo.DataHandler', version: '1.0.5' }
{ 'a-mo.ShakeShapeHandler', version: '1.0.2' }
}
}
log, DataHandler, ShakeShapeHandler = version\requireModules!
else
log = require 'a-mo.Log'
DataHandler = require 'a-mo.DataHandler'
ShakeShapeHandler = require 'a-mo.ShakeShapeHandler'
class DataWrapper
@version: version
new: =>
tryDataHandler = ( input ) =>
@dataObject = DataHandler input, @scriptResX, @scriptResY
if @dataObject.length
@type = "TSR"
return true
return false
tryShakeShape = ( input ) =>
@dataObject = ShakeShapeHandler input, @scriptResY
if @dataObject.length
@type = "SRS"
return true
return false
bestEffortParsingAttempt: ( input, scriptResX, scriptResY ) =>
if "string" != type( input )
return false
@scriptResX, @scriptResY = tonumber( scriptResX ), tonumber( scriptResY )
if input\match '^Adobe After Effects 6.0 Keyframe Data'
if tryDataHandler @, input
return true
elseif input\match '^shake_shape_data 4.0'
if tryShakeShape @, input
return true
else
if tryDataHandler @, input
return true
if tryShakeShape @, input
return true
return false
if haveDepCtrl
return version\register DataWrapper
else
return DataWrapper

View file

@ -0,0 +1,483 @@
local util, json, log, tags, Transform
version = '1.5.3'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'Line',
:version,
description: 'A class for containing and manipulating a line.',
author: 'torque',
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.Line'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'aegisub.util' }
{ 'json' }
{ 'a-mo.Log', version: '1.0.0' }
{ 'a-mo.Tags', version: '1.3.4' }
{ 'a-mo.Transform', version: '1.2.4' }
}
}
util, json, log, tags, Transform = version\requireModules!
else
util = require 'aegisub.util'
json = require 'json'
log = require 'a-mo.Log'
tags = require 'a-mo.Tags'
Transform = require 'a-mo.Transform'
frameFromMs = aegisub.frame_from_ms
msFromFrame = aegisub.ms_from_frame
class Line
@version: version
fieldsToDeepCopy: {
'extra'
}
fieldsToCopy: {
-- Line fields
'actor', 'class', 'comment', 'effect', 'end_time', 'layer', 'margin_l', 'margin_r', 'margin_t', 'section', 'start_time', 'style', 'text'
-- Our fields
'number', 'transforms', 'transformShift', 'transformsAreTokenized', 'properties', 'styleRef', 'wasLinear'
}
splitChar: "\\\6"
tPlaceholder: ( count ) -> "\\\3#{count}\\\3"
tTokenPattern: "(\\\3(%d+)\\\3)"
defaultXPosition: {
-- align 3, 6, 9
( subResX, leftMargin, rightMargin ) ->
return subResX - rightMargin
-- align 1, 4, 7
( subResX, leftMargin, rightMargin ) ->
return leftMargin
-- align 2, 5, 8
( subResX, leftMargin, rightMargin ) ->
return 0.5*subResX
}
defaultYPosition: {
-- align 1, 2, 3
( subResY, verticalMargin ) ->
return subResY - verticalMargin
-- align 4, 5, 6
( subResY, verticalMargin ) ->
return 0.5*subResY
-- align 7, 8, 9
( subResY, verticalMargin ) ->
return verticalMargin
}
new: ( line, @parentCollection, overrides ) =>
for field in *@fieldsToDeepCopy
if "table" == type line[field]
-- safe to assume that all fields to be deep copied are expected
-- to be tables, otherwise they wouldn't be being deep copied
if "table" == type( overrides ) and "table" == type overrides[field]
@[field] = util.deep_copy overrides[field]
else
@[field] = util.deep_copy line[field]
else
if overrides[field] != nil
@[field] = overrides[field]
else
@[field] = line[field]
if "table" == type overrides
for field in *@fieldsToCopy
if overrides[field] != nil
@[field] = overrides[field]
else
@[field] = line[field]
else
for field in *@fieldsToCopy
@[field] = line[field]
@duration = @end_time - @start_time
-- Gathers extra line metrics: the alignment and position.
-- Returns false if there is not already a position tag in the line.
extraMetrics: ( styleRef = @styleRef ) =>
alignPattern = tags.allTags.align.pattern
posPattern = tags.allTags.pos.pattern
moveTag = tags.allTags.move
@runCallbackOnOverrides ( tagBlock ) =>
tagBlock\gsub alignPattern, ( value ) ->
unless @align
@align = tonumber value
tagBlock\gsub posPattern, ( value ) ->
unless @xPosition or @move
x, y = value\match "([%.%d%-]+),([%.%d%-]+)"
@xPosition, @yPosition = tonumber( x ), tonumber( y )
tagBlock\gsub moveTag.pattern, ( value ) ->
unless @xPosition or @move
@move = moveTag\convert value
unless @align
@align = styleRef.align
unless @xPosition or @move
@xPosition, @yPosition = @getDefaultPosition!
return false
return true
formatTime = ( time ) ->
seconds = time/1000
minutes = seconds/60
hours = minutes/60
return ("%d:%02d:%05.2f")\format math.floor( hours ), math.floor( minutes%60 ), seconds%60
__tostring: =>
@createRaw!
return @raw
createRaw: =>
line = {
(@comment and ("Comment: %d")\format( @layer ) or ("Dialogue: %d")\format( @layer ))
formatTime @start_time
formatTime @end_time
@style
@actor
@margin_l
@margin_r
@margin_t
@effect
@text
}
@raw = table.concat line, ','
generateTagIndex: ( major, minor ) ->
return tonumber tostring( major ) .. "." .. tostring minor
splitTagIndex: ( index ) ->
major = math.floor index
minor = tostring( index )\match "%d+.(%d+)"
return major, tonumber minor
-- Tries to guarantee there will be no redundantly duplicate tags in
-- the line. Does no other processing. Unfortunately, actually doing
-- this perfectly is very complicated because, for example, \t() is
-- actually position dependent. e.g. with \t(\c&HFF0000&)\c&HFF0000&,
-- the \t will not actually do anything.
deduplicateTags: =>
-- Combine contiguous override blocks.
@text = @text\gsub "}{", @splitChar
-- note: most tags can appear multiple times in a line and only the
-- last instance in a given tag block is used. Some tags (\pos,
-- \move, \org, \an) can only appear once and only the first
-- instance in the entire line is used.
tagCollection = { }
@runCallbackOnOverrides ( tagBlock, major ) =>
for tag in *tags.oneTimeTags
tagBlock = tagBlock\gsub tag.pattern, ( value ) ->
unless tagCollection[tag.name]
tagCollection[tag.name] = @.generateTagIndex major, tagBlock\find tag.pattern
return nil
else
log.debug "#{tag.name} previously found at #{tagCollection[tag.name]}"
return ""
return tagBlock
-- Quirks: 2 clips are allowed, as long as one is vector and one is
-- rectangular. Move and pos obviously conflict, and whichever is
-- the first is the one that's used. The same happens with fad and
-- fade. And again, the same with clip and iclip. Also, rectangular
-- clips can exist inside of transforms. If a rect clip exists in a
-- transform, its type (i or not) dictates the type of all rect
-- clips in the line.
for _, v in ipairs {
{ "move", "pos" }
{ "fade", "fad" }
{ "rectClip", "rectiClip" }
{ "vectClip", "vectiClip" }
}
if tagCollection[v[1]] and tagCollection[v[2]]
if tagCollection[v[1]] < tagCollection[v[2]]
-- get rid of tagCollection[v[2]]
@runCallbackOnOverrides ( tagBlock ) =>
tagBlock = tagBlock\gsub tags.allTags[v[2]].pattern, ""
else
-- get rid of tagCollection[v[1]]
@runCallbackOnOverrides ( tagBlock ) =>
tagBlock = tagBlock\gsub tags.allTags[v[1]].pattern, ""
@runCallbackOnOverrides ( tagBlock ) =>
for tag in *tags.repeatTags
-- Calculates the number of times the pattern will be replaced.
_, num = tagBlock\gsub tag.pattern, ""
-- Replaces all instances except the last one.
tagBlock = tagBlock\gsub tag.pattern, "", num - 1
return tagBlock
-- Now the whole thing has to be rerun on the contents of all
-- transforms.
@text = @text\gsub @splitChar, "}{"
@text = @text\gsub "{}", ""
@text = @text\gsub "\\clip%(%)", "" -- useless even inside transforms
-- Find the first instance of an override tag in a line following
-- startIndex.
-- Arguments:
-- tag [table]: A well-formatted tag table, probably taken from tags.allTags.
-- text [string]: The text that will be searched for the tag.
-- Default: @text, the entire line text.
-- startIndex [number]: A number specifying the point at which the
-- search should start.
-- Default: 1, the beginning of the provided text block.
-- Returns:
-- - The value of the tag.
-- On error:
-- - nil
-- - A string containing an error message.
getTagValue: ( tag, text = @text, startIndex = 1 ) =>
unless tag
return nil, "No tag table was supplied."
value = text\match tag.pattern, startIndex
if value
return tag\convert value
else
return nil, "The specified tag could not be found"
-- Find all instances of a tag in a line. Only looks through override
-- tag blocks.
getAllTagValues: ( tag ) =>
values = { }
@runCallbackOnOverrides ( tagBlock ) =>
value = @getTagValue tag, tagBlock
if value
table.insert values, value
return tagBlock
return values
-- Sets all values of a tag in a line. The provided table of values
-- must have the same number of tables
setAllTagValues: ( tag, values ) =>
replacements = 1
@runCallbackOnOverrides ( tagBlock ) =>
tagBlock, count = tagBlock\gsub tag.pattern, ->
tag.format\format values[replacements]
replacements += 1
return tagBlock
-- combines getAllTagValues and setAllTagValues by running the
-- provided callback on all of the values collected.
modifyAllTagValues: ( tag, callback ) =>
values = @getAllTagValues tag
-- Callback modifies the values table in whatever way.
callback @, values
@setAllTagValues tag, values
-- Adds an empty override tag to the beginning of the line text if
-- there is not an override tag there already.
ensureLeadingOverrideBlockExists: =>
if '{' != @text\sub 1, 1
@text = "{}" .. @text
-- Runs the provided callback on all of the override tag blocks
-- present in the line.
runCallbackOnOverrides: ( callback, count ) =>
major = 0
@text = @text\gsub "({.-})", ( tagBlock ) ->
major += 1
return callback @, tagBlock, major,
count
-- Runs the provided callback on the first override tag block in the
-- line, provided that override tag occurs before any other text in
-- the line.
runCallbackOnFirstOverride: ( callback ) =>
@text = @text\gsub "^({.-})", ( tagBlock ) ->
return callback @, tagBlock
-- Runs the provided callback on all overrides that aren't the first
-- one.
runCallbackOnOtherOverrides: ( callback ) =>
@text = @text\sub( 1, 1 ) .. @text\sub( 2, -1 )\gsub "({.-})", ( tagBlock ) ->
return callback @, tagBlock
getPropertiesFromStyle: ( styleRef = @styleRef ) =>
@properties = { }
for tag in *tags.styleTags
switch tag.type
when "alpha"
@properties[tag] = tag\convert styleRef[tag.style]\sub( 3, 4 )
when "color"
@properties[tag] = tag\convert styleRef[tag.style]\sub( 5, 10 )
else
@properties[tag] = tag\convert styleRef[tag.style]
-- Because duplicate tags may exist within transforms, it becomes
-- useful to remove transforms from a line before doing various
-- processing.
tokenizeTransforms: =>
unless @transformsAreTokenized
@transforms = { }
count = 0
tagIndex = 0
@runCallbackOnOverrides ( tagBlock ) =>
tagIndex += 1
return tagBlock\gsub tags.allTags.transform.pattern, ( transform ) ->
count += 1
token = @.tPlaceholder count
transform = Transform\fromString transform, @duration, tagIndex, @
transform.token = token
@transforms[count] = transform
-- create a token for the transforms
return token
@transformsAreTokenized = true
loopOverTokenizedTransforms: ( callback ) =>
if @transformsAreTokenized
@runCallbackOnOverrides ( tagBlock ) =>
return tagBlock\gsub @tTokenPattern, ( placeholder, index ) ->
return callback @transforms[tonumber index], placeholder
detokenizeTransformsCopy: ( shift = 0 ) =>
if @transformsAreTokenized
return @text\gsub "({.-})", ( tagBlock ) ->
return tagBlock\gsub @tTokenPattern, ( placeholder, index ) ->
transform = @transforms[tonumber index]
transform.startTime -= shift
transform.endTime -= shift
result = transform\toString!
transform.startTime += shift
transform.endTime += shift
return result
detokenizeTransforms: ( shift = 0 ) =>
@loopOverTokenizedTransforms ( transform, placeholder ) ->
transform.startTime -= shift
transform.endTime -= shift
result = transform\toString!
transform.startTime += shift
transform.endTime += shift
return result
@transformsAreTokenized = false
-- detokenize using transform.rawString
dontTouchTransforms: =>
@loopOverTokenizedTransforms ( transform, placeholder ) ->
return "\\t" .. transform.rawString
@transformsAreTokenized = false
interpolateTransformsCopy: ( shift = 0, start = @start_time ) =>
newText = @text
@loopOverTokenizedTransforms ( transform, placeholder ) ->
transform.startTime -= shift
transform.endTime -= shift
frame = frameFromMs start
newText = transform\interpolate @, newText, placeholder, math.floor( 0.5*( msFromFrame( frame ) + msFromFrame( frame + 1 ) ) ) - start
transform.startTime += shift
transform.endTime += shift
return nil
return newText
interpolateTransforms: ( shift = 0, start = @start_time ) =>
newText = @text
@loopOverTokenizedTransforms ( transform, placeholder ) ->
transform.startTime -= shift
transform.endTime -= shift
frame = frameFromMs start
newText = transform\interpolate @, newText, placeholder, math.floor( 0.5*( msFromFrame( frame ) + msFromFrame( frame + 1 ) ) ) - start
transform.startTime += shift
transform.endTime += shift
return nil
@text = newText
@transformsAreTokenized = false
shiftKaraoke: ( shift = @karaokeShift ) =>
karaokeTag = tags.allTags.karaoke
@runCallbackOnOverrides ( tagBlock ) =>
return tagBlock\gsub karaokeTag.pattern, ( ... ) ->
time = karaokeTag\convert ...
if shift > 0
oldShift = -shift
newTime = time - shift
shift -= time
if newTime > 0
if karaokeTag.tag == "\\kf"
return karaokeTag\format( oldShift ) .. karaokeTag\format time
else
return karaokeTag\format newTime
else
return ""
else
return nil
combineWithLine: ( line ) =>
if @text == line.text and @style == line.style and (@start_time == line.end_time or @end_time == line.start_time)
@start_time = math.min @start_time, line.start_time
@end_time = math.max @end_time, line.end_time
return true
return false
delete: ( sub = @parentCollection.sub ) =>
unless sub
log.windowError "Sub doesn't exist, so I can't delete things. This isn't gonna work."
unless @hasBeenDeleted
sub.delete @number
@hasBeenDeleted = true
getDefaultPosition: ( styleRef = @styleRef ) =>
verticalMargin = if @margin_t == 0 then styleRef.margin_t else @margin_t
leftMargin = if @margin_l == 0 then styleRef.margin_l else @margin_l
rightMargin = if @margin_r == 0 then styleRef.margin_r else @margin_r
align = @align or styleRef.align
return @defaultXPosition[align%3+1]( @parentCollection.meta.PlayResX, leftMargin, rightMargin ), @defaultYPosition[math.ceil align/3]( @parentCollection.meta.PlayResY, verticalMargin )
setExtraData: ( field, data ) =>
if "table" != type @extra
@extra = {}
switch type data
when "table"
@extra[field] = json.encode data
when "string"
@extra[field] = data
else
@extra[field] = tostring data
getExtraData: ( field ) =>
if "table" != type @extra
return nil
value = @extra[field]
success, res = pcall json.decode, value
-- Should probably add something for luabins here but it is
-- extremely stupid and dumb so I really don't want to.
if success
return res
else
return value
if haveDepCtrl
return version\register Line
else
return Line

View file

@ -0,0 +1,272 @@
local log, Line
version = '1.3.0'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'LineCollection'
:version
description: 'A class for handling collections of lines.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.LineCollection'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
{ 'a-mo.Line', version: '1.5.3' }
}
}
log, Line = version\requireModules!
else
log = require 'a-mo.Log'
Line = require 'a-mo.Line'
frameFromMs = aegisub.frame_from_ms
class LineCollection
@version: version
@fromAllLines: ( sub, validationCb, selectLines ) =>
sel = { }
for i = 1, #@sub
table.insert( sel, i ) if @sub[i].class == "dialogue"
@ sub, sel, validationCb, selectLines
new: ( @sub, sel, validationCb, selectLines = true ) =>
@lines = { }
meta = getmetatable @
if 'function' != type meta.__index
metaIndex = meta.__index
meta.__index = ( index ) =>
if 'number' == type index
@lines[index]
else
metaIndex[index]
if type( sel ) == "table" and #sel > 0
@collectLines sel, validationCb, selectLines
if frameFromMs 0
@getFrameInfo!
else
for i = #@sub, 1, -1
if @sub[i].class != "dialogue" then
@firstLineNumber = i + 1
@lastLineNumber = i + 1
break
-- This method should update various properties such as
-- (start|end)(Time|Frame).
addLine: ( line, validationCb = (-> return true), selectLine = true, index = false ) =>
if validationCb line
line.parentCollection = @
line.inserted = false
line.selected = selectLine
line.number = index == true and line.number or index or nil
-- if @startTime is unset, @endTime should damn well be too.
if @startTime
if @startTime > line.start_time
@startTime = line.start_time
if @endTime < line.end_time
@endTime = line.end_time
else
@startTime = line.start_time
@endTime = line.end_time
if @hasMetaStyles
line.styleRef = @styles[line.style]
if @hasFrameInfo
line.startFrame = frameFromMs line.start_time
line.endFrame = frameFromMs line.end_time
@startFrame = frameFromMs @startTime
@endFrame = frameFromMs @endTime
@totalFrames = @endFrame - @startFrame
table.insert @lines, line
generateMetaAndStyles: =>
@styles = { }
@meta = { }
for i = 1, #@sub
line = @sub[i]
if line.class == "style"
@styles[line.name] = line
-- not going to bother porting all the special-case bullshit over
-- from karaskel.
elseif line.class == "info"
@meta[line.key] = line.value
elseif line.class == "dialogue"
break
unless next @styles
log.windowError "No styles could be found and I guarantee that's gonna break something."
@hasMetaStyles = true
collectLines: ( sel, validationCb = (( line ) -> return not line.comment), selectLines = true ) =>
unless @hasMetaStyles
@generateMetaAndStyles!
dialogueStart = 0
for x = 1, #@sub
if @sub[x].class == "dialogue"
dialogueStart = x - 1 -- start line of dialogue subs
break
@startTime = @sub[sel[1]].start_time
@endTime = @sub[sel[1]].end_time
@lastLineNumber = 0
for i = #sel, 1, -1
with line = Line @sub[sel[i]], @
if validationCb line
.number = sel[i]
@firstLineNumber = math.min .number, @firstLineNumber or .number
@lastLineNumber = math.max .number, @lastLineNumber
.inserted = true
.hasBeenDeleted = false
.selected = selectLines
.humanizedNumber = .number - dialogueStart
.styleRef = @styles[.style]
if .start_time < @startTime
@startTime = .start_time
if .end_time > @endTime
@endTime = .end_time
table.insert @lines, line
getFrameInfo: =>
for line in *@lines
line.startFrame = frameFromMs line.start_time
line.endFrame = frameFromMs line.end_time
@startFrame = frameFromMs @startTime
@endFrame = frameFromMs @endTime
@totalFrames = @endFrame - @startFrame
@hasFrameInfo = true
callMethodOnAllLines: ( methodName, ... ) =>
for line in *@lines
line[methodName] line, ...
combineIdenticalLines: =>
lastLine = @lines[1]
linesToSkip = { }
for i = 2, #@lines
log.checkCancellation!
if lastLine\combineWithLine @lines[i]
linesToSkip[#linesToSkip+1] = @lines[i]
@shouldInsertLines = true
continue
else lastLine = @lines[i]
@deleteLines linesToSkip
-- The third value passed to the callback is for progress reporting only,
-- and the fourth is the actual index.
runCallback: ( callback, reverse ) =>
lineCount = #@lines
if reverse
for index = lineCount, 1, -1
callback @, @lines[index], lineCount - index + 1, index
else
for index = 1, lineCount
callback @, @lines[index], index, index
deleteLines: ( lines = @lines, doShift = true ) =>
if lines.__class == Line
lines = { lines }
lineSet = {line,true for _,line in pairs lines when not line.hasBeenDeleted}
-- make sure all lines are unique and have not actually been already removed
lines = [k for k,v in pairs lineSet]
@sub.delete [line.number for line in *lines when line.inserted]
@lastLineNumber = @firstLineNumber
shift = #lines or 0
for line in *@lines
if lineSet[line]
line.hasBeenDeleted = true
shift -= line.inserted and 1 or 0
elseif not line.hasBeenDeleted and line.inserted
line.number -= doShift and shift or 0
@lastLineNumber = math.max(line.number, @lastLineNumber)
insertLines: =>
toInsert = [line for line in *@lines when not (line.inserted or line.hasBeenDeleted)]
tailLines, numberedLines = {}, {}
for i = 1, #toInsert
line = toInsert[i]
if line.number
numberedLines[#numberedLines + 1] = line
line.i = i
else
tailLines[#tailLines + 1] = line
line.number = @lastLineNumber + i
line.inserted = true
table.sort numberedLines, ( a, b ) ->
return (a.number < b.number) or (a.number == b.number) and (a.i < b.i)
for line in *numberedLines
@sub.insert line.number, line
line.inserted = true
@lastLineNumber = math.max @lastLineNumber, line.number
tailLineCnt, chunkSize = #tailLines, 1000
if tailLineCnt > 0
for i = 1, tailLineCnt, chunkSize
chunkSize = math.min chunkSize, tailLineCnt - i + 1
@sub.insert @lastLineNumber + i, unpack tailLines, i, i+chunkSize-1
@lastLineNumber = math.max @lastLineNumber, tailLines[tailLineCnt].number
replaceLines: =>
if @shouldInsertLines
@insertLines!
else
for line in *@lines
if line.inserted and not line.hasBeenDeleted
@sub[line.number] = line
getSelection: =>
sel = [line.number for line in *@lines when line.selected and line.inserted and not line.hasBeenDeleted]
return sel, sel[#sel]
__newindex: ( index, value ) =>
if 'number' == type index
@lines[index] = value
else
rawset @, index, value
__len: =>
#@lines
__ipairs: =>
iterator = ( tbl, i ) ->
i += 1
value = tbl[i]
if value
i, value
iterator, @lines, 0
-- There's no real reason to use pairs, but I've preserved it anyways
__pairs: =>
next, @lines, nil
if haveDepCtrl
return version\register LineCollection
else
return LineCollection

View file

@ -0,0 +1,63 @@
return {
version: "1.0.0"
debug: (...) ->
aegisub.log 4, ...
aegisub.log 4, '\n'
warn: (...) ->
aegisub.log 2, ...
aegisub.log 2, '\n'
-- I am not sure this is the logical place for this function.
checkCancellation: ->
if aegisub.progress.is_cancelled!
aegisub.cancel!
dump: ( item, ignore ) ->
level = 2
if "table" != type item
aegisub.log level, tostring item
aegisub.log level, "\n"
return
count = 1
tablecount = 1
result = { "{ @#{tablecount}" }
seen = { [item]: tablecount }
recurse = ( item, space ) ->
for key, value in pairs item
unless key == ignore
if "number" == type key
key = "##{key}"
if "table" == type value
unless seen[value]
tablecount += 1
seen[value] = tablecount
count += 1
result[count] = space .. "#{key}: { @#{tablecount}"
recurse value, space .. " "
count += 1
result[count] = space .. "}"
else
count += 1
result[count] = space .. "#{key}: @#{seen[value]}"
else
if "string" == type value
value = ("%q")\format value
count += 1
result[count] = space .. "#{key}: #{value}"
recurse item, " "
count += 1
result[count] = "}\n"
aegisub.log level, table.concat result, "\n"
windowError: ( errorMessage ) ->
aegisub.dialog.display { { class: "label", label: errorMessage } }, { "&Close" }, { cancel: "&Close" }
aegisub.cancel!
}

View file

@ -0,0 +1,20 @@
return {
version: "1.0.0"
round: ( num, idp ) ->
mult = 10^(idp or 0)
math.floor( num * mult + 0.5 ) / mult
dCos: (a) ->
math.cos math.rad a
dSin: (a) ->
math.sin math.rad a
dAtan: (y, x) ->
math.deg math.atan2 y, x
uuid: ->
('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')\gsub "[xy]", ( char ) ->
('%x')\format char=="x" and math.random( 0, 15 ) or math.random 8, 11
}

View file

@ -0,0 +1,312 @@
local log, Line, LineCollection, Math, tags, Transform
version = '1.1.8'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'MotionHandler'
:version
description: 'A class for applying motion data to a LineCollection.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.MotionHandler'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
{ 'a-mo.Line', version: '1.5.3' }
{ 'a-mo.LineCollection', version: '1.3.0' }
{ 'a-mo.Math', version: '1.0.0' }
{ 'a-mo.Tags', version: '1.3.4' }
{ 'a-mo.Transform', version: '1.2.4' }
}
}
log, Line, LineCollection, Math, tags, Transform = version\requireModules!
else
Line = require 'a-mo.Line'
LineCollection = require 'a-mo.LineCollection'
log = require 'a-mo.Log'
Math = require 'a-mo.Math'
tags = require 'a-mo.Tags'
Transform = require 'a-mo.Transform'
class MotionHandler
@version: version
new: ( @lineCollection, mainData, rectClipData = { }, vectClipData = { } ) =>
-- Create a local reference to the options table.
@options = @lineCollection.options
@lineTrackingData = mainData.dataObject
@rectClipData = rectClipData.dataObject
@vectClipData = vectClipData.dataObject
@xDelta = 0
@yDelta = 0
@callbacks = { }
-- Do NOT perform any normal callbacks if mainData is shake
-- rotoshape. In theory it would be possible to do plain translation
-- because the SRS data contains a center_x and center_y field for
-- each frame.
unless 'SRS' == mainData.type or @options.main.clipOnly
if @options.main.xPosition or @options.main.yPosition or @options.main.xScale or @options.main.zRotation
if @options.main.absPos
@callbacks["(\\pos)%(([%-%d%.]+,[%-%d%.]+)%)"] = absolutePosition
else
@callbacks["(\\pos)%(([%-%d%.]+,[%-%d%.]+)%)"] = position
if @options.main.origin
@callbacks["(\\org)%(([%-%d%.]+,[%-%d%.]+)%)"] = origin
if @options.main.xScale then
@callbacks["(\\fsc[xy])([%d%.]+)"] = scale
if @options.main.border
@callbacks["(\\[xy]?bord)([%d%.]+)"] = scale
if @options.main.shadow
@callbacks["(\\[xy]?shad)([%-%d%.]+)"] = scale
if @options.main.blur
@callbacks["(\\blur)([%d%.]+)"] = blur
if @options.main.zRotation
@callbacks["(\\frz?)([%-%d%.]+)"] = rotate
-- Don't support SRS for rectangular clips.
if @rectClipData and 'SRS' != rectClipData.type
@callbacks['(\\i?clip)(%([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+%))'] = rectangularClip
if @vectClipData
if 'SRS' == vectClipData.type
@callbacks['(\\i?clip)%(([^,]-)%)'] = vectorClipSRS
else
@callbacks['(\\i?clip)(%([^,]-%))'] = vectorClip
@resultingCollection = LineCollection @lineCollection.sub
@resultingCollection.shouldInsertLines = true
@resultingCollection.options = @options
-- This has to be copied over for clip interpolation
@resultingCollection.meta = @lineCollection.meta
for line in *@lineCollection.lines
if @options.main.linear and not (@options.main.origin and line.hasOrg) and not ((@rectClipData or @vectClipData) and line.hasClip)
line.method = linear
else
line.method = nonlinear
applyMotion: =>
setProgress = aegisub.progress.set
setProgress 0
totalLines = #@lineCollection.lines
-- The lines are collected in reverse order in LineCollection so
-- that we don't need to do things in reverse here.
insertNumber = @lineCollection.lines[totalLines].number
for index = 1, totalLines
with line = @lineCollection.lines[index]
-- start frame of line relative to start frame of tracked data
.relativeStart = .startFrame - @lineCollection.startFrame + 1
-- end frame of line relative to start frame of tracked data
.relativeEnd = .endFrame - @lineCollection.startFrame
.number = insertNumber
.method @, line
setProgress index/totalLines*100
return @resultingCollection
linear = ( line ) =>
moveTag = tags.allTags.move
posTag = tags.allTags.pos
with line
startFrameTime = aegisub.ms_from_frame aegisub.frame_from_ms .start_time
frameAfterStartTime = aegisub.ms_from_frame aegisub.frame_from_ms( .start_time ) + 1
frameBeforeEndTime = aegisub.ms_from_frame aegisub.frame_from_ms( .end_time ) - 1
endFrameTime = aegisub.ms_from_frame aegisub.frame_from_ms .end_time
-- Calculates the time length (in ms) from the start of the first
-- subtitle frame to the actual start of the line time.
beginTime = math.floor 0.5*(startFrameTime + frameAfterStartTime) - .start_time
-- Calculates the total length of the line plus the difference
-- (which is negative) between the start of the last frame the
-- line is on and the end time of the line.
endTime = math.floor 0.5*(frameBeforeEndTime + endFrameTime) - .start_time
if .move
.text = .text\gsub moveTag.pattern, ->
move = .move
progress = (.start_time - move.start)/(move.end - move.start)
return posTag\format moveTag\interpolate {move.x1, move.y1}, {move.x2, move.y2}, progress
for pattern, callback in pairs @callbacks
log.checkCancellation!
.text = .text\gsub pattern, ( tag, value ) ->
values = { }
for frame in *{ .relativeStart, .relativeEnd }
@lineTrackingData\calculateCurrentState frame
values[#values+1] = callback @, value, frame
("%s%s\\t(%d,%d,%s%s)")\format tag, values[1], beginTime, endTime, tag, values[2]
if @options.main.xPosition or @options.main.yPosition
.text = .text\gsub "\\pos(%b())\\t%((%d+,%d+),\\pos(%b())%)", ( start, time, finish ) ->
"\\move" .. start\sub( 1, -2 ) .. ',' .. finish\sub( 2, -2 ) .. ',' .. time .. ")"
@resultingCollection\addLine Line( line, nil, { wasLinear: true } ), nil, true, true
nonlinear = ( line ) =>
moveTag = tags.allTags.move
posTag = tags.allTags.pos
for frame = line.relativeEnd, line.relativeStart, -1
with line
log.checkCancellation!
newStartTime = math.floor(math.max(0, aegisub.ms_from_frame( @lineCollection.startFrame + frame - 1 ))/10)*10
newEndTime = math.floor(aegisub.ms_from_frame( @lineCollection.startFrame + frame )/10)*10
timeDelta = newStartTime - math.floor(math.max(0,aegisub.ms_from_frame( @lineCollection.startFrame + .relativeStart - 1 ))/10)*10
local newText
if @options.main.killTrans
newText = \interpolateTransformsCopy timeDelta, newStartTime
else
newText = \detokenizeTransformsCopy timeDelta
fadeTag = tags.allTags.fade
if @options.main.killTrans
local fade
newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub fadeTag.pattern, ( value ) ->
fade = fadeTag\convert value
return ""
if fade
-- multiplies every alpha tag by a scaling factor determined by the fade envelope
local fadeFactor
-- Piecewise function done with logicals :)
-- probably should go in fadeTag\interpolate, but I dunno how that whole section is supposed to work
f = { k, tonumber v for k, v in pairs fade }
fadeFactor = (
(timeDelta < f.t1) and f.a1 or
(timeDelta < f.t2) and f.a1 + (f.a2 - f.a1) * (timeDelta - f.t1) / (f.t2 - f.t1) or
(timeDelta < f.t3) and f.a2 or
(timeDelta < f.t4) and f.a2 + (f.a3 - f.a2) * (timeDelta - f.t3) / (f.t4 - f.t3) or
f.a3
)
-- factor between 0 and 1 representing opacity of fade line
fadeFactor = (255 - fadeFactor) / 255
-- apply the opacity factor to all alpha tags
newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub "(\\[1234]?a[lpha]-)&H(%x%x)&", ( alpha, value ) ->
value = Math.round( 255 - ( fadeFactor * (255 - tonumber value, 16) ) )
return alpha .. "&H%02X&"\format value
else
-- modified each fade tag in every override block
newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub fadeTag.pattern, ( value ) ->
fade = fadeTag\convert value
-- Erroneous fade tags are being ignored and left sitting around as long as they doesn't have 2 or 7 arguments.
-- if t1 == nil
-- message = "There is a malformed \\fade you must fix.\n\\fade requires 7 integer arguments.\nLine: #{.number}, tag: \\fade(#{fade})."
-- if fade\match("(%d+),(%d+)")
-- message ..= "\nPerhaps you meant to use \\fad."
-- log.windowError message
for i = 4, 7 -- t1 - t4
fade[i] -= timeDelta
return fadeTag\format fade
if .move
newText = newText\gsub moveTag.pattern, ->
move = .move
progress = (timeDelta - move.start)/(move.end - move.start)
return posTag\format moveTag\interpolate {move.x1, move.y1}, {move.x2, move.y2}, progress
-- In theory, this is more optimal if we loop over the frames on
-- the outside loop and over the lines on the inside loop, as
-- this only needs to be calculated once for each frame, whereas
-- currently it is being calculated for each frame for each
-- line. However, if the loop structure is changed, then
-- inserting lines into the resultingCollection would need to be
-- more clever to compensate for the fact that lines would no
-- longer be added to it in order.
@lineTrackingData\calculateCurrentState frame
-- iterate through the necessary operations
for pattern, callback in pairs @callbacks
newText = newText\gsub pattern, ( tag, value ) ->
tag .. callback @, value, frame
newLine = Line line, @resultingCollection, {
text: newText,
start_time: newStartTime,
end_time: newEndTime,
transformsAreTokenized: false,
}
newLine.karaokeShift = (newStartTime - .start_time)*0.1
@resultingCollection\addLine newLine, nil, true, true
position = ( pos, frame ) =>
x, y = pos\match "([%-%d%.]+),([%-%d%.]+)"
x, y = positionMath x, y, @lineTrackingData
("(%g,%g)")\format Math.round( x, 2 ), Math.round( y, 2 )
positionMath = ( x, y, data ) ->
x = (tonumber( x ) - data.xStartPosition)*data.xRatio
y = (tonumber( y ) - data.yStartPosition)*data.yRatio
radius = math.sqrt( x^2 + y^2 )
alpha = Math.dAtan( y, x )
x = data.xCurrentPosition + radius*Math.dCos( alpha - data.zRotationDiff )
y = data.yCurrentPosition + radius*Math.dSin( alpha - data.zRotationDiff )
return x, y
absolutePosition = ( pos, frame ) =>
x, y = pos\match "([%-%d%.]+),([%-%d%.]+)"
@xDelta = @lineTrackingData.xPosition[frame] - x
@yDelta = @lineTrackingData.yPosition[frame] - y
("(%g,%g)")\format Math.round( @lineTrackingData.xPosition[frame], 2 ), Math.round( @lineTrackingData.yPosition[frame], 2 )
-- Needs to be fixed.
origin = ( origin, frame ) =>
ox, oy = origin\match("([%-%d%.]+),([%-%d%.]+)")
ox, oy = positionMath ox, oy, @lineTrackingData
("(%g,%g)")\format Math.round( ox, 2 ), Math.round( oy, 2 )
scale = ( scale, frame ) =>
scale *= @lineTrackingData.xRatio
tostring Math.round scale, 2
blur = ( blur, frame ) =>
ratio = @lineTrackingData.xRatio
ratio = 1 - (1 - ratio)*@options.main.blurScale
tostring Math.round blur*ratio, 2
rotate = ( rotation, frame ) =>
rotation += @lineTrackingData.zRotationDiff
tostring Math.round rotation, 2
rectangularClip = ( clip, frame ) =>
@rectClipData\calculateCurrentState frame
@rectClipData.zRotationDiff = 0
return clip\gsub "([%.%d%-]+),([%.%d%-]+)", ( x, y ) ->
x, y = x + @xDelta, y + @yDelta
x, y = positionMath x, y, @rectClipData
("%g,%g")\format Math.round( x, 2 ), Math.round( y, 2 )
vectorClip = ( clip, frame ) =>
-- This is redundant if vectClipData is the same as
-- lineTrackingData.
@vectClipData\calculateCurrentState frame
return clip\gsub "([%.%d%-]+) ([%.%d%-]+)", ( x, y ) ->
x, y = x + @xDelta, y + @yDelta
x, y = positionMath x, y, @vectClipData
("%g %g")\format Math.round( x, 2 ), Math.round( y, 2 )
vectorClipSRS = ( clip, frame ) =>
return '(' .. clip .. ' ' .. @vectClipData.data[frame]\sub( 1, -2 ) .. ')'
if haveDepCtrl
return version\register MotionHandler
else
return MotionHandler

View file

@ -0,0 +1,130 @@
local log
version = '1.0.2'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'ShakeShapeHandler'
:version
description: 'A class for parsing shake shape motion data.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.ShakeShapeHandler'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
}
}
log = version\requireModules!
else
log = require 'a-mo.Log'
class ShakeShapeHandler
@version: version
new: ( input, @scriptHeight ) =>
if input
unless @parseRawDataString input
@parseFile input
if @rawData
@createDrawings!
parseRawDataString: ( rawDataString ) =>
if rawDataString\match "^shake_shape_data 4.0"
@tableize rawDataString
return true
return false
parseFile: ( fileName ) =>
if fileName\match "^\"[^\"]-\"$"
fileName = fileName\sub 2, -2
if file = io.open fileName, 'r'
return @parseRawDataString file\read '*a'
return false
tableize: ( rawDataString ) =>
shapes = rawDataString\match "num_shapes (%d+)"
@rawData = { }
rawDataString\gsub "([^\r\n]+)", ( line ) ->
if line\match "vertex_data"
table.insert @rawData, line
@numShapes = tonumber shapes
@length = #@rawData / @numShapes
createDrawings: =>
@data = { }
for baseIndex = 1, @length
results = { }
for curveIndex = baseIndex, @numShapes*@length, @length
line = @rawData[curveIndex]
table.insert results, convertVertex line, @scriptHeight
table.insert @data, table.concat results, ' '
fields = { "vx", "vy", "lx", "ly", "rx", "ry" }
updateCurve = ( curve, height, args ) ->
for index = 1, 6
field = fields[index]
if index % 2 == 0
curve[field] = height - args[index]
else
curve[field] = args[index]
processSegment = ( curveState, prevCurve, currCurve ) ->
result = ''
if ( prevCurve.vx == prevCurve.rx and prevCurve.vy == prevCurve.ry and
currCurve.lx == currCurve.vx and currCurve.ly == currCurve.vy )
unless curveState == 'l'
curveState = 'l'
result ..= 'l '
return curveState, result .. "#{currCurve.vx} #{currCurve.vy} "
else
unless curveState == 'b'
curveState = 'b'
result ..= 'b '
return curveState, result .. "#{prevCurve.rx} #{prevCurve.ry} #{currCurve.lx} #{currCurve.ly} #{currCurve.vx} #{currCurve.vy} "
convertVertex = ( vertex, scriptHeight ) ->
curveState = 'm'
drawString = {'m '}
prevCurve = { }
currCurve = { }
vertex = vertex\gsub "vertex_data ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+", ( ... ) ->
updateCurve prevCurve, scriptHeight, { ... }
table.insert drawString, "#{prevCurve.vx} #{prevCurve.vy} "
return ""
firstCurve = { k, v for k, v in pairs prevCurve }
vertex\gsub "([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) ([%-%.%d]+) [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+ [%-%.%d]+", ( ... ) ->
updateCurve currCurve, scriptHeight, { ... }
curveState, segment = processSegment curveState, prevCurve, currCurve
table.insert drawString, segment
prevCurve, currCurve = currCurve, prevCurve
curveState, segment = processSegment curveState, prevCurve, firstCurve
table.insert drawString, segment
return table.concat drawString
-- A function stub because I am too lazy to do this sort of thing
-- properly.
calculateCurrentState: =>
checkLength: ( totalFrames ) =>
if totalFrames == @length
true
else
false
if haveDepCtrl
return version\register ShakeShapeHandler
else
return ShakeShapeHandler

View file

@ -0,0 +1,153 @@
local json, log
version = '0.1.3'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'Statistics'
:version
description: 'A class for proving how cool you are.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.Statistics'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'json' }
{ 'a-mo.Log', version: '1.0.0' }
}
}
json, log = version\requireModules!
else
json = require 'json'
log = require 'a-mo.Log'
-- example = {
-- macroRunCount: {
-- Apply: 0
-- Trim: 0
-- Revert: 0
-- }
-- longestLine: 0
-- longestTrack: 0
-- largestOutput: 0
-- totalProduced: 0
-- uuid: 0
-- }
-- No way to migrate to different layouts. Seems like a pain in the ass.
-- Probably won't get implemented.
class Statistics
@version: version
new: ( @stats, fileName, filePath = aegisub.decode_path( '?user' ) ) =>
@fileName = ('%s/%s')\format filePath, fileName
@read!
merge = ( memory, disk, seenTables ) ->
unless seenTables[memory]
seenTables[memory] = true
for k, memVal in pairs memory
if ("table" == type( disk )) and (nil != disk[k])
diskVal = disk[k]
if ("table" == type( diskVal )) and ("table" == type( memVal ))
merge memVal, diskVal, seenTables
else
memory[k] = diskVal
read: =>
if fileHandle = io.open @fileName, 'r'
success, serializedStats = pcall json.decode, fileHandle\read '*a'
fileHandle\close!
unless success
log.warn "Couldn't parse stats from #{@filename} as valid json. This file will be overwritten."
@write!
return
if serializedStats
merge @stats, serializedStats, {}
else
@write!
write: =>
if fileHandle = io.open @fileName, 'w'
serializedStats = json.encode @stats
if serializedStats
fileHandle\write serializedStats
else
log.debug "Couldn't serialize stats."
fileHandle\close!
else
log.debug "Can't write statsfile: #{@fileName}"
fullFieldNamePriv = false
fieldBaseNamePriv = false
fieldNamePriv = false
fieldPriv = false
pushFieldPriv = ( fieldName ) =>
-- primary cache: nothing needs to change.
if fullFieldNamePriv == fieldName
return
-- secondary cache: fieldPriv doesn't need to change, nor does
-- fieldBaseNamePriv, but fullFieldNamePriv and fieldNamePriv do.
tempFieldName = fieldName\gsub ".+%.", ""
if fieldBaseNamePriv == fieldName\sub 0, -(#tempFieldName + 2)
fullFieldNamePriv = fieldName
fieldNamePriv = tempFieldName
return
-- Have to do everything from scratch.
fieldPriv = @stats
done = false
fieldNamePriv = fieldName\gsub "([^%.]+)%.", ( subName ) ->
if done
return nil
if "table" != type fieldPriv[subName]
done = true
return nil
fieldPriv = fieldPriv[subName]
return ""
-- Bad things will occur if fieldNamePriv has a '.' in it.
fullFieldNamePriv = fieldName
fieldBaseNamePriv = fieldName\sub 0, -(#fieldNamePriv + 2)
-- Accept syntax like 'macroRunCount.Apply' for fieldName.
-- `valueCb` is a function with the signature ( currentValue )
setValuePriv = ( fieldName, valueCb ) =>
pushFieldPriv @, fieldName
fieldPriv[fieldNamePriv] = valueCb fieldPriv[fieldNamePriv]
incrementValue: ( fieldName, amount = 1 ) =>
setValuePriv @, fieldName, ( value ) ->
value + amount
-- Convenience.
decrementValue: ( fieldName, amount = 1 ) =>
setValuePriv @, fieldName, ( value ) ->
value - amount
setValue: ( fieldName, newValue ) =>
setValuePriv @, fieldName, ( value ) ->
newValue
setMax: ( fieldName, amount ) =>
setValuePriv @, fieldName, ( value ) ->
math.max value, amount
setMin: ( fieldName, amount ) =>
setValuePriv @, fieldName, ( value ) ->
math.min value, amount
getValue: ( fieldName ) =>
pushFieldPriv @, fieldName
return fieldPriv[fieldNamePriv]
if haveDepCtrl
return version\register Statistics
else
return Statistics

View file

@ -0,0 +1,193 @@
local log, Transform
version = '1.3.4'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'Tags'
:version
description: 'A mess for manipulating tags.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.Tags'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
{ 'a-mo.Transform', version: '1.2.3' }
}
}
log, Transform = version\requireModules!
else
log = require 'a-mo.Log'
-- In the following conversion functions, self refers to the tag table.
convertStringValue = ( value ) =>
return value
convertNumberValue = ( value ) =>
return tonumber value
convertHexValue = ( value ) =>
return tonumber value, 16
convertColorValue = ( value ) =>
output = { }
for i = 1, 5, 2
table.insert output, tonumber value\sub( i, i+1 ), 16
output.r = output[3]
output.b = output[1]
output.g = output[2]
return output
convertKaraoke = ( ... ) =>
args = {...}
@tag = args[1]
return tonumber args[2]
-- This doesn't actually work with vector clips but i dont care.
convertMultiValue = ( value ) =>
output = { }
value\gsub "[%.%d%-]+", ( coord ) ->
table.insert output, coord
for index = 1, #@fieldnames
output[@fieldnames[index]] = output[index]
return output
convertTransformValue = ( value ) =>
-- awkwardly solve circular require.
Transform = Transform or require 'a-mo.Transform'
return Transform\fromString value
interpolateNumber = ( before, after, progress ) =>
return (1 - progress)*before + progress*after
interpolateMulti = ( before, after, progress ) =>
result = { }
for index = 1, #@fieldnames
key = @fieldnames[index]
result[index] = interpolateNumber @, before[index], after[index], progress
result[key] = result[index]
return result
interpolatePosition = ( before, after, progress ) =>
return {
interpolateNumber @, before[1], after[1], progress
interpolateNumber @, before[2], after[2], progress
}
interpolateColor = ( before, after, progress ) =>
return interpolateMulti { fieldnames: { 'b', 'g', 'r' } }, before, after, progress
formatString = ( value ) =>
return @tag .. value
formatInt = ( value ) =>
return ("%s%d")\format @tag, value
formatFloat = ( value ) =>
return ("%s%g")\format @tag, value
formatAlpha = ( alpha ) =>
return ("%s&H%02X&")\format @tag, alpha
formatColor = ( color ) =>
return ("%s&H%02X%02X%02X&")\format @tag, color.b, color.g, color.r
formatKaraoke = ( time ) =>
result = ("%s%d")\format @tag, time
return result
formatTransform = ( transform ) =>
return transform\toString!
formatMulti = ( value ) =>
return ("%s(%s)")\format @tag, table.concat value, ','
allTags = {
fontName: { pattern: "\\fn([^\\}]+)" , tag: "\\fn" , format: formatString, style: "fontname" , convert: convertStringValue }
fontSize: { pattern: "\\fs(%d+)" , tag: "\\fs" , format: formatInt , style: "fontsize", transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
fontSp: { pattern: "\\fsp([%.%d%-]+)" , tag: "\\fsp" , format: formatFloat , style: "spacing" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
xscale: { pattern: "\\fscx([%d%.]+)" , tag: "\\fscx" , format: formatFloat , style: "scale_x" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
yscale: { pattern: "\\fscy([%d%.]+)" , tag: "\\fscy" , format: formatFloat , style: "scale_y" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
zrot: { pattern: "\\frz?([%-%d%.]+)" , tag: "\\frz" , format: formatFloat , style: "angle" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
xrot: { pattern: "\\frx([%-%d%.]+)" , tag: "\\frx" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
yrot: { pattern: "\\fry([%-%d%.]+)" , tag: "\\fry" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
border: { pattern: "\\bord([%d%.]+)" , tag: "\\bord" , format: formatFloat , style: "outline" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
xborder: { pattern: "\\xbord([%d%.]+)" , tag: "\\xbord", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
yborder: { pattern: "\\ybord([%d%.]+)" , tag: "\\ybord", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
shadow: { pattern: "\\shad([%-%d%.]+)" , tag: "\\shad" , format: formatFloat , style: "shadow" , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
xshadow: { pattern: "\\xshad([%-%d%.]+)", tag: "\\xshad", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
yshadow: { pattern: "\\yshad([%-%d%.]+)", tag: "\\yshad", format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
reset: { pattern: "\\r([^\\}]*)" , tag: "\\r" , format: formatString , convert: convertStringValue }
alpha: { pattern: "\\alpha&H(%x%x)&" , tag: "\\alpha", format: formatAlpha , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha" }
alpha1: { pattern: "\\1a&H(%x%x)&" , tag: "\\1a" , format: formatAlpha , style: "color1" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
alpha2: { pattern: "\\2a&H(%x%x)&" , tag: "\\2a" , format: formatAlpha , style: "color2" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
alpha3: { pattern: "\\3a&H(%x%x)&" , tag: "\\3a" , format: formatAlpha , style: "color3" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
alpha4: { pattern: "\\4a&H(%x%x)&" , tag: "\\4a" , format: formatAlpha , style: "color4" , transformable: true , convert: convertHexValue , interpolate: interpolateNumber, type: "alpha", affectedBy: { "alpha" } }
color1: { pattern: "\\1?c&H(%x+)&" , tag: "\\1c" , format: formatColor , style: "color1" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
color2: { pattern: "\\2c&H(%x+)&" , tag: "\\2c" , format: formatColor , style: "color2" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
color3: { pattern: "\\3c&H(%x+)&" , tag: "\\3c" , format: formatColor , style: "color3" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
color4: { pattern: "\\4c&H(%x+)&" , tag: "\\4c" , format: formatColor , style: "color4" , transformable: true , convert: convertColorValue , interpolate: interpolateColor , type: "color" }
be: { pattern: "\\be([%d%.]+)" , tag: "\\be" , format: formatInt , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
blur: { pattern: "\\blur([%d%.]+)" , tag: "\\blur" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
xshear: { pattern: "\\fax([%-%d%.]+)" , tag: "\\fax" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
yshear: { pattern: "\\fay([%-%d%.]+)" , tag: "\\fay" , format: formatFloat , transformable: true , convert: convertNumberValue, interpolate: interpolateNumber }
align: { pattern: "\\an([1-9])" , tag: "\\an" , format: formatInt , style: "align" , convert: convertNumberValue , global: true }
-- bold, italic, underline and strikeout are actually stored in the style table as boolean values.
bold: { pattern: "\\b(%d+)" , tag: "\\b" , format: formatInt , style: "bold" , convert: convertNumberValue }
underline:{ pattern: "\\u([01])" , tag: "\\u" , format: formatInt , style: "underline" , convert: convertNumberValue }
italic: { pattern: "\\i([01])" , tag: "\\i" , format: formatInt , style: "italic" , convert: convertNumberValue }
strike: { pattern: "\\s([01])" , tag: "\\s" , format: formatInt , style: "strikeout" , convert: convertNumberValue }
drawing: { pattern: "\\p(%d+)" , tag: "\\p" , format: formatInt , convert: convertNumberValue }
transform:{ pattern: "\\t(%(.-%))" , tag: "\\t" , format: formatTransform , convert: convertTransformValue }
karaoke: { pattern: "(\\[kK][fo]?)(%d+)" , format: formatInt , convert: convertKaraoke }
-- Problematic tags:
pos: { fieldnames: { "x", "y" } , output: "multi", pattern: "\\pos%(([%.%d%-]+,[%.%d%-]+)%)", tag: "\\pos" , format: formatMulti, convert: convertMultiValue, global: true }
org: { fieldnames: { "x", "y" } , output: "multi", pattern: "\\org%(([%.%d%-]+,[%.%d%-]+)%)", tag: "\\org" , format: formatMulti, convert: convertMultiValue, global: true }
fad: { fieldnames: { "in", "out" } , output: "multi", pattern: "\\fade?%((%d+,%d+)%)" , tag: "\\fad" , format: formatMulti, convert: convertMultiValue, global: true }
vectClip: { fieldnames: { "scale", "shape" }, output: "multi", pattern: "\\clip%((%d+,)?([^,]-)%)" , tag: "\\clip" , format: formatMulti, convert: convertMultiValue, global: true }
vectiClip:{ fieldnames: { "scale", "shape" }, output: "multi", pattern: "\\iclip%((%d+,)?([^,]-)%)" , tag: "\\iclip", format: formatMulti, convert: convertMultiValue, global: true }
rectClip: { fieldnames: { "xLeft", "yTop", "xRight", "yBottom" } , output: "multi", pattern: "\\clip%(([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+)%)?" , transformable: true, tag: "\\clip" , format: formatMulti, convert: convertMultiValue, interpolate: interpolateMulti, global: true }
rectiClip:{ fieldnames: { "xLeft", "yTop", "xRight", "yBottom" } , output: "multi", pattern: "\\iclip%(([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+)%)?", transformable: true, tag: "\\iclip", format: formatMulti, convert: convertMultiValue, interpolate: interpolateMulti, global: true }
move: { fieldnames: { "x1", "y1", "x2", "y2", "start", "end" } , output: "multi", pattern: "\\move%(([%.%d%-]+,[%.%d%-]+,[%.%d%-]+,[%.%d%-]+,[%d%-]+,[%d%-]+)%)" , tag: "\\move" , format: formatMulti, convert: convertMultiValue, interpolate: interpolatePosition, global: true }
fade: { fieldnames: { "a1", "a2", "a3", "t1", "t2", "t3", "t4" }, output: "multi", pattern: "\\fade%((%d+,%d+,%d+,[%d%-]+,[%d%-]+,[%d%-]+,[%d%-]+)%)" , tag: "\\fade" , format: formatMulti, convert: convertMultiValue, global: true }
}
repeatTags = { }
oneTimeTags = { }
styleTags = { }
transformTags = { }
for k, v in pairs allTags
v.name = k
unless v.global
table.insert repeatTags, v
else
table.insert oneTimeTags, v
if v.style
table.insert styleTags, v
if v.transformable
table.insert transformTags, v
tags = {
:version
:repeatTags
:oneTimeTags
:styleTags
:transformTags
:allTags
}
if haveDepCtrl
return version\register tags
else
return tags

View file

@ -0,0 +1,143 @@
local log, Math, tags
version = '1.2.4'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'Transform'
:version
description: 'A class for managing the transform tag.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.Transform'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
{ 'a-mo.Math', version: '1.0.0' }
{ 'a-mo.Tags', version: '1.3.4' }
}
}
log, Math, tags = version\requireModules!
else
log = require 'a-mo.Log'
Math = require 'a-mo.Math'
class Transform
@version: version
tags = tags or require 'a-mo.Tags'
-- An alternate constructor.
@fromString: ( transformString, lineDuration, tagIndex, parentLine ) =>
transStart, transEnd, transExp, transEffect = transformString\match "%(([%-%d]*),?([%-%d]*),?([%d%.]*),?(.+)%)"
-- Catch the case of \t(2.345,\1c&H0000FF&), where the 2 gets
-- matched to transStart and the .345 gets matched to transEnd.
if tonumber( transStart ) and not tonumber( transEnd )
transExp = transStart .. transExp
transStart = ""
transExp = tonumber( transExp ) or 1
transStart = tonumber( transStart ) or 0
transEnd = tonumber( transEnd ) or 0
if transEnd == 0
transEnd = lineDuration
object = @ transStart, transEnd, transExp, transEffect, tagIndex, parentLine
object.rawString = transformString
return object
new: ( @startTime, @endTime, @accel, @effect, @index, @parentLine ) =>
@gatherTagsInEffect!
__tostring: => return @toString!
toString: ( line = @parentLine ) =>
if @effect == ""
return ""
elseif @endTime <= 0
return @effect
elseif @startTime > line.duration or @endTime < @startTime
return ""
elseif @accel == 1
return ("\\t(%s,%s,%s)")\format @startTime, @endTime, @effect
else
return ("\\t(%s,%s,%s,%s)")\format @startTime, @endTime, @accel, @effect
gatherTagsInEffect: =>
if @effectTags
return
@effectTags = { }
for tag in *tags.transformTags
@effect\gsub tag.pattern, ( value ) ->
log.debug "Found tag: %s -> %s", tag.name, value
unless @effectTags[tag]
@effectTags[tag] = { }
endValue = tag\convert value
table.insert @effectTags[tag], endValue
@effectTags[tag].last = endValue
collectPriorState: ( line, text, placeholder ) =>
-- Fill out all of the relevant tag defaults. This works great for
-- everything except \clip, which defaults to 0, 0, width, height
@priorValues = { }
for tag, _ in pairs @effectTags
if tag.style
@priorValues[tag] = line.properties[tag]
else
@priorValues[tag] = 0
if @effectTags[tags.allTags.rectClip]
@priorValues[tags.allTags.rectClip] = { 0, 0, line.parentCollection.meta.PlayResX, line.parentCollection.meta.PlayResY }
if @effectTags[tags.allTags.rectiClip]
@priorValues[tags.allTags.rectiClip] = { 0, 0, line.parentCollection.meta.PlayResX, line.parentCollection.meta.PlayResY }
i = 1
text\gsub "({.-})", ( tagBlock ) ->
for tag, _ in pairs @effectTags
if tag.affectedBy
newTagBlock = tagBlock\gsub ".-"..tag.pattern, ( value ) ->
@priorValues[tag] = tag\convert value
return ""
for tagName in *tag.affectedBy
newTag = tags.allTags[tagName]
newTagBlock = newTagBlock\gsub ".-"..newTag.pattern, ( value ) ->
@priorValues[tag] = newTag\convert value
return ""
else
tagBlock\gsub tag.pattern, ( value ) ->
@priorValues[tag] = tag\convert value
i += 1
return nil,
@index
interpolate: ( line, text, placeholder, time ) =>
@collectPriorState line, text, placeholder
linearProgress = (time - @startTime)/(@endTime - @startTime)
progress = math.pow linearProgress, @accel
text = text\gsub placeholder, ->
resultString = {}
for tag, endValues in pairs @effectTags
if linearProgress <= 0
table.insert resultString, tag\format @priorValues[tag]
elseif linearProgress >= 1
table.insert resultString, tag\format endValues.last
else
value = @priorValues[tag]
for endValue in *endValues
value = tag\interpolate value, endValue, progress
table.insert resultString, tag\format value
return table.concat resultString
return text
if haveDepCtrl
return version\register Transform
else
return Transform

View file

@ -0,0 +1,185 @@
local log
version = '1.0.5'
haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'
if haveDepCtrl
version = DependencyControl {
name: 'TrimHandler'
:version
description: 'A class for encoding video clips.'
author: 'torque'
url: 'https://github.com/TypesettingTools/Aegisub-Motion'
moduleName: 'a-mo.TrimHandler'
feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
{
{ 'a-mo.Log', version: '1.0.0' }
}
}
log = version\requireModules!
else
log = require 'a-mo.Log'
windows = jit.os == "Windows"
class TrimHandler
@version: version
@windows: windows
existingPresets: {
"x264", "ffmpeg"
}
-- Set up encoder presets.
defaults: {
x264: '"#{encbin}" --crf 16 --tune fastdecode -i 250 --fps 23.976 --sar 1:1 --index "#{prefix}/#{index}.index" --seek #{startf} --frames #{lenf} -o "#{prefix}/#{output}[#{startf}-#{endf}].mp4" "#{inpath}/#{input}"'
ffmpeg: '"#{encbin}" -ss #{startt} -an -sn -i "#{inpath}/#{input}" -q:v 1 -vsync passthrough -frames:v #{lenf} "#{prefix}/#{output}[#{startf}-#{endf}]-%05d.jpg"'
-- avs2pipe: 'echo FFVideoSource("#{inpath}#{input}",cachefile="#{prefix}#{index}.index").trim(#{startf},#{endf}).ConvertToRGB.ImageWriter("#{prefix}/#{output}-[#{startf}-#{endf}]\\",type="png").ConvertToYV12 > "#{temp}/a-mo.encode.avs"\nmkdir "#{prefix}#{output}-[#{startf}-#{endf}]"\n"#{encbin}" video "#{temp}/a-mo.encode.avs"\ndel "#{temp}/a-mo.encode.avs"'
-- vapoursynth:
}
-- Example trimConfig:
-- trimConfig = {
-- -- The prefix is the directory the output will be written to. It
-- -- is passed through aegisub.decode_path.
-- prefix: "?video"
-- -- The name of the built in encoding preset to use. Overridden by
-- -- command if that is neither nil nor an empty string.
-- preset: "x264"
-- -- The path of the executable used to actually do the encoding.
-- -- Full path is recommended as the shell environment may be
-- -- different than expected on non-windows systems.
-- encBin: "C:\x264.exe"
-- -- A custom encoding command that can be used to override the
-- -- built-in defaults. Usable token documentation to come.
-- -- Overrides preset if that is set.
-- command: nil
-- -- Script should attempt to create prefix directory.
-- makePfix: nil
-- -- Script should attempt to log output of the encoding command.
-- writeLog: true
-- }
new: ( trimConfig ) =>
@tokens = { }
if trimConfig.command != nil
trimConfig.command = trimConfig.command\gsub "[\t \r\n]*$", ""
if trimConfig.command != ""
@command = trimConfig.command
else
@command = @defaults[trimConfig.preset]
else
@command = @defaults[trimConfig.preset]
@makePrefix = trimConfig.makePfix
@writeLog = trimConfig.writeLog
with @tokens
.temp = aegisub.decode_path "?temp"
-- For some reason, aegisub appends / to the end of ?temp but not
-- other tokens.
finalTemp = .temp\sub -1, -1
if finalTemp == '\\' or finalTemp == '/'
.temp = .temp\sub 1, -2
.encbin = trimConfig.encBin
.prefix = aegisub.decode_path trimConfig.prefix
.inpath = aegisub.decode_path "?video"
.log = aegisub.decode_path "#{.temp}/a-mo.encode.log"
getVideoName @
getVideoName = =>
with @tokens
video = aegisub.project_properties!.video_file
if video\len! == 0
log.windowError "Aegisub thinks your video is 0 frames long.\nTheoretically it should be impossible to get this error."
if video\match "^?dummy"
log.windowError "I can't encode that dummy video for you."
.input = video\gsub( "^[A-Z]:\\", "", 1 )\gsub ".+[^\\/]-[\\/]", "", 1
.index = .input\match "(.+)%.[^%.]+$"
.output = .index
calculateTrimLength: ( lineCollection ) =>
with @tokens
.startt = lineCollection.startTime / 1000
.endt = lineCollection.endTime / 1000
.lent = .endt - .startt
.startf = lineCollection.startFrame
.endf = lineCollection.endFrame - 1
.lenf = lineCollection.totalFrames
performTrim: =>
with platform = ({
[true]: {
pre: @tokens.temp
ext: ".ps1"
-- This needs to be run from cmd or it will not work.
exec: 'powershell -c iex "$(gc "%s" -en UTF8)"'
preCom: (@makePrefix and "mkdir -Force \"#{@tokens.prefix}\"; & " or "& ")
postCom: (@writeLog and " 2>&1 | Out-File #{@tokens.log} -en UTF8; if($LASTEXITCODE -ne 0) {echo \"If there is no log before this, your encoder is not a working executable or your encoding command is invalid.\" | ac -en utf8 #{@tokens.log}; exit 1}" or "") .. "; exit 0"
execFunc: ( encodeScript ) ->
-- clear out old logfile because it doesn't get overwritten
-- when certain errors occur.
if @writeLog
logFile = io.open @tokens.log, 'wb'
logFile\close! if logFile
success = os.execute encodeScript
if @writeLog and not success
logFile = io.open @tokens.log, 'r'
unless logFile
log.windowError "Could not read log file #{@tokens.log}.\nSomething has gone horribly wrong."
encodeLog = logFile\read '*a'
logFile\close!
log.warn "\nEncoding error:"
log.warn encodeLog
log.windowError "Encoding failed. Log has been printed to progress window."
elseif not success
log.windowError "Encoding seems to have failed but you didn't write a log file."
}
[false]: {
pre: @tokens.temp
ext: ".sh"
exec: 'sh "%s"'
preCom: @makePrefix and "mkdir -p \"#{@tokens.prefix}\"\n" or ""
postCom: " 2>&1; if [[ $? -ne 0 ]]; then echo \"If there is no log before this, your encoder is not a working executable or your encoding command is invalid.\"; false; fi"
execFunc: ( encodeScript ) ->
logFile = io.popen encodeScript, 'r'
encodeLog = logFile\read '*a'
-- When closing a file handle created with io.popen,
-- file:close returns the same values returned by
-- os.execute.
success = logFile\close!
unless success
log.warn "\nEncoding error:"
log.warn encodeLog
log.windowError "Encoding failed. Log has been printed to progress window."
}
})[windows]
-- check encoder binary exists
encoder = io.open @tokens.encbin, "rb"
unless encoder
log.windowError "Encoding binary (#{@tokens.encbin}) does not appear to exist."
encoder\close!
encodeScript = aegisub.decode_path "#{.pre}/a-mo.encode#{.ext}"
encodeScriptFile = io.open encodeScript, "w+"
unless encodeScriptFile
log.windowError "Encoding script could not be written.\nSomething is wrong with your temp dir (#{.pre})."
encodeString = .preCom .. @command\gsub( "#{(.-)}", ( token ) -> @tokens[token] ) .. .postCom
if windows
encodeString = encodeString\gsub "`", "``"
log.debug encodeString
encodeScriptFile\write encodeString
encodeScriptFile\close!
.execFunc .exec\format encodeScript
if haveDepCtrl
return version\register TrimHandler
else
return TrimHandler

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,362 @@
[[
README
This file is a library of commonly used functions across all my automation
scripts. This way, if there are errors or updates for any of these functions,
I'll only need to update one file.
The filename is a bit vain, perhaps, but I couldn't come up with anything else.
]]
DependencyControl = require("l0.DependencyControl")
version = DependencyControl{
name: "LibLyger",
version: "2.0.3",
description: "Library of commonly used functions across all of lyger's automation scripts.",
author: "lyger",
url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts",
moduleName: "lyger.LibLyger",
feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
{
"aegisub.util", "karaskel"
}
}
util = version\requireModules!
class LibLyger
msgs = {
preproc_lines: {
bad_type: "Error: argument #1 must be either a line object, an index into the subtitle object or a table of indexes; got a %s."
}
}
new: (sub, sel, generate_furigana) =>
@set_sub sub, sel, generate_furigana if sub
set_sub: (@sub, @sel = {}, generate_furigana = false) =>
@script_info, @lines, @dialogue, @dlg_cnt = {}, {}, {}, 0
for i, line in ipairs sub
@lines[i] = line
switch line.class
when "info" then @script_info[line.key] = line.value
when "dialogue"
@dlg_cnt += 1
@dialogue[@dlg_cnt], line.i = line, i
@meta, @styles = karaskel.collect_head @sub, generate_furigana
@preproc_lines @sel
insert_line: (line, i = #@lines + 1) =>
table.insert(@lines, i, line)
@sub.insert(i, line)
preproc_lines: (lines) =>
val_type = type lines
-- indexes into the subtitles object
if val_type == "number"
lines, val_type = {@lines[lines]}, "table"
assert val_type == "table", msgs.preproc_lines.bad_type\format val_type
-- line objects
if lines.raw and lines.section and not lines.duration
karaskel.preproc_line @sub, @meta, @styles, lines
-- tables of line numbers/objects such as the selection
else @preproc_lines line for line in *lines
-- returns a "Lua" portable version of the string
exportstring: (s) -> string.format "%q", s
--Lookup table for the nature of each kind of parameter
param_type: {
alpha: "alpha"
"1a": "alpha"
"2a": "alpha"
"3a": "alpha"
"4a": "alpha"
c: "color"
"1c": "color"
"2c": "color"
"3c": "color"
"4c": "color"
fscx: "number"
fscy: "number"
frz: "angle"
frx: "angle"
fry: "angle"
shad: "number"
bord: "number"
fsp: "number"
fs: "number"
fax: "number"
fay: "number"
blur: "number"
be: "number"
xbord: "number"
ybord: "number"
xshad: "number"
yshad: "number"
pos: "point"
org: "point"
clip: "clip"
}
--Convert float to neatly formatted string
float2str: (f) -> "%.3f"\format(f)\gsub("%.(%d-)0+$","%.%1")\gsub "%.$", ""
--Escapes string for use in gsub
esc: (str) -> str\gsub "([%%%(%)%[%]%.%*%-%+%?%$%^])","%%%1"
[[
Tags that can have any character after the tag declaration: \r, \fn
Otherwise, the first character after the tag declaration must be:
a number, decimal point, open parentheses, minus sign, or ampersand
]]
-- Remove listed tags from the given text
line_exclude: (text, exclude) ->
remove_t = false
new_text = text\gsub "\\([^\\{}]*)", (a) ->
if a\match "^r"
for val in *exclude
return "" if val == "r"
elseif a\match "^fn"
for val in *exclude
return "" if val == "fn"
else
tag = a\match "^[1-4]?%a+"
for val in *exclude
if val == tag
--Hacky exception handling for \t statements
if val == "t"
remove_t = true
return "\\#{a}"
elseif a\match "%)$"
return a\match("%b()") and "" or ")"
else
return ""
return "\\"..a
if remove_t
new_text = new_text\gsub "\\t%b()", ""
return new_text\gsub "{}", ""
-- Remove all tags except the given ones
line_exclude_except: (text, exclude) ->
remove_t = true
new_text = text\gsub "\\([^\\{}]*)", (a) ->
if a\match "^r"
for val in *exclude
return "\\#{a}" if val == "r"
elseif a\match "^fn"
for val in *exclude
return "\\#{a}" if val == "fn"
else
tag = a\match "^[1-4]?%a+"
for val in *exclude
if val == tag
remove_t = false if val == "t"
return "\\#{a}"
if a\match "^t"
return "\\#{a}"
elseif a\match "%)$"
return a\match("%b()") and "" or ")"
else return ""
if remove_t
new_text = new_text\gsub "\\t%b()", ""
return new_text
-- Returns the position of a line
get_default_pos: (line, align_x, align_y) =>
@preproc_lines line
x = {
@script_info.PlayResX - line.eff_margin_r,
line.eff_margin_l,
line.eff_margin_l + (@script_info.PlayResX - line.eff_margin_l - line.eff_margin_r) / 2
}
y = {
@script_info.PlayResY - line.eff_margin_b,
@script_info.PlayResY / 2
line.eff_margin_t
}
return x[align_x], y[align_y]
get_pos: (line) =>
posx, posy = line.text\match "\\pos%(([%d%.%-]*),([%d%.%-]*)%)"
unless posx
posx, posy = line.text\match "\\move%(([%d%.%-]*),([%d%.%-]*),"
return tonumber(posx), tonumber(posy) if posx
-- \an alignment
if align = tonumber line.text\match "\\an([%d%.%-]+)"
return @get_default_pos line, align%3 + 1, math.ceil align/3
-- \a alignment
elseif align = tonumber line.text\match "\\a([%d%.%-]+)"
return @get_default_pos line, align%4,
align > 8 and 2 or align> 4 and 3 or 1
-- no alignment tags (take karaskel values)
else return line.x, line.y
-- Returns the origin of a line
get_org: (line) =>
orgx, orgy = line.text\match "\\org%(([%d%.%-]*),([%d%.%-]*)%)"
if orgx
return orgx, orgy
else return @get_pos line
-- Returns a table of default values
style_lookup: (line) =>
@preproc_lines line
return {
alpha: "&H00&"
"1a": util.alpha_from_style line.styleref.color1
"2a": util.alpha_from_style line.styleref.color2
"3a": util.alpha_from_style line.styleref.color3
"4a": util.alpha_from_style line.styleref.color4
c: util.color_from_style line.styleref.color1
"1c": util.color_from_style line.styleref.color1
"2c": util.color_from_style line.styleref.color2
"3c": util.color_from_style line.styleref.color3
"4c": util.color_from_style line.styleref.color4
fscx: line.styleref.scale_x
fscy: line.styleref.scale_y
frz: line.styleref.angle
frx: 0
fry: 0
shad: line.styleref.shadow
bord: line.styleref.outline
fsp: line.styleref.spacing
fs: line.styleref.fontsize
fax: 0
fay: 0
xbord: line.styleref.outline
ybord: line.styleref.outline
xshad: line.styleref.shadow
yshad: line.styleref.shadow
blur: 0
be: 0
}
-- Modify the line tables so they are split at the same locations
match_splits: (line_table1, line_table2) ->
for i=1, #line_table1
text1 = line_table1[i].text
text2 = line_table2[i].text
insert = (target, text, i) ->
for j = #target, i+1, -1
target[j+1] = target[j]
target[i+1] = tag: "{}", text: target[i].text\match "#{LibLyger.esc(text)}(.*)"
target[i] = tag: target[i].tag, :text
if #text1 > #text2
-- If the table1 item has longer text, break it in two based on the text of table2
insert line_table1, text2, i
elseif #text2 > #text1
-- If the table2 item has longer text, break it in two based on the text of table1
insert line_table2, text1, i
return line_table1, line_table2
-- Remove listed tags from any \t functions in the text
time_exclude: (text, exclude) ->
text = text\gsub "(\\t%b())", (a) ->
b = a
for tag in *exclude
if a\match "\\#{tag}"
b = b\gsub(tag == "clip" and "\\#{tag}%b()" or "\\#{tag}[^\\%)]*", "")
return b
-- get rid of empty blocks
return text\gsub "\\t%([%-%.%d,]*%)", ""
-- Returns a state table, restricted by the tags given in "tag_table"
-- WILL NOT WORK FOR \fn AND \r
make_state_table: (line_table, tag_table) ->
this_state_table = {}
for i, val in ipairs line_table
temp_line_table = {}
pstate = LibLyger.line_exclude_except val.tag, tag_table
for j, ctag in ipairs tag_table
-- param MUST start in a non-alpha character, because ctag will never be \r or \fn
-- If it is, you fucked up
param = pstate\match "\\#{ctag}(%A[^\\{}]*)"
temp_line_table[ctag] = param if param
this_state_table[i] = temp_line_table
return this_state_table
interpolate: (this_table, start_state_table, end_state_table, factor, preset) ->
this_current_state = {}
rebuilt_text = for k, val in ipairs this_table
temp_tag = val.tag
-- Cycle through all the tag blocks and interpolate
for ctag, param in pairs start_state_table[k]
temp_tag = "{}" if #temp_tag == 0
temp_tag = temp_tag\gsub "}", ->
tval_start, tval_end = start_state_table[k][ctag], end_state_table[k][ctag]
tag_type = LibLyger.param_type[ctag]
ivalue = switch tag_type
when "alpha"
util.interpolate_alpha factor, tval_start, tval_end
when "color"
util.interpolate_color factor, tval_start, tval_end
when "number", "angle"
nstart, nend = tonumber(tval_start), tonumber(tval_end)
if tag_type == "angle" and preset.c.flip_rot
nstart %= 360
nend %= 360
ndelta = nend - nstart
if 180 < math.abs ndelta
nstart += ndelta * 360 / math.abs ndelta
nvalue = util.interpolate factor, nstart, nend
nvalue += 360 if tag_type == "angle" and nvalue < 0
LibLyger.float2str nvalue
when "point", "clip" then nil -- not touched by this function
else ""
-- check for redundancy
if this_current_state[ctag] == ivalue
return "}"
this_current_state[ctag] = ivalue
return "\\#{ctag..ivalue}}"
temp_tag .. val.text
return table.concat(rebuilt_text)\gsub "{}", ""
write_table: (my_table, file, indent) ->
indent or= ""
charS, charE = " ", "\n"
--Opening brace of the table
file\write "#{indent}{#{charE}"
for key,val in pairs my_table
file\write switch type key
when "number" then indent..charS
when "string" then table.concat {indent, charS, "[", LibLyger.exportstring(key), "]="}
else "#{indent}#{charS}#{key}="
switch type val
when "table"
file\write charE
LibLyger.write_table val, file, indent..charS
file\write indent..charS
when "string" then file\write LibLyger.exportstring val
when "number" then file\write tostring val
when "boolean" then file\write val and "true" or "false"
file\write ","..charE
-- Closing brace of the table
file\write "#{indent}}#{charE}"
:version
return version\register LibLyger

View file

@ -0,0 +1,387 @@
DependencyControl = require "l0.DependencyControl"
version = DependencyControl {
name: "ASSParser",
version: "0.0.4",
description: "Utility function for parsing ASS files",
author: "Myaamori",
url: "http://github.com/TypesettingTools/Myaamori-Aegisub-Scripts",
moduleName: "myaa.ASSParser",
feed: "https://raw.githubusercontent.com/TypesettingTools/Myaamori-Aegisub-Scripts/master/DependencyControl.json",
{
"aegisub.re", "aegisub.util",
{"l0.Functional", version: "0.6.0", url: "https://github.com/TypesettingTools/Functional",
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"}
}
}
re, util, F = version\requireModules!
import lshift, rshift, band, bor from bit
parser = {}
STYLE_FORMAT_STRING = "Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, " ..
"OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, " ..
"Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, " ..
"MarginV, Encoding"
EVENT_FORMAT_STRING = "Layer, Start, End, Style, Name, MarginL, MarginR, " ..
"MarginV, Effect, Text"
DATA_FORMAT_STRING = "Id, Key, Value"
DIALOGUE_DEFAULTS =
actor: "", class: "dialogue", comment: false, effect: "",
start_time: 0, end_time: 0, layer: 0, margin_l: 0,
margin_r: 0, margin_t: 0, section: "[Events]", style: "Default",
text: "", extra: nil
STYLE_DEFAULTS =
class: "style", section: "[V4+ Styles]", name: "Default",
fontname: "Arial", fontsize: 45, color1: "&H00FFFFFF",
color2: "&H000000FF", color3: "&H00000000", color4: "&H00000000",
bold: false, italic: false, underline: false, strikeout: false,
scale_x: 100, scale_y: 100, spacing: 0, angle: 0,
borderstyle: 1, outline: 4.5, shadow: 4.5, align: 2,
margin_l: 23, margin_r: 23, margin_t: 23, encoding: 1
create_line_from = (line, fields)->
line = util.copy line
if fields
for key, value in pairs fields
line[key] = value
return line
parser.create_dialogue_line = (fields)->
line = create_line_from DIALOGUE_DEFAULTS, fields
line.extra = line.extra or {}
line
parser.create_style_line = (fields)-> create_line_from STYLE_DEFAULTS, fields
parser.decode_extradata_value = (value)->
enc, data = value\match "^([eu])(.*)$"
if enc == 'e'
return parser.inline_string_decode data
else
return parser.uudecode data
parse_format_line = (format_string)-> [match for match in format_string\gmatch "([^, ]+)"]
parser.raw_to_line = (raw, extradata=nil, format=nil)->
line_type, value = raw\match "^([^:]+):%s*(.*)$"
if not value
return nil
default_format = {Dialogue: EVENT_FORMAT_STRING,
Comment: EVENT_FORMAT_STRING,
Style: STYLE_FORMAT_STRING,
Data: DATA_FORMAT_STRING}
if line_type == "Format"
return {class: "format", format: parse_format_line value}
elseif not default_format[line_type]
return {class: "info", key: line_type, value: value}
format = format or parse_format_line default_format[line_type]
elements = F.string.split value, ",", 1, true, #format - 1
return nil if #elements != #format
fields = {format[i], elements[i] for i=1,#elements}
if line_type == "Dialogue" or line_type == "Comment"
line = parser.create_dialogue_line
actor: fields.Name, comment: line_type == "Comment"
effect: fields.Effect, start_time: F.util.assTimecode2ms(fields.Start)
end_time: F.util.assTimecode2ms(fields.End), layer: tonumber(fields.Layer)
margin_l: tonumber(fields.MarginL), margin_r: tonumber(fields.MarginR)
margin_t: tonumber(fields.MarginV), style: fields.Style
text: fields.Text
-- handle extradata (e.g. '{=32=33}Line text')
extramatch = re.match line.text, "^\\{((?:=\\d+)+)\\}(.*)$"
if extramatch
line.text = extramatch[3].str
if extradata
for id in extramatch[2].str\gmatch "=(%d+)"
id = tonumber id
if extradata[id]
eline = extradata[id]
line.extra[eline.key] = eline.value
else
aegisub.log 2,
"WARNING: Found extradata ID, but no extradata mapping provided: " ..
"#{raw}\n"
return line
elseif line_type == "Style"
boolean_map = {["-1"]: true, ["0"]: false}
line = parser.create_style_line
name: fields.Name, fontname: fields.Fontname
fontsize: tonumber(fields.Fontsize), color1: fields.PrimaryColour
color2: fields.SecondaryColour, color3: fields.OutlineColour
color4: fields.BackColour, bold: boolean_map[fields.Bold]
italic: boolean_map[fields.Italic], underline: boolean_map[fields.Underline]
strikeout: boolean_map[fields.StrikeOut], scale_x: tonumber(fields.ScaleX)
scale_y: tonumber(fields.ScaleY), spacing: tonumber(fields.Spacing)
angle: tonumber(fields.Angle), borderstyle: tonumber(fields.BorderStyle)
outline: tonumber(fields.Outline), shadow: tonumber(fields.Shadow)
align: tonumber(fields.Alignment), margin_l: tonumber(fields.MarginL)
margin_r: tonumber(fields.MarginR), margin_t: tonumber(fields.MarginV)
encoding: tonumber(fields.Encoding)
return line
elseif line_type == "Data"
return {class: "data", id: tonumber(fields.Id),
key: fields.Key, value: parser.decode_extradata_value fields.Value}
parser.line_to_raw = (line)->
if line.class == "dialogue"
prefix = if line.comment then "Comment" else "Dialogue"
"#{prefix}: #{line.layer},#{F.util.ms2AssTimecode line.start_time}," ..
"#{F.util.ms2AssTimecode line.end_time},#{line.style},#{line.actor}," ..
"#{line.margin_l},#{line.margin_r},#{line.margin_t},#{line.effect},#{line.text}"
elseif line.class == "style"
map = {[true]: "-1", [false]: "0"}
clr = (color)-> util.ass_style_color util.extract_color color
"Style: #{line.name},#{line.fontname},#{line.fontsize},#{clr line.color1}," ..
"#{clr line.color2},#{clr line.color3},#{clr line.color4},#{map[line.bold]}," ..
"#{map[line.italic]},#{map[line.underline]},#{map[line.strikeout]}," ..
"#{line.scale_x},#{line.scale_y},#{line.spacing},#{line.angle}," ..
"#{line.borderstyle},#{line.outline},#{line.shadow},#{line.align}," ..
"#{line.margin_l},#{line.margin_r},#{line.margin_t},#{line.encoding}"
elseif line.class == "info"
"#{line.key}: #{line.value}"
parser.inline_string_encode = (input)->
output = {}
for i=1,#input
c = input\byte i
if c <= 0x1F or c >= 0x80 or c == 0x23 or c == 0x2C or c == 0x3A or c == 0x7C
table.insert output, string.format "#%02X", c
else
table.insert output, input\sub i,i
return table.concat output
parser.inline_string_decode = (input)->
output = {}
i = 1
while i <= #input
if (input\sub i, i) != "#" or i + 1 > #input
table.insert output, input\sub i, i
else
table.insert output, string.char tonumber (input\sub i+1, i+2), 16
i += 2
i += 1
return table.concat output
parser.uuencode = (input)->
ret = {}
for pos=1,#input,3
chunk = input\sub pos, pos+2
src = [c\byte! for c in chunk\gmatch "."]
while #src < 3
src[#src+1] = 0
dst = {(rshift src[1], 2),
(bor (lshift (band src[1], 0x3), 4), (rshift (band src[2], 0xF0), 4)),
(bor (lshift (band src[2], 0xF), 2), (rshift (band src[3], 0xC0), 6)),
(band src[3], 0x3F)}
for i=1,math.min(#input - pos + 2, 4)
table.insert ret, dst[i] + 33
return table.concat [string.char i for i in *ret]
parser.uudecode = (input)->
ret = {}
pos = 1
while pos <= #input
chunk = input\sub pos, pos+3
src = [(string.byte c) - 33 for c in chunk\gmatch "."]
if #src > 1
table.insert ret, bor (lshift src[1], 2), (rshift src[2], 4)
if #src > 2
table.insert ret, bor (lshift (band src[2], 0xF), 4), (rshift src[3], 2)
if #src > 3
table.insert ret, bor (lshift (band src[3], 0x3), 6), src[4]
pos += #src
return table.concat [string.char i for i in *ret]
class ASSFile
new: (file)=>
@sections = {}
@styles = {}
@events = {}
@script_info = {}
@script_info_mapping = {}
@aegisub_garbage = {}
@aegisub_garbage_mapping = {}
@extradata = {}
@extradata_mapping = {}
@parse file
parse: (file)=>
@read_sections file
@parse_extradata!
@script_info = @parse_section "Script Info", {"info": true}
@aegisub_garbage = @parse_section "Aegisub Project Garbage", {"info": true}
@styles = @parse_section "V4+ Styles", {"style": true}
@events = @parse_section "Events", {"dialogue": true}
for info in *@script_info
@script_info_mapping[info.key] = info.value
for garbage in *@aegisub_garbage
@aegisub_garbage_mapping[garbage.key] = garbage.value
read_sections: (file)=>
current_section = nil
-- read lines from file, sort into sections
for row in file\lines!
-- remove BOM if present, remove newlines, and trim leading spaces
row = F.string.trimLeft (row\gsub "^\xEF\xBB\xBF", "")\gsub "[\r\n]*$", ""
if row == "" or row\match "^;"
continue
section = row\match "^%[(.*)%]$"
if section
current_section = section
@sections[current_section] = {}
continue
table.insert @sections[current_section], row
parse_extradata: =>
if @sections["Aegisub Extradata"]
for row in *@sections["Aegisub Extradata"]
line = parser.raw_to_line row
if not line or line.class != "data"
aegisub.log 2, "WARNING: Malformed data line: #{row}\n"
continue
@extradata[line.id] = line
@extradata_mapping[line.key] = @extradata_mapping[line.key] or {}
@extradata_mapping[line.key][line.value] = line.id
parse_section: (section, expected_classes)=>
lines = {}
return lines if not @sections[section]
format = nil
for row in *@sections[section]
line = parser.raw_to_line row, @extradata, format
if not line
aegisub.log 2, "WARNING: Malformed line: #{line}\n"
elseif line.class == "format"
format = line.format
elseif expected_classes[line.class]
table.insert lines, line
else
aegisub.log 2, "WARNING: Unexpected type #{line.class} in section #{section}\n"
return lines
parser.parse_file = (file)->
return ASSFile file
parser.generate_styles_section = (styles, callback)->
callback "[V4+ Styles]\n"
callback "Format: #{STYLE_FORMAT_STRING}\n"
for line in *styles
callback parser.line_to_raw(line) .. "\n"
parser.generate_events_section = (events, extradata_mapping, callback)->
callback "[Events]\n"
callback "Format: #{EVENT_FORMAT_STRING}\n"
-- find the largest extradata ID seen so far
last_eid = 0
if extradata_mapping
for key, v in pairs extradata_mapping
for value, eid in pairs v
last_eid = math.max last_eid, eid
extradata_to_write = {}
for line in *events
-- handle extradata
if line.extra and extradata_mapping
lineindices = {}
for key, value in pairs line.extra
-- look for data in the original file's extradata
cached_id = extradata_mapping[key] and extradata_mapping[key][value]
if not cached_id
-- if new extradata, generate new ID and cache it
last_eid += 1
cached_id = last_eid
extradata_mapping[key] = extradata_mapping[key] or {}
extradata_mapping[key][value] = cached_id
table.insert lineindices, cached_id
extradata_to_write[cached_id] = {key, value}
-- add indices to line text (e.g. {=32=33}Text)
if #lineindices > 0
table.sort lineindices
indexstring = table.concat ["=#{ind}" for ind in *lineindices]
line.text = "{#{indexstring}}" .. line.text
callback parser.line_to_raw(line) .. "\n"
out_indices = [ind for ind, _ in pairs extradata_to_write]
if #out_indices > 0
callback "\n[Aegisub Extradata]\n"
table.sort out_indices
for ind in *out_indices
{key, value} = extradata_to_write[ind]
encoded_data = parser.inline_string_encode value
-- a mystical incantation passed down from subtitle_format_ass.cpp
if 4*#value < 3*#encoded_data
value = "u" .. parser.uuencode value
else
value = "e" .. encoded_data
callback "Data: #{ind},#{key},#{value}\n"
parser.generate_script_info_section = (lines, callback, bom=true)->
if bom
callback "\xEF\xBB\xBF"
callback "[Script Info]\n"
for line in *lines
callback parser.line_to_raw(line) .. "\n"
parser.generate_aegisub_garbage_section = (lines, callback)->
callback "[Aegisub Project Garbage]\n"
for line in *lines
callback parser.line_to_raw(line) .. "\n"
parser.generate_file = (script_info, aegisub_garbage, styles, events, extradata_mapping, callback)->
sec_added = false
new_section = ->
if sec_added
callback "\n"
sec_added = true
if script_info
new_section!
parser.generate_script_info_section script_info, callback
if aegisub_garbage
new_section!
parser.generate_aegisub_garbage_section aegisub_garbage, callback
if styles
new_section!
parser.generate_styles_section styles, callback
if events
new_section!
parser.generate_events_section events, extradata_mapping, callback
parser.version = version
return version\register parser

View file

@ -0,0 +1,34 @@
local DependencyControl = require("l0.DependencyControl")
local version = DependencyControl{
name = "Penlight",
version = "1.6.0",
description = "Python-inspired utility library.",
author = "stevedonovan",
url = "http://github.com/Myaamori/Penlight",
moduleName = "myaa.pl",
}
package.path = package.path .. ";" .. aegisub.decode_path("?user/automation/include/myaa/?.lua")
local modules_to_load = {
utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
input=true,seq=true,lexer=true,stringx=true,
config=true,pretty=true,data=true,func=true,text=true,
operator=true,lapp=true,array2d=true,
comprehension=true,xml=true,types=true,
test = true, app = true, file = true, class = true,
luabalanced = true, permute = true, template = true,
url = true, compat = true, List = true, Map = true, Set = true,
OrderedMap = true, MultiMap = true, Date = true,
-- classes --
}
local modules = {}
for k, v in pairs(modules_to_load) do
modules[k] = require ("pl." .. k)
end
modules.version = version
return modules

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
--- A Map class.
--
-- > Map = require 'pl.Map'
-- > m = Map{one=1,two=2}
-- > m:update {three=3,four=4,two=20}
-- > = m == M{one=1,two=20,three=3,four=4}
-- true
--
-- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.pretty`
-- @classmod pl.Map
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local stdmt = utils.stdmt
local deepcompare = tablex.deepcompare
local pretty_write = require 'pl.pretty' . write
local Map = stdmt.Map
local Set = stdmt.Set
local class = require 'pl.class'
-- the Map class ---------------------
class(nil,nil,Map)
function Map:_init (t)
local mt = getmetatable(t)
if mt == Set or mt == Map then
self:update(t)
else
return t -- otherwise assumed to be a map-like table
end
end
local function makelist(t)
return setmetatable(t, require('pl.List'))
end
--- list of keys.
Map.keys = tablex.keys
--- list of values.
Map.values = tablex.values
--- return an iterator over all key-value pairs.
function Map:iter ()
return pairs(self)
end
--- return a List of all key-value pairs, sorted by the keys.
function Map:items()
local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self))
ls:sort(function(t1,t2) return t1[1] < t2[1] end)
return ls
end
-- Will return the existing value, or if it doesn't exist it will set
-- a default value and return it.
function Map:setdefault(key, defaultval)
return self[key] or self:set(key,defaultval) or defaultval
end
--- size of map.
-- note: this is a relatively expensive operation!
-- @class function
-- @name Map:len
Map.len = tablex.size
--- put a value into the map.
-- This will remove the key if the value is `nil`
-- @param key the key
-- @param val the value
function Map:set (key,val)
self[key] = val
end
--- get a value from the map.
-- @param key the key
-- @return the value, or nil if not found.
function Map:get (key)
return rawget(self,key)
end
local index_by = tablex.index_by
--- get a list of values indexed by a list of keys.
-- @param keys a list-like table of keys
-- @return a new list
function Map:getvalues (keys)
return makelist(index_by(self,keys))
end
--- update the map using key/value pairs from another table.
-- @tab table
-- @function Map:update
Map.update = tablex.update
--- equality between maps.
-- @within metamethods
-- @tparam Map m another map.
function Map:__eq (m)
-- note we explicitly ask deepcompare _not_ to use __eq!
return deepcompare(self,m,true)
end
--- string representation of a map.
-- @within metamethods
function Map:__tostring ()
return pretty_write(self,'')
end
return Map

View file

@ -0,0 +1,54 @@
--- MultiMap, a Map which has multiple values per key.
--
-- Dependencies: `pl.utils`, `pl.class`, `pl.List`, `pl.Map`
-- @classmod pl.MultiMap
local utils = require 'pl.utils'
local class = require 'pl.class'
local List = require 'pl.List'
local Map = require 'pl.Map'
-- MultiMap is a standard MT
local MultiMap = utils.stdmt.MultiMap
class(Map,nil,MultiMap)
MultiMap._name = 'MultiMap'
function MultiMap:_init (t)
if not t then return end
self:update(t)
end
--- update a MultiMap using a table.
-- @param t either a Multimap or a map-like table.
-- @return the map
function MultiMap:update (t)
utils.assert_arg(1,t,'table')
if Map:class_of(t) then
for k,v in pairs(t) do
self[k] = List()
self[k]:append(v)
end
else
for k,v in pairs(t) do
self[k] = List(v)
end
end
end
--- add a new value to a key. Setting a nil value removes the key.
-- @param key the key
-- @param val the value
-- @return the map
function MultiMap:set (key,val)
if val == nil then
self[key] = nil
else
if not self[key] then
self[key] = List()
end
self[key]:append(val)
end
end
return MultiMap

View file

@ -0,0 +1,167 @@
--- OrderedMap, a map which preserves ordering.
--
-- Derived from `pl.Map`.
--
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.List`, `pl.Map`
-- @classmod pl.OrderedMap
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local List = require 'pl.List'
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
local class = require 'pl.class'
local Map = require 'pl.Map'
local OrderedMap = class(Map)
OrderedMap._name = 'OrderedMap'
local rawset = rawset
--- construct an OrderedMap.
-- Will throw an error if the argument is bad.
-- @param t optional initialization table, same as for @{OrderedMap:update}
function OrderedMap:_init (t)
rawset(self,'_keys',List())
if t then
local map,err = self:update(t)
if not map then error(err,2) end
end
end
local assert_arg,raise = utils.assert_arg,utils.raise
--- update an OrderedMap using a table.
-- If the table is itself an OrderedMap, then its entries will be appended.
-- if it s a table of the form `{{key1=val1},{key2=val2},...}` these will be appended.
--
-- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary.
-- @tab t a table.
-- @return the map, or nil in case of error
-- @return the error message
function OrderedMap:update (t)
assert_arg(1,t,'table')
if OrderedMap:class_of(t) then
for k,v in t:iter() do
self:set(k,v)
end
elseif #t > 0 then -- an array must contain {key=val} tables
if type(t[1]) == 'table' then
for _,pair in ipairs(t) do
local key,value = next(pair)
if not key then return raise 'empty pair initialization table' end
self:set(key,value)
end
else
return raise 'cannot use an array to initialize an OrderedMap'
end
else
for k,v in pairs(t) do
self:set(k,v)
end
end
return self
end
--- set the key's value. This key will be appended at the end of the map.
--
-- If the value is nil, then the key is removed.
-- @param key the key
-- @param val the value
-- @return the map
function OrderedMap:set (key,val)
if rawget(self, key) == nil and val ~= nil then -- new key
self._keys:append(key) -- we keep in order
rawset(self,key,val) -- don't want to provoke __newindex!
else -- existing key-value pair
if val == nil then
self._keys:remove_value(key)
rawset(self,key,nil)
else
self[key] = val
end
end
return self
end
OrderedMap.__newindex = OrderedMap.set
--- insert a key/value pair before a given position.
-- Note: if the map already contains the key, then this effectively
-- moves the item to the new position by first removing at the old position.
-- Has no effect if the key does not exist and val is nil
-- @int pos a position starting at 1
-- @param key the key
-- @param val the value; if nil use the old value
function OrderedMap:insert (pos,key,val)
local oldval = self[key]
val = val or oldval
if oldval then
self._keys:remove_value(key)
end
if val then
self._keys:insert(pos,key)
rawset(self,key,val)
end
return self
end
--- return the keys in order.
-- (Not a copy!)
-- @return List
function OrderedMap:keys ()
return self._keys
end
--- return the values in order.
-- this is relatively expensive.
-- @return List
function OrderedMap:values ()
return List(index_by(self,self._keys))
end
--- sort the keys.
-- @func cmp a comparison function as for @{table.sort}
-- @return the map
function OrderedMap:sort (cmp)
tsort(self._keys,cmp)
return self
end
--- iterate over key-value pairs in order.
function OrderedMap:iter ()
local i = 0
local keys = self._keys
local idx
return function()
i = i + 1
if i > #keys then return nil end
idx = keys[i]
return idx,self[idx]
end
end
--- iterate over an ordered map (5.2).
-- @within metamethods
-- @function OrderedMap:__pairs
OrderedMap.__pairs = OrderedMap.iter
--- string representation of an ordered map.
-- @within metamethods
function OrderedMap:__tostring ()
local res = {}
for i,v in ipairs(self._keys) do
local val = self[v]
local vs = tostring(val)
if type(val) ~= 'number' then
vs = '"'..vs..'"'
end
res[i] = tostring(v)..'='..vs
end
return '{'..concat(res,',')..'}'
end
return OrderedMap

View file

@ -0,0 +1,222 @@
--- A Set class.
--
-- > Set = require 'pl.Set'
-- > = Set{'one','two'} == Set{'two','one'}
-- true
-- > fruit = Set{'apple','banana','orange'}
-- > = fruit['banana']
-- true
-- > = fruit['hazelnut']
-- nil
-- > colours = Set{'red','orange','green','blue'}
-- > = fruit,colours
-- [apple,orange,banana] [blue,green,orange,red]
-- > = fruit+colours
-- [blue,green,apple,red,orange,banana]
-- [orange]
-- > more_fruits = fruit + 'apricot'
-- > = fruit*colours
-- > = more_fruits, fruit
-- [banana,apricot,apple,orange] [banana,apple,orange]
--
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.Map`, (`pl.List` if __tostring is used)
-- @module pl.Set
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local array_tostring, concat = utils.array_tostring, table.concat
local merge,difference = tablex.merge,tablex.difference
local Map = require 'pl.Map'
local class = require 'pl.class'
local stdmt = utils.stdmt
local Set = stdmt.Set
-- the Set class --------------------
class(Map,nil,Set)
-- note: Set has _no_ methods!
Set.__index = nil
local function makeset (t)
return setmetatable(t,Set)
end
--- create a set. <br>
-- @param t may be a Set, Map or list-like table.
-- @class function
-- @name Set
function Set:_init (t)
t = t or {}
local mt = getmetatable(t)
if mt == Set or mt == Map then
for k in pairs(t) do self[k] = true end
else
for _,v in ipairs(t) do self[v] = true end
end
end
--- string representation of a set.
-- @within metamethods
function Set:__tostring ()
return '['..concat(array_tostring(Set.values(self)),',')..']'
end
--- get a list of the values in a set.
-- @param self a Set
-- @function Set.values
-- @return a list
Set.values = Map.keys
--- map a function over the values of a set.
-- @param self a Set
-- @param fn a function
-- @param ... extra arguments to pass to the function.
-- @return a new set
function Set.map (self,fn,...)
fn = utils.function_arg(1,fn)
local res = {}
for k in pairs(self) do
res[fn(k,...)] = true
end
return makeset(res)
end
--- union of two sets (also +).
-- @param self a Set
-- @param set another set
-- @return a new set
function Set.union (self,set)
return merge(self,set,true)
end
--- modifies '+' operator to allow addition of non-Set elements
--- Preserves +/- semantics - does not modify first argument.
local function setadd(self,other)
local mt = getmetatable(other)
if mt == Set or mt == Map then
return Set.union(self,other)
else
local new = Set(self)
new[other] = true
return new
end
end
--- union of sets.
-- @within metamethods
-- @function Set.__add
Set.__add = setadd
--- intersection of two sets (also *).
-- @param self a Set
-- @param set another set
-- @return a new set
-- @usage
-- > s = Set{10,20,30}
-- > t = Set{20,30,40}
-- > = t
-- [20,30,40]
-- > = Set.intersection(s,t)
-- [30,20]
-- > = s*t
-- [30,20]
function Set.intersection (self,set)
return merge(self,set,false)
end
--- intersection of sets.
-- @within metamethods
-- @function Set.__mul
Set.__mul = Set.intersection
--- new set with elements in the set that are not in the other (also -).
-- @param self a Set
-- @param set another set
-- @return a new set
function Set.difference (self,set)
return difference(self,set,false)
end
--- modifies "-" operator to remove non-Set values from set.
--- Preserves +/- semantics - does not modify first argument.
local function setminus (self,other)
local mt = getmetatable(other)
if mt == Set or mt == Map then
return Set.difference(self,other)
else
local new = Set(self)
new[other] = nil
return new
end
end
--- difference of sets.
-- @within metamethods
-- @function Set.__sub
Set.__sub = setminus
-- a new set with elements in _either_ the set _or_ other but not both (also ^).
-- @param self a Set
-- @param set another set
-- @return a new set
function Set.symmetric_difference (self,set)
return difference(self,set,true)
end
--- symmetric difference of sets.
-- @within metamethods
-- @function Set.__pow
Set.__pow = Set.symmetric_difference
--- is the first set a subset of the second (also <)?.
-- @param self a Set
-- @param set another set
-- @return true or false
function Set.issubset (self,set)
for k in pairs(self) do
if not set[k] then return false end
end
return true
end
--- first set subset of second?
-- @within metamethods
-- @function Set.__lt
Set.__lt = Set.issubset
--- is the set empty?.
-- @param self a Set
-- @return true or false
function Set.isempty (self)
return next(self) == nil
end
--- are the sets disjoint? (no elements in common).
-- Uses naive definition, i.e. that intersection is empty
-- @param s1 a Set
-- @param s2 another set
-- @return true or false
function Set.isdisjoint (s1,s2)
return Set.isempty(Set.intersection(s1,s2))
end
--- size of this set (also # for 5.2).
-- @param s a Set
-- @return size
-- @function Set.len
Set.len = tablex.size
--- cardinality of set (5.2).
-- @within metamethods
-- @function Set.__len
Set.__len = Set.len
--- equality between sets.
-- @within metamethods
function Set.__eq (s1,s2)
return Set.issubset(s1,s2) and Set.issubset(s2,s1)
end
return Set

View file

@ -0,0 +1,159 @@
--- Application support functions.
-- See @{01-introduction.md.Application_Support|the Guide}
--
-- Dependencies: `pl.utils`, `pl.path`
-- @module pl.app
local io,package,require = _G.io, _G.package, _G.require
local utils = require 'pl.utils'
local path = require 'pl.path'
local app = {}
local function check_script_name ()
if _G.arg == nil then error('no command line args available\nWas this run from a main script?') end
return _G.arg[0]
end
--- add the current script's path to the Lua module path.
-- Applies to both the source and the binary module paths. It makes it easy for
-- the main file of a multi-file program to access its modules in the same directory.
-- `base` allows these modules to be put in a specified subdirectory, to allow for
-- cleaner deployment and resolve potential conflicts between a script name and its
-- library directory.
-- @string base optional base directory.
-- @treturn string the current script's path with a trailing slash
function app.require_here (base)
local p = path.dirname(check_script_name())
if not path.isabs(p) then
p = path.join(path.currentdir(),p)
end
if p:sub(-1,-1) ~= path.sep then
p = p..path.sep
end
if base then
p = p..base..path.sep
end
local so_ext = path.is_windows and 'dll' or 'so'
local lsep = package.path:find '^;' and '' or ';'
local csep = package.cpath:find '^;' and '' or ';'
package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
return p
end
--- return a suitable path for files private to this application.
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
-- SNAME is the name of the script without .lua extension.
-- @string file a filename (w/out path)
-- @return a full pathname, or nil
-- @return 'cannot create' error
function app.appfile (file)
local sname = path.basename(check_script_name())
local name = path.splitext(sname)
local dir = path.join(path.expanduser('~'),'.'..name)
if not path.isdir(dir) then
local ret = path.mkdir(dir)
if not ret then return utils.raise ('cannot create '..dir) end
end
return path.join(dir,file)
end
--- return string indicating operating system.
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
function app.platform()
if path.is_windows then
return 'Windows'
else
local f = io.popen('uname')
local res = f:read()
if res == 'Darwin' then res = 'OSX' end
f:close()
return res
end
end
--- return the full command-line used to invoke this script.
-- Any extra flags occupy slots, so that `lua -lpl` gives us `{[-2]='lua',[-1]='-lpl'}`
-- @return command-line
-- @return name of Lua program used
function app.lua ()
local args = _G.arg or error "not in a main program"
local imin = 0
for i in pairs(args) do
if i < imin then imin = i end
end
local cmd, append = {}, table.insert
for i = imin,-1 do
append(cmd, utils.quote_arg(args[i]))
end
return table.concat(cmd,' '),args[imin]
end
--- parse command-line arguments into flags and parameters.
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`);
-- note that a number value can be given without a space.
-- Multiple short args can be combined like so: ( `-abcd`).
-- @tparam {string} args an array of strings (default is the global `arg`)
-- @tab flags_with_values any flags that take values, e.g. `{out=true}`
-- @return a table of flags (flag=value pairs)
-- @return an array of parameters
-- @raise if args is nil, then the global `args` must be available!
function app.parse_args (args,flags_with_values)
if not args then
args = _G.arg
if not args then error "Not in a main program: 'arg' not found" end
end
flags_with_values = flags_with_values or {}
local _args = {}
local flags = {}
local i = 1
while i <= #args do
local a = args[i]
local v = a:match('^-(.+)')
local is_long
if v then -- we have a flag
if v:find '^-' then
is_long = true
v = v:sub(2)
end
if flags_with_values[v] then
if i == #args or args[i+1]:find '^-' then
return utils.raise ("no value for '"..v.."'")
end
flags[v] = args[i+1]
i = i + 1
else
-- a value can also be indicated with = or :
local var,val = utils.splitv (v,'[=:]')
var = var or v
val = val or true
if not is_long then
if #var > 1 then
if var:find '.%d+' then -- short flag, number value
val = var:sub(2)
var = var:sub(1,1)
else -- multiple short flags
for i = 1,#var do
flags[var:sub(i,i)] = true
end
val = nil -- prevents use of var as a flag below
end
else -- single short flag (can have value, defaults to true)
val = val or true
end
end
if val then
flags[var] = val
end
end
else
_args[#_args+1] = a
end
i = i + 1
end
return flags,_args
end
return app

View file

@ -0,0 +1,493 @@
--- Operations on two-dimensional arrays.
-- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide}
--
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.types`
-- @module pl.array2d
local tonumber,assert,tostring,io,ipairs,string,table =
_G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
local setmetatable,getmetatable = setmetatable,getmetatable
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local types = require 'pl.types'
local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by
local remove = table.remove
local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg
local byte = string.byte
local stdout = io.stdout
local min = math.min
local array2d = {}
local function obj (int,out)
local mt = getmetatable(int)
if mt then
setmetatable(out,mt)
end
return out
end
local function makelist (res)
return setmetatable(res, require('pl.List'))
end
local function index (t,k)
return t[k]
end
--- return the row and column size.
-- @array2d t a 2d array
-- @treturn int number of rows
-- @treturn int number of cols
function array2d.size (t)
assert_arg(1,t,'table')
return #t,#t[1]
end
--- extract a column from the 2D array.
-- @array2d a 2d array
-- @param key an index or key
-- @return 1d array
function array2d.column (a,key)
assert_arg(1,a,'table')
return makelist(imap(index,a,key))
end
local column = array2d.column
--- map a function over a 2D array
-- @func f a function of at least one argument
-- @array2d a 2d array
-- @param arg an optional extra argument to be passed to the function.
-- @return 2d array
function array2d.map (f,a,arg)
assert_arg(1,a,'table')
f = utils.function_arg(1,f)
return obj(a,imap(function(row) return imap(f,row,arg) end, a))
end
--- reduce the rows using a function.
-- @func f a binary function
-- @array2d a 2d array
-- @return 1d array
-- @see pl.tablex.reduce
function array2d.reduce_rows (f,a)
assert_arg(1,a,'table')
return tmap(function(row) return reduce(f,row) end, a)
end
--- reduce the columns using a function.
-- @func f a binary function
-- @array2d a 2d array
-- @return 1d array
-- @see pl.tablex.reduce
function array2d.reduce_cols (f,a)
assert_arg(1,a,'table')
return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1]))
end
--- reduce a 2D array into a scalar, using two operations.
-- @func opc operation to reduce the final result
-- @func opr operation to reduce the rows
-- @param a 2D array
function array2d.reduce2 (opc,opr,a)
assert_arg(3,a,'table')
local tmp = array2d.reduce_rows(opr,a)
return reduce(opc,tmp)
end
--- map a function over two arrays.
-- They can be both or either 2D arrays
-- @func f function of at least two arguments
-- @int ad order of first array (1 or 2)
-- @int bd order of second array (1 or 2)
-- @tab a 1d or 2d array
-- @tab b 1d or 2d array
-- @param arg optional extra argument to pass to function
-- @return 2D array, unless both arrays are 1D
function array2d.map2 (f,ad,bd,a,b,arg)
assert_arg(1,a,'table')
assert_arg(2,b,'table')
f = utils.function_arg(1,f)
if ad == 1 and bd == 2 then
return imap(function(row)
return tmap2(f,a,row,arg)
end, b)
elseif ad == 2 and bd == 1 then
return imap(function(row)
return tmap2(f,row,b,arg)
end, a)
elseif ad == 1 and bd == 1 then
return tmap2(f,a,b)
elseif ad == 2 and bd == 2 then
return tmap2(function(rowa,rowb)
return tmap2(f,rowa,rowb,arg)
end, a,b)
end
end
--- cartesian product of two 1d arrays.
-- @func f a function of 2 arguments
-- @array t1 a 1d table
-- @array t2 a 1d table
-- @return 2d table
-- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}}
function array2d.product (f,t1,t2)
f = utils.function_arg(1,f)
assert_arg(2,t1,'table')
assert_arg(3,t2,'table')
local res, map = {}, tablex.map
for i,v in ipairs(t2) do
res[i] = map(f,t1,v)
end
return res
end
--- flatten a 2D array.
-- (this goes over columns first.)
-- @array2d t 2d table
-- @return a 1d table
-- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6}
function array2d.flatten (t)
local res = {}
local k = 1
for _,a in ipairs(t) do -- for all rows
for i = 1,#a do
res[k] = a[i]
k = k + 1
end
end
return makelist(res)
end
--- reshape a 2D array.
-- @array2d t 2d array
-- @int nrows new number of rows
-- @bool co column-order (Fortran-style) (default false)
-- @return a new 2d array
function array2d.reshape (t,nrows,co)
local nr,nc = array2d.size(t)
local ncols = nr*nc / nrows
local res = {}
local ir,ic = 1,1
for i = 1,nrows do
local row = {}
for j = 1,ncols do
row[j] = t[ir][ic]
if not co then
ic = ic + 1
if ic > nc then
ir = ir + 1
ic = 1
end
else
ir = ir + 1
if ir > nr then
ic = ic + 1
ir = 1
end
end
end
res[i] = row
end
return obj(t,res)
end
--- swap two rows of an array.
-- @array2d t a 2d array
-- @int i1 a row index
-- @int i2 a row index
function array2d.swap_rows (t,i1,i2)
assert_arg(1,t,'table')
t[i1],t[i2] = t[i2],t[i1]
end
--- swap two columns of an array.
-- @array2d t a 2d array
-- @int j1 a column index
-- @int j2 a column index
function array2d.swap_cols (t,j1,j2)
assert_arg(1,t,'table')
for i = 1,#t do
local row = t[i]
row[j1],row[j2] = row[j2],row[j1]
end
end
--- extract the specified rows.
-- @array2d t 2d array
-- @tparam {int} ridx a table of row indices
function array2d.extract_rows (t,ridx)
return obj(t,index_by(t,ridx))
end
--- extract the specified columns.
-- @array2d t 2d array
-- @tparam {int} cidx a table of column indices
function array2d.extract_cols (t,cidx)
assert_arg(1,t,'table')
local res = {}
for i = 1,#t do
res[i] = index_by(t[i],cidx)
end
return obj(t,res)
end
--- remove a row from an array.
-- @function array2d.remove_row
-- @array2d t a 2d array
-- @int i a row index
array2d.remove_row = remove
--- remove a column from an array.
-- @array2d t a 2d array
-- @int j a column index
function array2d.remove_col (t,j)
assert_arg(1,t,'table')
for i = 1,#t do
remove(t[i],j)
end
end
local function _parse (s)
local c,r
if s:sub(1,1) == 'R' then
r,c = s:match 'R(%d+)C(%d+)'
r,c = tonumber(r),tonumber(c)
else
c,r = s:match '(.)(.)'
c = byte(c) - byte 'A' + 1
r = tonumber(r)
end
assert(c ~= nil and r ~= nil,'bad cell specifier: '..s)
return r,c
end
--- parse a spreadsheet range.
-- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
-- a special case is a single element (e.g 'A1' or 'R1C1')
-- @string s a range.
-- @treturn int start col
-- @treturn int start row
-- @treturn int end col
-- @treturn int end row
function array2d.parse_range (s)
if s:find ':' then
local start,finish = splitv(s,':')
local i1,j1 = _parse(start)
local i2,j2 = _parse(finish)
return i1,j1,i2,j2
else -- single value
local i,j = _parse(s)
return i,j
end
end
--- get a slice of a 2D array using spreadsheet range notation. @see parse_range
-- @array2d t a 2D array
-- @string rstr range expression
-- @return a slice
-- @see array2d.parse_range
-- @see array2d.slice
function array2d.range (t,rstr)
assert_arg(1,t,'table')
local i1,j1,i2,j2 = array2d.parse_range(rstr)
if i2 then
return array2d.slice(t,i1,j1,i2,j2)
else -- single value
return t[i1][j1]
end
end
local function default_range (t,i1,j1,i2,j2)
local nr, nc = array2d.size(t)
i1,j1 = i1 or 1, j1 or 1
i2,j2 = i2 or nr, j2 or nc
if i2 < 0 then i2 = nr + i2 + 1 end
if j2 < 0 then j2 = nc + j2 + 1 end
return i1,j1,i2,j2
end
--- get a slice of a 2D array. Note that if the specified range has
-- a 1D result, the rank of the result will be 1.
-- @array2d t a 2D array
-- @int i1 start row (default 1)
-- @int j1 start col (default 1)
-- @int i2 end row (default N)
-- @int j2 end col (default M)
-- @return an array, 2D in general but 1D in special cases.
function array2d.slice (t,i1,j1,i2,j2)
assert_arg(1,t,'table')
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
local res = {}
for i = i1,i2 do
local val
local row = t[i]
if j1 == j2 then
val = row[j1]
else
val = {}
for j = j1,j2 do
val[#val+1] = row[j]
end
end
res[#res+1] = val
end
if i1 == i2 then res = res[1] end
return obj(t,res)
end
--- set a specified range of an array to a value.
-- @array2d t a 2D array
-- @param value the value (may be a function)
-- @int i1 start row (default 1)
-- @int j1 start col (default 1)
-- @int i2 end row (default N)
-- @int j2 end col (default M)
-- @see tablex.set
function array2d.set (t,value,i1,j1,i2,j2)
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
for i = i1,i2 do
tset(t[i],value,j1,j2)
end
end
--- write a 2D array to a file.
-- @array2d t a 2D array
-- @param f a file object (default stdout)
-- @string fmt a format string (default is just to use tostring)
-- @int i1 start row (default 1)
-- @int j1 start col (default 1)
-- @int i2 end row (default N)
-- @int j2 end col (default M)
function array2d.write (t,f,fmt,i1,j1,i2,j2)
assert_arg(1,t,'table')
f = f or stdout
local rowop
if fmt then
rowop = function(row,j) fprintf(f,fmt,row[j]) end
else
rowop = function(row,j) f:write(tostring(row[j]),' ') end
end
local function newline()
f:write '\n'
end
array2d.forall(t,rowop,newline,i1,j1,i2,j2)
end
--- perform an operation for all values in a 2D array.
-- @array2d t 2D array
-- @func row_op function to call on each value
-- @func end_row_op function to call at end of each row
-- @int i1 start row (default 1)
-- @int j1 start col (default 1)
-- @int i2 end row (default N)
-- @int j2 end col (default M)
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
assert_arg(1,t,'table')
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
for i = i1,i2 do
local row = t[i]
for j = j1,j2 do
row_op(row,j)
end
if end_row_op then end_row_op(i) end
end
end
---- move a block from the destination to the source.
-- @array2d dest a 2D array
-- @int di start row in dest
-- @int dj start col in dest
-- @array2d src a 2D array
-- @int i1 start row (default 1)
-- @int j1 start col (default 1)
-- @int i2 end row (default N)
-- @int j2 end col (default M)
function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
assert_arg(1,dest,'table')
assert_arg(4,src,'table')
i1,j1,i2,j2 = default_range(src,i1,j1,i2,j2)
local nr,nc = array2d.size(dest)
i2, j2 = min(nr,i2), min(nc,j2)
--i1, j1 = max(1,i1), max(1,j1)
dj = dj - 1
for i = i1,i2 do
local drow, srow = dest[i+di-1], src[i]
for j = j1,j2 do
drow[j+dj] = srow[j]
end
end
end
--- iterate over all elements in a 2D array, with optional indices.
-- @array2d a 2D array
-- @tparam {int} indices with indices (default false)
-- @int i1 start row (default 1)
-- @int j1 start col (default 1)
-- @int i2 end row (default N)
-- @int j2 end col (default M)
-- @return either value or i,j,value depending on indices
function array2d.iter (a,indices,i1,j1,i2,j2)
assert_arg(1,a,'table')
local norowset = not (i2 and j2)
i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2)
local i,j = i1-1,j1-1
local row,nr = nil,0
local onr = j2 - j1 + 1
return function()
j = j + 1
if j > nr then
j = j1
i = i + 1
if i > i2 then return nil end
row = a[i]
nr = norowset and #row or onr
end
if indices then
return i,j,row[j]
else
return row[j]
end
end
end
--- iterate over all columns.
-- @array2d a a 2D array
-- @return each column in turn
function array2d.columns (a)
assert_arg(1,a,'table')
local n = a[1][1]
local i = 0
return function()
i = i + 1
if i > n then return nil end
return column(a,i)
end
end
--- new array of specified dimensions
-- @int rows number of rows
-- @int cols number of cols
-- @param val initial value; if it's a function then use `val(i,j)`
-- @return new 2d array
function array2d.new(rows,cols,val)
local res = {}
local fun = types.is_callable(val)
for i = 1,rows do
local row = {}
if fun then
for j = 1,cols do row[j] = val(i,j) end
else
for j = 1,cols do row[j] = val end
end
res[i] = row
end
return res
end
return array2d

View file

@ -0,0 +1,261 @@
--- Provides a reuseable and convenient framework for creating classes in Lua.
-- Two possible notations:
--
-- B = class(A)
-- class.B(A)
--
-- The latter form creates a named class within the current environment. Note
-- that this implicitly brings in `pl.utils` as a dependency.
--
-- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion}
-- @module pl.class
local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type =
_G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type
local compat
-- this trickery is necessary to prevent the inheritance of 'super' and
-- the resulting recursive call problems.
local function call_ctor (c,obj,...)
-- nice alias for the base class ctor
local base = rawget(c,'_base')
if base then
local parent_ctor = rawget(base,'_init')
while not parent_ctor do
base = rawget(base,'_base')
if not base then break end
parent_ctor = rawget(base,'_init')
end
if parent_ctor then
rawset(obj,'super',function(obj,...)
call_ctor(base,obj,...)
end)
end
end
local res = c._init(obj,...)
rawset(obj,'super',nil)
return res
end
--- initializes an __instance__ upon creation.
-- @function class:_init
-- @param ... parameters passed to the constructor
-- @usage local Cat = class()
-- function Cat:_init(name)
-- --self:super(name) -- call the ancestor initializer if needed
-- self.name = name
-- end
--
-- local pussycat = Cat("pussycat")
-- print(pussycat.name) --> pussycat
--- checks whether an __instance__ is derived from some class.
-- Works the other way around as `class_of`. It has two ways of using;
-- 1) call with a class to check against, 2) call without params.
-- @function instance:is_a
-- @param some_class class to check against, or `nil` to return the class
-- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then
-- it returns the class table of the instance
-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
-- if pussycat:is_a(Cat) then
-- -- it's true, it is a Lion, but also a Cat
-- end
--
-- if pussycat:is_a() == Lion then
-- -- It's true
-- end
local function is_a(self,klass)
if klass == nil then
-- no class provided, so return the class this instance is derived from
return getmetatable(self)
end
local m = getmetatable(self)
if not m then return false end --*can't be an object!
while m do
if m == klass then return true end
m = rawget(m,'_base')
end
return false
end
--- checks whether an __instance__ is derived from some class.
-- Works the other way around as `is_a`.
-- @function some_class:class_of
-- @param some_instance instance to check against
-- @return `true` if `some_instance` is derived from `some_class`
-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
-- if Cat:class_of(pussycat) then
-- -- it's true
-- end
local function class_of(klass,obj)
if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end
return klass.is_a(obj,klass)
end
--- cast an object to another class.
-- It is not clever (or safe!) so use carefully.
-- @param some_instance the object to be changed
-- @function some_class:cast
local function cast (klass, obj)
return setmetatable(obj,klass)
end
local function _class_tostring (obj)
local mt = obj._class
local name = rawget(mt,'_name')
setmetatable(obj,nil)
local str = tostring(obj)
setmetatable(obj,mt)
if name then str = name ..str:gsub('table','') end
return str
end
local function tupdate(td,ts,dont_override)
for k,v in pairs(ts) do
if not dont_override or td[k] == nil then
td[k] = v
end
end
end
local function _class(base,c_arg,c)
-- the class `c` will be the metatable for all its objects,
-- and they will look up their methods in it.
local mt = {} -- a metatable for the class to support __call and _handler
-- can define class by passing it a plain table of methods
local plain = type(base) == 'table' and not getmetatable(base)
if plain then
c = base
base = c._base
else
c = c or {}
end
if type(base) == 'table' then
-- our new class is a shallow copy of the base class!
-- but be careful not to wipe out any methods we have been given at this point!
tupdate(c,base,plain)
c._base = base
-- inherit the 'not found' handler, if present
if rawget(c,'_handler') then mt.__index = c._handler end
elseif base ~= nil then
error("must derive from a table type",3)
end
c.__index = c
setmetatable(c,mt)
if not plain then
c._init = nil
end
if base and rawget(base,'_class_init') then
base._class_init(c,c_arg)
end
-- expose a ctor which can be called by <classname>(<args>)
mt.__call = function(class_tbl,...)
local obj
if rawget(c,'_create') then obj = c._create(...) end
if not obj then obj = {} end
setmetatable(obj,c)
if rawget(c,'_init') then -- explicit constructor
local res = call_ctor(c,obj,...)
if res then -- _if_ a ctor returns a value, it becomes the object...
obj = res
setmetatable(obj,c)
end
elseif base and rawget(base,'_init') then -- default constructor
-- make sure that any stuff from the base class is initialized!
call_ctor(base,obj,...)
end
if base and rawget(base,'_post_init') then
base._post_init(obj)
end
return obj
end
-- Call Class.catch to set a handler for methods/properties not found in the class!
c.catch = function(self, handler)
if type(self) == "function" then
-- called using . instead of :
handler = self
end
c._handler = handler
mt.__index = handler
end
c.is_a = is_a
c.class_of = class_of
c.cast = cast
c._class = c
if not rawget(c,'__tostring') then
c.__tostring = _class_tostring
end
return c
end
--- create a new class, derived from a given base class.
-- Supporting two class creation syntaxes:
-- either `Name = class(base)` or `class.Name(base)`.
-- The first form returns the class directly and does not set its `_name`.
-- The second form creates a variable `Name` in the current environment set
-- to the class, and also sets `_name`.
-- @function class
-- @param base optional base class
-- @param c_arg optional parameter to class constructor
-- @param c optional table to be used as class
local class
class = setmetatable({},{
__call = function(fun,...)
return _class(...)
end,
__index = function(tbl,key)
if key == 'class' then
io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n')
return class
end
compat = compat or require 'pl.compat'
local env = compat.getfenv(2)
return function(...)
local c = _class(...)
c._name = key
rawset(env,key,c)
return c
end
end
})
class.properties = class()
function class.properties._class_init(klass)
klass.__index = function(t,key)
-- normal class lookup!
local v = klass[key]
if v then return v end
-- is it a getter?
v = rawget(klass,'get_'..key)
if v then
return v(t)
end
-- is it a field?
return rawget(t,'_'..key)
end
klass.__newindex = function (t,key,value)
-- if there's a setter, use that, otherwise directly set table
local p = 'set_'..key
local setter = klass[p]
if setter then
setter(t,value)
else
rawset(t,key,value)
end
end
end
return class

View file

@ -0,0 +1,165 @@
----------------
--- Lua 5.1/5.2/5.3 compatibility.
-- Ensures that `table.pack` and `package.searchpath` are available
-- for Lua 5.1 and LuaJIT.
-- The exported function `load` is Lua 5.2 compatible.
-- `compat.setfenv` and `compat.getfenv` are available for Lua 5.2, although
-- they are not always guaranteed to work.
-- @module pl.compat
local compat = {}
compat.lua51 = _VERSION == 'Lua 5.1'
local isJit = (tostring(assert):match('builtin') ~= nil)
if isJit then
-- 'goto' is a keyword when 52 compatibility is enabled in LuaJit
compat.jit52 = not loadstring("local goto = 1")
end
compat.dir_separator = _G.package.config:sub(1,1)
compat.is_windows = compat.dir_separator == '\\'
--- execute a shell command.
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
-- @param cmd a shell command
-- @return true if successful
-- @return actual return code
function compat.execute (cmd)
local res1,_,res3 = os.execute(cmd)
if compat.lua51 and not compat.jit52 then
if compat.is_windows then
res1 = res1 > 255 and res1 % 256 or res1
return res1==0,res1
else
res1 = res1 > 255 and res1 / 256 or res1
return res1==0,res1
end
else
if compat.is_windows then
res3 = res3 > 255 and res3 % 256 or res3
return res3==0,res3
else
return not not res1,res3
end
end
end
----------------
-- Load Lua code as a text or binary chunk.
-- @param ld code string or loader
-- @param[opt] source name of chunk for errors
-- @param[opt] mode 'b', 't' or 'bt'
-- @param[opt] env environment to load the chunk in
-- @function compat.load
---------------
-- Get environment of a function.
-- With Lua 5.2, may return nil for a function with no global references!
-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html)
-- @param f a function or a call stack reference
-- @function compat.getfenv
---------------
-- Set environment of a function
-- @param f a function or a call stack reference
-- @param env a table that becomes the new environment of `f`
-- @function compat.setfenv
if compat.lua51 then -- define Lua 5.2 style load()
if not isJit then -- but LuaJIT's load _is_ compatible
local lua51_load = load
function compat.load(str,src,mode,env)
local chunk,err
if type(str) == 'string' then
if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then
return nil,"attempt to load a binary chunk"
end
chunk,err = loadstring(str,src)
else
chunk,err = lua51_load(str,src)
end
if chunk and env then setfenv(chunk,env) end
return chunk,err
end
else
compat.load = load
end
compat.setfenv, compat.getfenv = setfenv, getfenv
else
compat.load = load
-- setfenv/getfenv replacements for Lua 5.2
-- by Sergey Rozhenko
-- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
-- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
-- in the case of a function with no globals:
-- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
function compat.setfenv(f, t)
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
local name
local up = 0
repeat
up = up + 1
name = debug.getupvalue(f, up)
until name == '_ENV' or name == nil
if name then
debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
debug.setupvalue(f, up, t)
end
if f ~= 0 then return f end
end
function compat.getfenv(f)
local f = f or 0
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
local name, val
local up = 0
repeat
up = up + 1
name, val = debug.getupvalue(f, up)
until name == '_ENV' or name == nil
return val
end
end
--- Lua 5.2 Functions Available for 5.1
-- @section lua52
--- pack an argument list into a table.
-- @param ... any arguments
-- @return a table with field n set to the length
-- @function table.pack
if not table.pack then
function table.pack (...) -- luacheck: ignore
return {n=select('#',...); ...}
end
end
--- unpack a table and return the elements.
-- @param t table to unpack
-- @param[opt] i index from which to start unpacking, defaults to 1
-- @param[opt] t index of the last element to unpack, defaults to #t
-- @return multiple returns values from the table
if not table.unpack then
table.unpack = unpack -- luacheck: ignore
end
------
-- return the full path where a Lua module name would be matched.
-- @param mod module name, possibly dotted
-- @param path a path in the same form as package.path or package.cpath
-- @see path.package_path
-- @function package.searchpath
if not package.searchpath then
local sep = package.config:sub(1,1)
function package.searchpath (mod,path) -- luacheck: ignore
mod = mod:gsub('%.',sep)
for m in path:gmatch('[^;]+') do
local nm = m:gsub('?',mod)
local f = io.open(nm,'r')
if f then f:close(); return nm end
end
end
end
return compat

View file

@ -0,0 +1,285 @@
--- List comprehensions implemented in Lua.
--
-- See the [wiki page](http://lua-users.org/wiki/ListComprehensions)
--
-- local C= require 'pl.comprehension' . new()
--
-- C ('x for x=1,10') ()
-- ==> {1,2,3,4,5,6,7,8,9,10}
-- C 'x^2 for x=1,4' ()
-- ==> {1,4,9,16}
-- C '{x,x^2} for x=1,4' ()
-- ==> {{1,1},{2,4},{3,9},{4,16}}
-- C '2*x for x' {1,2,3}
-- ==> {2,4,6}
-- dbl = C '2*x for x'
-- dbl {10,20,30}
-- ==> {20,40,60}
-- C 'x for x if x % 2 == 0' {1,2,3,4,5}
-- ==> {2,4}
-- C '{x,y} for x = 1,2 for y = 1,2' ()
-- ==> {{1,1},{1,2},{2,1},{2,2}}
-- C '{x,y} for x for y' ({1,2},{10,20})
-- ==> {{1,10},{1,20},{2,10},{2,20}}
-- assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2)
--
-- (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license).
--
-- Dependencies: `pl.utils`, `pl.luabalanced`
--
-- See @{07-functional.md.List_Comprehensions|the Guide}
-- @module pl.comprehension
local utils = require 'pl.utils'
local status,lb = pcall(require, "pl.luabalanced")
if not status then
lb = require 'luabalanced'
end
local math_max = math.max
local table_concat = table.concat
-- fold operations
-- http://en.wikipedia.org/wiki/Fold_(higher-order_function)
local ops = {
list = {init=' {} ', accum=' __result[#__result+1] = (%s) '},
table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '},
sum = {init=' 0 ', accum=' __result = __result + (%s) '},
min = {init=' nil ', accum=' local __tmp = %s ' ..
' if __result then if __tmp < __result then ' ..
'__result = __tmp end else __result = __tmp end '},
max = {init=' nil ', accum=' local __tmp = %s ' ..
' if __result then if __tmp > __result then ' ..
'__result = __tmp end else __result = __tmp end '},
}
-- Parses comprehension string expr.
-- Returns output expression list <out> string, array of for types
-- ('=', 'in' or nil) <fortypes>, array of input variable name
-- strings <invarlists>, array of input variable value strings
-- <invallists>, array of predicate expression strings <preds>,
-- operation name string <opname>, and number of placeholder
-- parameters <max_param>.
--
-- The is equivalent to the mathematical set-builder notation:
--
-- <opname> { <out> | <invarlist> in <invallist> , <preds> }
--
-- @usage "x^2 for x" -- array values
-- @usage "x^2 for x=1,10,2" -- numeric for
-- @usage "k^v for k,v in pairs(_1)" -- iterator for
-- @usage "(x+y)^2 for x for y if x > y" -- nested
--
local function parse_comprehension(expr)
local pos = 1
-- extract opname (if exists)
local opname
local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos)
local pose = #expr + 1
if tok then
local tok2, posb = lb.match_bracketed(expr, post-1)
assert(tok2, 'syntax error')
if expr:match('^%s*$', posb) then
opname = tok
pose = posb - 1
pos = post
end
end
opname = opname or "list"
-- extract out expression list
local out; out, pos = lb.match_explist(expr, pos)
assert(out, "syntax error: missing expression list")
out = table_concat(out, ', ')
-- extract "for" clauses
local fortypes = {}
local invarlists = {}
local invallists = {}
while 1 do
local post = expr:match('^%s*for%s+()', pos)
if not post then break end
pos = post
-- extract input vars
local iv; iv, pos = lb.match_namelist(expr, pos)
assert(#iv > 0, 'syntax error: zero variables')
for _,ident in ipairs(iv) do
assert(not ident:match'^__',
"identifier " .. ident .. " may not contain __ prefix")
end
invarlists[#invarlists+1] = iv
-- extract '=' or 'in' (optional)
local fortype, post = expr:match('^(=)%s*()', pos)
if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end
if fortype then
pos = post
-- extract input value range
local il; il, pos = lb.match_explist(expr, pos)
assert(#il > 0, 'syntax error: zero expressions')
assert(fortype ~= '=' or #il == 2 or #il == 3,
'syntax error: numeric for requires 2 or three expressions')
fortypes[#invarlists] = fortype
invallists[#invarlists] = il
else
fortypes[#invarlists] = false
invallists[#invarlists] = false
end
end
assert(#invarlists > 0, 'syntax error: missing "for" clause')
-- extract "if" clauses
local preds = {}
while 1 do
local post = expr:match('^%s*if%s+()', pos)
if not post then break end
pos = post
local pred; pred, pos = lb.match_expression(expr, pos)
assert(pred, 'syntax error: predicated expression not found')
preds[#preds+1] = pred
end
-- extract number of parameter variables (name matching "_%d+")
local stmp = ''; lb.gsub(expr, function(u, sin) -- strip comments/strings
if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end
end)
local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s)
local s = s:match('^_(%d+)$')
if s then max_param = math_max(max_param, tonumber(s)) end
end)
if pos ~= pose then
assert(false, "syntax error: unrecognized " .. expr:sub(pos))
end
--DEBUG:
--print('----\n', string.format("%q", expr), string.format("%q", out), opname)
--for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end
--for k,v in ipairs(preds) do print(k,v) end
return out, fortypes, invarlists, invallists, preds, opname, max_param
end
-- Create Lua code string representing comprehension.
-- Arguments are in the form returned by parse_comprehension.
local function code_comprehension(
out, fortypes, invarlists, invallists, preds, opname, max_param
)
local op = assert(ops[opname])
local code = op.accum:gsub('%%s', out)
for i=#preds,1,-1 do local pred = preds[i]
code = ' if ' .. pred .. ' then ' .. code .. ' end '
end
for i=#invarlists,1,-1 do
if not fortypes[i] then
local arrayname = '__in' .. i
local idx = '__idx' .. i
code =
' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' ..
' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' ..
code .. ' end '
else
code =
' for ' ..
table_concat(invarlists[i], ', ') ..
' ' .. fortypes[i] .. ' ' ..
table_concat(invallists[i], ', ') ..
' do ' .. code .. ' end '
end
end
code = ' local __result = ( ' .. op.init .. ' ) ' .. code
return code
end
-- Convert code string represented by code_comprehension
-- into Lua function. Also must pass ninputs = #invarlists,
-- max_param, and invallists (from parse_comprehension).
-- Uses environment env.
local function wrap_comprehension(code, ninputs, max_param, invallists, env)
assert(ninputs > 0)
local ts = {}
for i=1,max_param do
ts[#ts+1] = '_' .. i
end
for i=1,ninputs do
if not invallists[i] then
local name = '__in' .. i
ts[#ts+1] = name
end
end
if #ts > 0 then
code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code
end
code = code .. ' return __result '
--print('DEBUG:', code)
local f, err = utils.load(code,'tmp','t',env)
if not f then assert(false, err .. ' with generated code ' .. code) end
return f
end
-- Build Lua function from comprehension string.
-- Uses environment env.
local function build_comprehension(expr, env)
local out, fortypes, invarlists, invallists, preds, opname, max_param
= parse_comprehension(expr)
local code = code_comprehension(
out, fortypes, invarlists, invallists, preds, opname, max_param)
local f = wrap_comprehension(code, #invarlists, max_param, invallists, env)
return f
end
-- Creates new comprehension cache.
-- Any list comprehension function created are set to the environment
-- env (defaults to caller of new).
local function new(env)
-- Note: using a single global comprehension cache would have had
-- security implications (e.g. retrieving cached functions created
-- in other environments).
-- The cache lookup function could have instead been written to retrieve
-- the caller's environment, lookup up the cache private to that
-- environment, and then looked up the function in that cache.
-- That would avoid the need for this <new> call to
-- explicitly manage caches; however, that might also have an undue
-- performance penalty.
if not env then
env = utils.getfenv(2)
end
local mt = {}
local cache = setmetatable({}, mt)
-- Index operator builds, caches, and returns Lua function
-- corresponding to comprehension expression string.
--
-- Example: f = comprehension['x^2 for x']
--
function mt:__index(expr)
local f = build_comprehension(expr, env)
self[expr] = f -- cache
return f
end
-- Convenience syntax.
-- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x'].
mt.__call = mt.__index
cache.new = new
return cache
end
local comprehension = {}
comprehension.new = new
return comprehension

View file

@ -0,0 +1,207 @@
--- Reads configuration files into a Lua table.
-- Understands INI files, classic Unix config files, and simple
-- delimited columns of values. See @{06-data.md.Reading_Configuration_Files|the Guide}
--
-- # test.config
-- # Read timeout in seconds
-- read.timeout=10
-- # Write timeout in seconds
-- write.timeout=5
-- #acceptable ports
-- ports = 1002,1003,1004
--
-- -- readconfig.lua
-- local config = require 'config'
-- local t = config.read 'test.config'
-- print(pretty.write(t))
--
-- ### output #####
-- {
-- ports = {
-- 1002,
-- 1003,
-- 1004
-- },
-- write_timeout = 5,
-- read_timeout = 10
-- }
--
-- @module pl.config
local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
local function split(s,re)
local res = {}
local t_insert = table.insert
re = '[^'..re..']+'
for k in s:gmatch(re) do t_insert(res,k) end
return res
end
local function strip(s)
return s:gsub('^%s+',''):gsub('%s+$','')
end
local function strip_quotes (s)
return s:gsub("['\"](.*)['\"]",'%1')
end
local config = {}
--- like io.lines(), but allows for lines to be continued with '\'.
-- @param file a file-like object (anything where read() returns the next line) or a filename.
-- Defaults to stardard input.
-- @return an iterator over the lines, or nil
-- @return error 'not a file-like object' or 'file is nil'
function config.lines(file)
local f,openf,err
local line = ''
if type(file) == 'string' then
f,err = io.open(file,'r')
if not f then return nil,err end
openf = true
else
f = file or io.stdin
if not file.read then return nil, 'not a file-like object' end
end
if not f then return nil, 'file is nil' end
return function()
local l = f:read()
while l do
-- only for non-blank lines that don't begin with either ';' or '#'
if l:match '%S' and not l:match '^%s*[;#]' then
-- does the line end with '\'?
local i = l:find '\\%s*$'
if i then -- if so,
line = line..l:sub(1,i-1)
elseif line == '' then
return l
else
l = line..l
line = ''
return l
end
end
l = f:read()
end
if openf then f:close() end
end
end
--- read a configuration file into a table
-- @param file either a file-like object or a string, which must be a filename
-- @tab[opt] cnfg a configuration table that may contain these fields:
--
-- * `smart` try to deduce what kind of config file we have (default false)
-- * `variablilize` make names into valid Lua identifiers (default true)
-- * `convert_numbers` try to convert values into numbers (default true)
-- * `trim_space` ensure that there is no starting or trailing whitespace with values (default true)
-- * `trim_quotes` remove quotes from strings (default false)
-- * `list_delim` delimiter to use when separating columns (default ',')
-- * `keysep` separator between key and value pairs (default '=')
--
-- @return a table containing items, or `nil`
-- @return error message (same as @{config.lines}
function config.read(file,cnfg)
local auto
local iter,err = config.lines(file)
if not iter then return nil,err end
local line = iter()
cnfg = cnfg or {}
if cnfg.smart then
auto = true
if line:match '^[^=]+=' then
cnfg.keysep = '='
elseif line:match '^[^:]+:' then
cnfg.keysep = ':'
cnfg.list_delim = ':'
elseif line:match '^%S+%s+' then
cnfg.keysep = ' '
-- more than two columns assume that it's a space-delimited list
-- cf /etc/fstab with /etc/ssh/ssh_config
if line:match '^%S+%s+%S+%s+%S+' then
cnfg.list_delim = ' '
end
cnfg.variabilize = false
end
end
local function check_cnfg (var,def)
local val = cnfg[var]
if val == nil then return def else return val end
end
local initial_digits = '^[%d%+%-]'
local t = {}
local top_t = t
local variablilize = check_cnfg ('variabilize',true)
local list_delim = check_cnfg('list_delim',',')
local convert_numbers = check_cnfg('convert_numbers',true)
local convert_boolean = check_cnfg('convert_boolean',false)
local trim_space = check_cnfg('trim_space',true)
local trim_quotes = check_cnfg('trim_quotes',false)
local ignore_assign = check_cnfg('ignore_assign',false)
local keysep = check_cnfg('keysep','=')
local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*'
if list_delim == ' ' then list_delim = '%s+' end
local function process_name(key)
if variablilize then
key = key:gsub('[^%w]','_')
end
return key
end
local function process_value(value)
if list_delim and value:find(list_delim) then
value = split(value,list_delim)
for i,v in ipairs(value) do
value[i] = process_value(v)
end
elseif convert_numbers and value:find(initial_digits) then
local val = tonumber(value)
if not val and value:match ' kB$' then
value = value:gsub(' kB','')
val = tonumber(value)
end
if val then value = val end
elseif convert_boolean and value == 'true' then
return true
elseif convert_boolean and value == 'false' then
return false
end
if type(value) == 'string' then
if trim_space then value = strip(value) end
if not trim_quotes and auto and value:match '^"' then
trim_quotes = true
end
if trim_quotes then value = strip_quotes(value) end
end
return value
end
while line do
if line:find('^%[') then -- section!
local section = process_name(line:match('%[([^%]]+)%]'))
t = top_t
t[section] = {}
t = t[section]
else
line = line:gsub('^%s*','')
local i1,i2 = line:find(keypat)
if i1 and not ignore_assign then -- key,value assignment
local key = process_name(line:sub(1,i1-1))
local value = process_value(line:sub(i2+1))
t[key] = value
else -- a plain list of values...
t[#t+1] = process_value(line)
end
end
line = iter()
end
return top_t
end
return config

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,470 @@
--- Listing files in directories and creating/removing directory paths.
--
-- Dependencies: `pl.utils`, `pl.path`
--
-- Soft Dependencies: `alien`, `ffi` (either are used on Windows for copying/moving files)
-- @module pl.dir
local utils = require 'pl.utils'
local path = require 'pl.path'
local is_windows = path.is_windows
local ldir = path.dir
local mkdir = path.mkdir
local rmdir = path.rmdir
local sub = string.sub
local os,pcall,ipairs,pairs,require,setmetatable = os,pcall,ipairs,pairs,require,setmetatable
local remove = os.remove
local append = table.insert
local wrap = coroutine.wrap
local yield = coroutine.yield
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
local dir = {}
local function makelist(l)
return setmetatable(l, require('pl.List'))
end
local function assert_dir (n,val)
assert_arg(n,val,'string',path.isdir,'not a directory',4)
end
local function filemask(mask)
mask = utils.escape(path.normcase(mask))
return '^'..mask:gsub('%%%*','.*'):gsub('%%%?','.')..'$'
end
--- Test whether a file name matches a shell pattern.
-- Both parameters are case-normalized if operating system is
-- case-insensitive.
-- @string filename A file name.
-- @string pattern A shell pattern. The only special characters are
-- `'*'` and `'?'`: `'*'` matches any sequence of characters and
-- `'?'` matches any single character.
-- @treturn bool
-- @raise dir and mask must be strings
function dir.fnmatch(filename,pattern)
assert_string(1,filename)
assert_string(2,pattern)
return path.normcase(filename):find(filemask(pattern)) ~= nil
end
--- Return a list of all file names within an array which match a pattern.
-- @tab filenames An array containing file names.
-- @string pattern A shell pattern.
-- @treturn List(string) List of matching file names.
-- @raise dir and mask must be strings
function dir.filter(filenames,pattern)
assert_arg(1,filenames,'table')
assert_string(2,pattern)
local res = {}
local mask = filemask(pattern)
for i,f in ipairs(filenames) do
if path.normcase(f):find(mask) then append(res,f) end
end
return makelist(res)
end
local function _listfiles(dir,filemode,match)
local res = {}
local check = utils.choose(filemode,path.isfile,path.isdir)
if not dir then dir = '.' end
for f in ldir(dir) do
if f ~= '.' and f ~= '..' then
local p = path.join(dir,f)
if check(p) and (not match or match(f)) then
append(res,p)
end
end
end
return makelist(res)
end
--- return a list of all files in a directory which match the a shell pattern.
-- @string dir A directory. If not given, all files in current directory are returned.
-- @string mask A shell pattern. If not given, all files are returned.
-- @treturn {string} list of files
-- @raise dir and mask must be strings
function dir.getfiles(dir,mask)
assert_dir(1,dir)
if mask then assert_string(2,mask) end
local match
if mask then
mask = filemask(mask)
match = function(f)
return path.normcase(f):find(mask)
end
end
return _listfiles(dir,true,match)
end
--- return a list of all subdirectories of the directory.
-- @string dir A directory
-- @treturn {string} a list of directories
-- @raise dir must be a a valid directory
function dir.getdirectories(dir)
assert_dir(1,dir)
return _listfiles(dir,false)
end
local alien,ffi,ffi_checked,CopyFile,MoveFile,GetLastError,win32_errors,cmd_tmpfile
local function execute_command(cmd,parms)
if not cmd_tmpfile then cmd_tmpfile = path.tmpname () end
local err = path.is_windows and ' > ' or ' 2> '
cmd = cmd..' '..parms..err..utils.quote_arg(cmd_tmpfile)
local ret = utils.execute(cmd)
if not ret then
local err = (utils.readfile(cmd_tmpfile):gsub('\n(.*)',''))
remove(cmd_tmpfile)
return false,err
else
remove(cmd_tmpfile)
return true
end
end
local function find_ffi_copyfile ()
if not ffi_checked then
ffi_checked = true
local res
res,alien = pcall(require,'alien')
if not res then
alien = nil
res, ffi = pcall(require,'ffi')
end
if not res then
ffi = nil
return
end
else
return
end
if alien then
-- register the Win32 CopyFile and MoveFile functions
local kernel = alien.load('kernel32.dll')
CopyFile = kernel.CopyFileA
CopyFile:types{'string','string','int',ret='int',abi='stdcall'}
MoveFile = kernel.MoveFileA
MoveFile:types{'string','string',ret='int',abi='stdcall'}
GetLastError = kernel.GetLastError
GetLastError:types{ret ='int', abi='stdcall'}
elseif ffi then
ffi.cdef [[
int CopyFileA(const char *src, const char *dest, int iovr);
int MoveFileA(const char *src, const char *dest);
int GetLastError();
]]
CopyFile = ffi.C.CopyFileA
MoveFile = ffi.C.MoveFileA
GetLastError = ffi.C.GetLastError
end
win32_errors = {
ERROR_FILE_NOT_FOUND = 2,
ERROR_PATH_NOT_FOUND = 3,
ERROR_ACCESS_DENIED = 5,
ERROR_WRITE_PROTECT = 19,
ERROR_BAD_UNIT = 20,
ERROR_NOT_READY = 21,
ERROR_WRITE_FAULT = 29,
ERROR_READ_FAULT = 30,
ERROR_SHARING_VIOLATION = 32,
ERROR_LOCK_VIOLATION = 33,
ERROR_HANDLE_DISK_FULL = 39,
ERROR_BAD_NETPATH = 53,
ERROR_NETWORK_BUSY = 54,
ERROR_DEV_NOT_EXIST = 55,
ERROR_FILE_EXISTS = 80,
ERROR_OPEN_FAILED = 110,
ERROR_INVALID_NAME = 123,
ERROR_BAD_PATHNAME = 161,
ERROR_ALREADY_EXISTS = 183,
}
end
local function two_arguments (f1,f2)
return utils.quote_arg(f1)..' '..utils.quote_arg(f2)
end
local function file_op (is_copy,src,dest,flag)
if flag == 1 and path.exists(dest) then
return false,"cannot overwrite destination"
end
if is_windows then
-- if we haven't tried to load Alien/LuaJIT FFI before, then do so
find_ffi_copyfile()
-- fallback if there's no Alien, just use DOS commands *shudder*
-- 'rename' involves a copy and then deleting the source.
if not CopyFile then
src = path.normcase(src)
dest = path.normcase(dest)
local res, err = execute_command('copy',two_arguments(src,dest))
if not res then return false,err end
if not is_copy then
return execute_command('del',utils.quote_arg(src))
end
return true
else
if path.isdir(dest) then
dest = path.join(dest,path.basename(src))
end
local ret
if is_copy then ret = CopyFile(src,dest,flag)
else ret = MoveFile(src,dest) end
if ret == 0 then
local err = GetLastError()
for name,value in pairs(win32_errors) do
if value == err then return false,name end
end
return false,"Error #"..err
else return true
end
end
else -- for Unix, just use cp for now
return execute_command(is_copy and 'cp' or 'mv',
two_arguments(src,dest))
end
end
--- copy a file.
-- @string src source file
-- @string dest destination file or directory
-- @bool flag true if you want to force the copy (default)
-- @treturn bool operation succeeded
-- @raise src and dest must be strings
function dir.copyfile (src,dest,flag)
assert_string(1,src)
assert_string(2,dest)
flag = flag==nil or flag
return file_op(true,src,dest,flag and 0 or 1)
end
--- move a file.
-- @string src source file
-- @string dest destination file or directory
-- @treturn bool operation succeeded
-- @raise src and dest must be strings
function dir.movefile (src,dest)
assert_string(1,src)
assert_string(2,dest)
return file_op(false,src,dest,0)
end
local function _dirfiles(dir,attrib)
local dirs = {}
local files = {}
for f in ldir(dir) do
if f ~= '.' and f ~= '..' then
local p = path.join(dir,f)
local mode = attrib(p,'mode')
if mode=='directory' then
append(dirs,f)
else
append(files,f)
end
end
end
return makelist(dirs), makelist(files)
end
local function _walker(root,bottom_up,attrib)
local dirs,files = _dirfiles(root,attrib)
if not bottom_up then yield(root,dirs,files) end
for i,d in ipairs(dirs) do
_walker(root..path.sep..d,bottom_up,attrib)
end
if bottom_up then yield(root,dirs,files) end
end
--- return an iterator which walks through a directory tree starting at root.
-- The iterator returns (root,dirs,files)
-- Note that dirs and files are lists of names (i.e. you must say path.join(root,d)
-- to get the actual full path)
-- If bottom_up is false (or not present), then the entries at the current level are returned
-- before we go deeper. This means that you can modify the returned list of directories before
-- continuing.
-- This is a clone of os.walk from the Python libraries.
-- @string root A starting directory
-- @bool bottom_up False if we start listing entries immediately.
-- @bool follow_links follow symbolic links
-- @return an iterator returning root,dirs,files
-- @raise root must be a directory
function dir.walk(root,bottom_up,follow_links)
assert_dir(1,root)
local attrib
if path.is_windows or not follow_links then
attrib = path.attrib
else
attrib = path.link_attrib
end
return wrap(function () _walker(root,bottom_up,attrib) end)
end
--- remove a whole directory tree.
-- @string fullpath A directory path
-- @return true or nil
-- @return error if failed
-- @raise fullpath must be a string
function dir.rmtree(fullpath)
assert_dir(1,fullpath)
if path.islink(fullpath) then return false,'will not follow symlink' end
for root,dirs,files in dir.walk(fullpath,true) do
for i,f in ipairs(files) do
local res, err = remove(path.join(root,f))
if not res then return nil,err end
end
local res, err = rmdir(root)
if not res then return nil,err end
end
return true
end
local dirpat
if path.is_windows then
dirpat = '(.+)\\[^\\]+$'
else
dirpat = '(.+)/[^/]+$'
end
local _makepath
function _makepath(p)
-- windows root drive case
if p:find '^%a:[\\]*$' then
return true
end
if not path.isdir(p) then
local subp = p:match(dirpat)
local ok, err = _makepath(subp)
if not ok then return nil, err end
return mkdir(p)
else
return true
end
end
--- create a directory path.
-- This will create subdirectories as necessary!
-- @string p A directory path
-- @return true on success, nil + errormsg on failure
-- @raise failure to create
function dir.makepath (p)
assert_string(1,p)
return _makepath(path.normcase(path.abspath(p)))
end
--- clone a directory tree. Will always try to create a new directory structure
-- if necessary.
-- @string path1 the base path of the source tree
-- @string path2 the new base path for the destination
-- @func file_fun an optional function to apply on all files
-- @bool verbose an optional boolean to control the verbosity of the output.
-- It can also be a logging function that behaves like print()
-- @return true, or nil
-- @return error message, or list of failed directory creations
-- @return list of failed file operations
-- @raise path1 and path2 must be strings
-- @usage clonetree('.','../backup',copyfile)
function dir.clonetree (path1,path2,file_fun,verbose)
assert_string(1,path1)
assert_string(2,path2)
if verbose == true then verbose = print end
local abspath,normcase,isdir,join = path.abspath,path.normcase,path.isdir,path.join
local faildirs,failfiles = {},{}
if not isdir(path1) then return raise 'source is not a valid directory' end
path1 = abspath(normcase(path1))
path2 = abspath(normcase(path2))
if verbose then verbose('normalized:',path1,path2) end
-- particularly NB that the new path isn't fully contained in the old path
if path1 == path2 then return raise "paths are the same" end
local _,i2 = path2:find(path1,1,true)
if i2 == #path1 and path2:sub(i2+1,i2+1) == path.sep then
return raise 'destination is a subdirectory of the source'
end
local cp = path.common_prefix (path1,path2)
local idx = #cp
if idx == 0 then -- no common path, but watch out for Windows paths!
if path1:sub(2,2) == ':' then idx = 3 end
end
for root,dirs,files in dir.walk(path1) do
local opath = path2..root:sub(idx)
if verbose then verbose('paths:',opath,root) end
if not isdir(opath) then
local ret = dir.makepath(opath)
if not ret then append(faildirs,opath) end
if verbose then verbose('creating:',opath,ret) end
end
if file_fun then
for i,f in ipairs(files) do
local p1 = join(root,f)
local p2 = join(opath,f)
local ret = file_fun(p1,p2)
if not ret then append(failfiles,p2) end
if verbose then
verbose('files:',p1,p2,ret)
end
end
end
end
return true,faildirs,failfiles
end
--- return an iterator over all entries in a directory tree
-- @string d a directory
-- @return an iterator giving pathname and mode (true for dir, false otherwise)
-- @raise d must be a non-empty string
function dir.dirtree( d )
assert( d and d ~= "", "directory parameter is missing or empty" )
local exists, isdir = path.exists, path.isdir
local sep = path.sep
local last = sub ( d, -1 )
if last == sep or last == '/' then
d = sub( d, 1, -2 )
end
local function yieldtree( dir )
for entry in ldir( dir ) do
if entry ~= "." and entry ~= ".." then
entry = dir .. sep .. entry
if exists(entry) then -- Just in case a symlink is broken.
local is_dir = isdir(entry)
yield( entry, is_dir )
if is_dir then
yieldtree( entry )
end
end
end
end
end
return wrap( function() yieldtree( d ) end )
end
--- Recursively returns all the file starting at _path_. It can optionally take a shell pattern and
-- only returns files that match _shell_pattern_. If a pattern is given it will do a case insensitive search.
-- @string start_path A directory. If not given, all files in current directory are returned.
-- @string shell_pattern A shell pattern. If not given, all files are returned.
-- @treturn List(string) containing all the files found recursively starting at _path_ and filtered by _shell_pattern_.
-- @raise start_path must be a directory
function dir.getallfiles( start_path, shell_pattern )
assert_dir(1,start_path)
shell_pattern = shell_pattern or "*"
local files = {}
local normcase = path.normcase
for filename, mode in dir.dirtree( start_path ) do
if not mode then
local mask = filemask( shell_pattern )
if normcase(filename):find( mask ) then
files[#files + 1] = filename
end
end
end
return makelist(files)
end
return dir

View file

@ -0,0 +1,62 @@
--- File manipulation functions: reading, writing, moving and copying.
--
-- Dependencies: `pl.utils`, `pl.dir`, `pl.path`
-- @module pl.file
local os = os
local utils = require 'pl.utils'
local dir = require 'pl.dir'
local path = require 'pl.path'
--[[
module ('pl.file',utils._module)
]]
local file = {}
--- return the contents of a file as a string
-- @function file.read
-- @string filename The file path
-- @return file contents
file.read = utils.readfile
--- write a string to a file
-- @function file.write
-- @string filename The file path
-- @string str The string
file.write = utils.writefile
--- copy a file.
-- @function file.copy
-- @string src source file
-- @string dest destination file
-- @bool flag true if you want to force the copy (default)
-- @return true if operation succeeded
file.copy = dir.copyfile
--- move a file.
-- @function file.move
-- @string src source file
-- @string dest destination file
-- @return true if operation succeeded, else false and the reason for the error.
file.move = dir.movefile
--- Return the time of last access as the number of seconds since the epoch.
-- @function file.access_time
-- @string path A file path
file.access_time = path.getatime
---Return when the file was created.
-- @function file.creation_time
-- @string path A file path
file.creation_time = path.getctime
--- Return the time of last modification
-- @function file.modified_time
-- @string path A file path
file.modified_time = path.getmtime
--- Delete a file
-- @function file.delete
-- @string path A file path
file.delete = os.remove
return file

View file

@ -0,0 +1,393 @@
--- Functional helpers like composition, binding and placeholder expressions.
-- Placeholder expressions are useful for short anonymous functions, and were
-- inspired by the Boost Lambda library.
--
-- > utils.import 'pl.func'
-- > ls = List{10,20,30}
-- > = ls:map(_1+1)
-- {11,21,31}
--
-- They can also be used to _bind_ particular arguments of a function.
--
-- > p = bind(print,'start>',_0)
-- > p(10,20,30)
-- > start> 10 20 30
--
-- See @{07-functional.md.Creating_Functions_from_Functions|the Guide}
--
-- Dependencies: `pl.utils`, `pl.tablex`
-- @module pl.func
local type,setmetatable,getmetatable,rawset = type,setmetatable,getmetatable,rawset
local concat,append = table.concat,table.insert
local tostring = tostring
local utils = require 'pl.utils'
local pairs,rawget,unpack,pack = pairs,rawget,utils.unpack,utils.pack
local tablex = require 'pl.tablex'
local map = tablex.map
local _DEBUG = rawget(_G,'_DEBUG')
local assert_arg = utils.assert_arg
local func = {}
-- metatable for Placeholder Expressions (PE)
local _PEMT = {}
local function P (t)
setmetatable(t,_PEMT)
return t
end
func.PE = P
local function isPE (obj)
return getmetatable(obj) == _PEMT
end
func.isPE = isPE
-- construct a placeholder variable (e.g _1 and _2)
local function PH (idx)
return P {op='X',repr='_'..idx, index=idx}
end
-- construct a constant placeholder variable (e.g _C1 and _C2)
local function CPH (idx)
return P {op='X',repr='_C'..idx, index=idx}
end
func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5)
func._0 = P{op='X',repr='...',index=0}
function func.Var (name)
local ls = utils.split(name,'[%s,]+')
local res = {}
for i = 1, #ls do
append(res,P{op='X',repr=ls[i],index=0})
end
return unpack(res)
end
function func._ (value)
return P{op='X',repr=value,index='wrap'}
end
local repr
func.Nil = func.Var 'nil'
function _PEMT.__index(obj,key)
return P{op='[]',obj,key}
end
function _PEMT.__call(fun,...)
return P{op='()',fun,...}
end
function _PEMT.__tostring (e)
return repr(e)
end
function _PEMT.__unm(arg)
return P{op='unm',arg}
end
function func.Not (arg)
return P{op='not',arg}
end
function func.Len (arg)
return P{op='#',arg}
end
local function binreg(context,t)
for name,op in pairs(t) do
rawset(context,name,function(x,y)
return P{op=op,x,y}
end)
end
end
local function import_name (name,fun,context)
rawset(context,name,function(...)
return P{op='()',fun,...}
end)
end
local imported_functions = {}
local function is_global_table (n)
return type(_G[n]) == 'table'
end
--- wrap a table of functions. This makes them available for use in
-- placeholder expressions.
-- @string tname a table name
-- @tab context context to put results, defaults to environment of caller
function func.import(tname,context)
assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
local t = _G[tname]
context = context or _G
for name,fun in pairs(t) do
import_name(name,fun,context)
imported_functions[fun] = name
end
end
--- register a function for use in placeholder expressions.
-- @func fun a function
-- @string[opt] name an optional name
-- @return a placeholder functiond
function func.register (fun,name)
assert_arg(1,fun,'function')
if name then
assert_arg(2,name,'string')
imported_functions[fun] = name
end
return function(...)
return P{op='()',fun,...}
end
end
function func.lookup_imported_name (fun)
return imported_functions[fun]
end
local function _arg(...) return ... end
function func.Args (...)
return P{op='()',_arg,...}
end
-- binary operators with their precedences (see Lua manual)
-- precedences might be incremented by one before use depending on
-- left- or right-associativity, space them out
local binary_operators = {
['or'] = 0,
['and'] = 2,
['=='] = 4, ['~='] = 4, ['<'] = 4, ['>'] = 4, ['<='] = 4, ['>='] = 4,
['..'] = 6,
['+'] = 8, ['-'] = 8,
['*'] = 10, ['/'] = 10, ['%'] = 10,
['^'] = 14
}
-- unary operators with their precedences
local unary_operators = {
['not'] = 12, ['#'] = 12, ['unm'] = 12
}
-- comparisons (as prefix functions)
binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='})
-- standard binary operators (as metamethods)
binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'})
binreg (_PEMT,{__eq='=='})
--- all elements of a table except the first.
-- @tab ls a list-like table.
function func.tail (ls)
assert_arg(1,ls,'table')
local res = {}
for i = 2,#ls do
append(res,ls[i])
end
return res
end
--- create a string representation of a placeholder expression.
-- @param e a placeholder expression
-- @param lastpred not used
function repr (e,lastpred)
local tail = func.tail
if isPE(e) then
local pred = binary_operators[e.op] or unary_operators[e.op]
if pred then
-- binary or unary operator
local s
if binary_operators[e.op] then
local left_pred = pred
local right_pred = pred
if e.op == '..' or e.op == '^' then
left_pred = left_pred + 1
else
right_pred = right_pred + 1
end
local left_arg = repr(e[1], left_pred)
local right_arg = repr(e[2], right_pred)
s = left_arg..' '..e.op..' '..right_arg
else
local op = e.op == 'unm' and '-' or e.op
s = op..' '..repr(e[1], pred)
end
if lastpred and lastpred > pred then
s = '('..s..')'
end
return s
else -- either postfix, or a placeholder
local ls = map(repr,e)
if e.op == '[]' then
return ls[1]..'['..ls[2]..']'
elseif e.op == '()' then
local fn
if ls[1] ~= nil then -- was _args, undeclared!
fn = ls[1]
else
fn = ''
end
return fn..'('..concat(tail(ls),',')..')'
else
return e.repr
end
end
elseif type(e) == 'string' then
return '"'..e..'"'
elseif type(e) == 'function' then
local name = func.lookup_imported_name(e)
if name then return name else return tostring(e) end
else
return tostring(e) --should not really get here!
end
end
func.repr = repr
-- collect all the non-PE values in this PE into vlist, and replace each occurence
-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
local collect_values
function collect_values (e,vlist)
if isPE(e) then
if e.op ~= 'X' then
local m = 0
for i = 1,#e do
local subx = e[i]
local pe = isPE(subx)
if pe then
if subx.op == 'X' and subx.index == 'wrap' then
subx = subx.repr
pe = false
else
m = math.max(m,collect_values(subx,vlist))
end
end
if not pe then
append(vlist,subx)
e[i] = CPH(#vlist)
end
end
return m
else -- was a placeholder, it has an index...
return e.index
end
else -- plain value has no placeholder dependence
return 0
end
end
func.collect_values = collect_values
--- instantiate a PE into an actual function. First we find the largest placeholder used,
-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
-- any non-PE values from the PE, and build up a constant binding list.
-- Finally, the expression can be compiled, and e.__PE_function is set.
-- @param e a placeholder expression
-- @return a function
function func.instantiate (e)
local consts,values,parms = {},{},{}
local rep, err, fun
local n = func.collect_values(e,values)
for i = 1,#values do
append(consts,'_C'..i)
if _DEBUG then print(i,values[i]) end
end
for i =1,n do
append(parms,'_'..i)
end
consts = concat(consts,',')
parms = concat(parms,',')
rep = repr(e)
local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
if _DEBUG then print(fstr) end
fun,err = utils.load(fstr,'fun')
if not fun then return nil,err end
fun = fun() -- get wrapper
fun = fun(unpack(values)) -- call wrapper (values could be empty)
e.__PE_function = fun
return fun
end
--- instantiate a PE unless it has already been done.
-- @param e a placeholder expression
-- @return the function
function func.I(e)
if rawget(e,'__PE_function') then
return e.__PE_function
else return func.instantiate(e)
end
end
utils.add_function_factory(_PEMT,func.I)
--- bind the first parameter of the function to a value.
-- @function func.bind1
-- @func fn a function of one or more arguments
-- @param p a value
-- @return a function of one less argument
-- @usage (bind1(math.max,10))(20) == math.max(10,20)
func.bind1 = utils.bind1
func.curry = func.bind1
--- create a function which chains two functions.
-- @func f a function of at least one argument
-- @func g a function of at least one argument
-- @return a function
-- @usage printf = compose(io.write,string.format)
function func.compose (f,g)
return function(...) return f(g(...)) end
end
--- bind the arguments of a function to given values.
-- `bind(fn,v,_2)` is equivalent to `bind1(fn,v)`.
-- @func fn a function of at least one argument
-- @param ... values or placeholder variables
-- @return a function
-- @usage (bind(f,_1,a))(b) == f(a,b)
-- @usage (bind(f,_2,_1))(a,b) == f(b,a)
function func.bind(fn,...)
local args = pack(...)
local holders,parms,bvalues,values = {},{},{'fn'},{}
local nv,maxplace,varargs = 1,0,false
for i = 1,args.n do
local a = args[i]
if isPE(a) and a.op == 'X' then
append(holders,a.repr)
maxplace = math.max(maxplace,a.index)
if a.index == 0 then varargs = true end
else
local v = '_v'..nv
append(bvalues,v)
append(holders,v)
append(values,a)
nv = nv + 1
end
end
for np = 1,maxplace do
append(parms,'_'..np)
end
if varargs then append(parms,'...') end
bvalues = concat(bvalues,',')
parms = concat(parms,',')
holders = concat(holders,',')
local fstr = ([[
return function (%s)
return function(%s) return fn(%s) end
end
]]):format(bvalues,parms,holders)
if _DEBUG then print(fstr) end
local res = utils.load(fstr)
res = res()
return res(fn,unpack(values))
end
return func

View file

@ -0,0 +1,171 @@
--- Iterators for extracting words or numbers from an input source.
--
-- require 'pl'
-- local total,n = seq.sum(input.numbers())
-- print('average',total/n)
--
-- _source_ is defined as a string or a file-like object (i.e. has a read() method which returns the next line)
--
-- See @{06-data.md.Reading_Unstructured_Text_Data|here}
--
-- Dependencies: `pl.utils`
-- @module pl.input
local strfind = string.find
local strsub = string.sub
local strmatch = string.match
local utils = require 'pl.utils'
local unpack = utils.unpack
local pairs,type,tonumber = pairs,type,tonumber
local patterns = utils.patterns
local io = io
local input = {}
--- create an iterator over all tokens.
-- based on allwords from PiL, 7.1
-- @func getter any function that returns a line of text
-- @string pattern
-- @string[opt] fn Optionally can pass a function to process each token as it's found.
-- @return an iterator
function input.alltokens (getter,pattern,fn)
local line = getter() -- current line
local pos = 1 -- current position in the line
return function () -- iterator function
while line do -- repeat while there are lines
local s, e = strfind(line, pattern, pos)
if s then -- found a word?
pos = e + 1 -- next position is after this token
local res = strsub(line, s, e) -- return the token
if fn then res = fn(res) end
return res
else
line = getter() -- token not found; try next line
pos = 1 -- restart from first position
end
end
return nil -- no more lines: end of traversal
end
end
local alltokens = input.alltokens
-- question: shd this _split_ a string containing line feeds?
--- create a function which grabs the next value from a source. If the source is a string, then the getter
-- will return the string and thereafter return nil. If not specified then the source is assumed to be stdin.
-- @param f a string or a file-like object (i.e. has a read() method which returns the next line)
-- @return a getter function
function input.create_getter(f)
if f then
if type(f) == 'string' then
local ls = utils.split(f,'\n')
local i,n = 0,#ls
return function()
i = i + 1
if i > n then return nil end
return ls[i]
end
else
-- anything that supports the read() method!
if not f.read then error('not a file-like object') end
return function() return f:read() end
end
else
return io.read -- i.e. just read from stdin
end
end
--- generate a sequence of numbers from a source.
-- @param f A source
-- @return An iterator
function input.numbers(f)
return alltokens(input.create_getter(f),
'('..patterns.FLOAT..')',tonumber)
end
--- generate a sequence of words from a source.
-- @param f A source
-- @return An iterator
function input.words(f)
return alltokens(input.create_getter(f),"%w+")
end
local function apply_tonumber (no_fail,...)
local args = {...}
for i = 1,#args do
local n = tonumber(args[i])
if n == nil then
if not no_fail then return nil,args[i] end
else
args[i] = n
end
end
return args
end
--- parse an input source into fields.
-- By default, will fail if it cannot convert a field to a number.
-- @param ids a list of field indices, or a maximum field index
-- @string delim delimiter to parse fields (default space)
-- @param f a source @see create_getter
-- @tab opts option table, `{no_fail=true}`
-- @return an iterator with the field values
-- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin
function input.fields (ids,delim,f,opts)
local sep
local s
local getter = input.create_getter(f)
local no_fail = opts and opts.no_fail
local no_convert = opts and opts.no_convert
if not delim or delim == ' ' then
delim = '%s'
sep = '%s+'
s = '%s*'
else
sep = delim
s = ''
end
local max_id = 0
if type(ids) == 'table' then
for i,id in pairs(ids) do
if id > max_id then max_id = id end
end
else
max_id = ids
ids = {}
for i = 1,max_id do ids[#ids+1] = i end
end
local pat = '[^'..delim..']*'
local k = 1
for i = 1,max_id do
if ids[k] == i then
k = k + 1
s = s..'('..pat..')'
else
s = s..pat
end
if i < max_id then
s = s..sep
end
end
local linecount = 1
return function()
local line,results,err
repeat
line = getter()
linecount = linecount + 1
if not line then return nil end
if no_convert then
results = {strmatch(line,s)}
else
results,err = apply_tonumber(no_fail,strmatch(line,s))
if not results then
utils.quit("line "..(linecount-1)..": cannot convert '"..err.."' to number")
end
end
until #results > 0
return unpack(results)
end
end
return input

View file

@ -0,0 +1,451 @@
--- Simple command-line parsing using human-readable specification.
-- Supports GNU-style parameters.
--
-- lapp = require 'pl.lapp'
-- local args = lapp [[
-- Does some calculations
-- -o,--offset (default 0.0) Offset to add to scaled number
-- -s,--scale (number) Scaling factor
-- <number> (number) Number to be scaled
-- ]]
--
-- print(args.offset + args.scale * args.number)
--
-- Lines beginning with `'-'` are flags; there may be a short and a long name;
-- lines beginning with `'<var>'` are arguments. Anything in parens after
-- the flag/argument is either a default, a type name or a range constraint.
--
-- See @{08-additional.md.Command_line_Programs_with_Lapp|the Guide}
--
-- Dependencies: `pl.sip`
-- @module pl.lapp
local status,sip = pcall(require,'pl.sip')
if not status then
sip = require 'sip'
end
local match = sip.match_at_start
local append,tinsert = table.insert,table.insert
sip.custom_pattern('X','(%a[%w_%-]*)')
local function lines(s) return s:gmatch('([^\n]*)\n') end
local function lstrip(str) return str:gsub('^%s+','') end
local function strip(str) return lstrip(str):gsub('%s+$','') end
local function at(s,k) return s:sub(k,k) end
local lapp = {}
local open_files,parms,aliases,parmlist,usage,script
lapp.callback = false -- keep Strict happy
local filetypes = {
stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},
stderr = {io.stderr,'file-out'}
}
--- controls whether to dump usage on error.
-- Defaults to true
lapp.show_usage_error = true
--- quit this script immediately.
-- @string msg optional message
-- @bool no_usage suppress 'usage' display
function lapp.quit(msg,no_usage)
if no_usage == 'throw' then
error(msg)
end
if msg then
io.stderr:write(msg..'\n\n')
end
if not no_usage then
io.stderr:write(usage)
end
os.exit(1)
end
--- print an error to stderr and quit.
-- @string msg a message
-- @bool no_usage suppress 'usage' display
function lapp.error(msg,no_usage)
if not lapp.show_usage_error then
no_usage = true
elseif lapp.show_usage_error == 'throw' then
no_usage = 'throw'
end
lapp.quit(script..': '..msg,no_usage)
end
--- open a file.
-- This will quit on error, and keep a list of file objects for later cleanup.
-- @string file filename
-- @string[opt] opt same as second parameter of `io.open`
function lapp.open (file,opt)
local val,err = io.open(file,opt)
if not val then lapp.error(err,true) end
append(open_files,val)
return val
end
--- quit if the condition is false.
-- @bool condn a condition
-- @string msg message text
function lapp.assert(condn,msg)
if not condn then
lapp.error(msg)
end
end
local function range_check(x,min,max,parm)
lapp.assert(min <= x and max >= x,parm..' out of range')
end
local function xtonumber(s)
local val = tonumber(s)
if not val then lapp.error("unable to convert to number: "..s) end
return val
end
local types = {}
local builtin_types = {string=true,number=true,['file-in']='file',['file-out']='file',boolean=true}
local function convert_parameter(ps,val)
if ps.converter then
val = ps.converter(val)
end
if ps.type == 'number' then
val = xtonumber(val)
elseif builtin_types[ps.type] == 'file' then
val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' )
elseif ps.type == 'boolean' then
return val
end
if ps.constraint then
ps.constraint(val)
end
return val
end
--- add a new type to Lapp. These appear in parens after the value like
-- a range constraint, e.g. '<ival> (integer) Process PID'
-- @string name name of type
-- @param converter either a function to convert values, or a Lua type name.
-- @func[opt] constraint optional function to verify values, should use lapp.error
-- if failed.
function lapp.add_type (name,converter,constraint)
types[name] = {converter=converter,constraint=constraint}
end
local function force_short(short)
lapp.assert(#short==1,short..": short parameters should be one character")
end
-- deducing type of variable from default value;
local function process_default (sval,vtype)
local val, success
if not vtype or vtype == 'number' then
val = tonumber(sval)
end
if val then -- we have a number!
return val,'number'
elseif filetypes[sval] then
local ft = filetypes[sval]
return ft[1],ft[2]
else
if sval == 'true' and not vtype then
return true, 'boolean'
end
if sval:match '^["\']' then sval = sval:sub(2,-2) end
local ps = types[vtype] or {}
ps.type = vtype
local show_usage_error = lapp.show_usage_error
lapp.show_usage_error = "throw"
success, val = pcall(convert_parameter, ps, sval)
lapp.show_usage_error = show_usage_error
if success then
return val, vtype or 'string'
end
return sval,vtype or 'string'
end
end
--- process a Lapp options string.
-- Usually called as `lapp()`.
-- @string str the options text
-- @tparam {string} args a table of arguments (default is `_G.arg`)
-- @return a table with parameter-value pairs
function lapp.process_options_string(str,args)
local results = {}
local varargs
local arg = args or _G.arg
open_files = {}
parms = {}
aliases = {}
parmlist = {}
local function check_varargs(s)
local res,cnt = s:gsub('^%.%.%.%s*','')
return res, (cnt > 0)
end
local function set_result(ps,parm,val)
parm = type(parm) == "string" and parm:gsub("%W", "_") or parm -- so foo-bar becomes foo_bar in Lua
if not ps.varargs then
results[parm] = val
else
if not results[parm] then
results[parm] = { val }
else
append(results[parm],val)
end
end
end
usage = str
for _,a in ipairs(arg) do
if a == "-h" or a == "--help" then
return lapp.quit()
end
end
for line in lines(str) do
local res = {}
local optparm,defval,vtype,constraint,rest
line = lstrip(line)
local function check(str)
return match(str,line,res)
end
-- flags: either '-<short>', '-<short>,--<long>' or '--<long>'
if check '-$v{short}, --$o{long} $' or check '-$v{short} $' or check '--$o{long} $' then
if res.long then
optparm = res.long:gsub('[^%w%-]','_') -- I'm not sure the $o pattern will let anything else through?
if #res.rest == 1 then optparm = optparm .. res.rest end
if res.short then aliases[res.short] = optparm end
else
optparm = res.short
end
if res.short and not lapp.slack then force_short(res.short) end
res.rest, varargs = check_varargs(res.rest)
elseif check '$<{name} $' then -- is it <parameter_name>?
-- so <input file...> becomes input_file ...
optparm,rest = res.name:match '([^%.]+)(.*)'
optparm = optparm:gsub('%A','_')
varargs = rest == '...'
append(parmlist,optparm)
end
-- this is not a pure doc line and specifies the flag/parameter type
if res.rest then
line = res.rest
res = {}
local optional
-- do we have ([optional] [<type>] [default <val>])?
if match('$({def} $',line,res) or match('$({def}',line,res) then
local typespec = strip(res.def)
local ftype, rest = typespec:match('^(%S+)(.*)$')
rest = strip(rest)
if ftype == 'optional' then
ftype, rest = rest:match('^(%S+)(.*)$')
rest = strip(rest)
optional = true
end
local default
if ftype == 'default' then
default = true
if rest == '' then lapp.error("value must follow default") end
else -- a type specification
if match('$f{min}..$f{max}',ftype,res) then
-- a numerical range like 1..10
local min,max = res.min,res.max
vtype = 'number'
constraint = function(x)
range_check(x,min,max,optparm)
end
elseif not ftype:match '|' then -- plain type
vtype = ftype
else
-- 'enum' type is a string which must belong to
-- one of several distinct values
local enums = ftype
local enump = '|' .. enums .. '|'
vtype = 'string'
constraint = function(s)
lapp.assert(enump:match('|'..s..'|'),
"value '"..s.."' not in "..enums
)
end
end
end
res.rest = rest
typespec = res.rest
-- optional 'default value' clause. Type is inferred as
-- 'string' or 'number' if there's no explicit type
if default or match('default $r{rest}',typespec,res) then
defval,vtype = process_default(res.rest,vtype)
end
else -- must be a plain flag, no extra parameter required
defval = false
vtype = 'boolean'
end
local ps = {
type = vtype,
defval = defval,
required = defval == nil and not optional,
comment = res.rest or optparm,
constraint = constraint,
varargs = varargs
}
varargs = nil
if types[vtype] then
local converter = types[vtype].converter
if type(converter) == 'string' then
ps.type = converter
else
ps.converter = converter
end
ps.constraint = types[vtype].constraint
elseif not builtin_types[vtype] and vtype then
lapp.error(vtype.." is unknown type")
end
parms[optparm] = ps
end
end
-- cool, we have our parms, let's parse the command line args
local iparm = 1
local iextra = 1
local i = 1
local parm,ps,val
local end_of_flags = false
local function check_parm (parm)
local eqi = parm:find '[=:]'
if eqi then
tinsert(arg,i+1,parm:sub(eqi+1))
parm = parm:sub(1,eqi-1)
end
return parm,eqi
end
local function is_flag (parm)
return parms[aliases[parm] or parm]
end
while i <= #arg do
local theArg = arg[i]
local res = {}
-- after '--' we don't parse args and they end up in
-- the array part of the result (args[1] etc)
if theArg == '--' then
end_of_flags = true
iparm = #parmlist + 1
i = i + 1
theArg = arg[i]
if not theArg then
break
end
end
-- look for a flag, -<short flags> or --<long flag>
if not end_of_flags and (match('--$S{long}',theArg,res) or match('-$S{short}',theArg,res)) then
if res.long then -- long option
parm = check_parm(res.long)
elseif #res.short == 1 or is_flag(res.short) then
parm = res.short
else
local parmstr,eq = check_parm(res.short)
if not eq then
parm = at(parmstr,1)
local flag = is_flag(parm)
if flag and flag.type ~= 'boolean' then
--if isdigit(at(parmstr,2)) then
-- a short option followed by a digit is an exception (for AW;))
-- push ahead into the arg array
tinsert(arg,i+1,parmstr:sub(2))
else
-- push multiple flags into the arg array!
for k = 2,#parmstr do
tinsert(arg,i+k-1,'-'..at(parmstr,k))
end
end
else
parm = parmstr
end
end
if aliases[parm] then parm = aliases[parm] end
if not parms[parm] and (parm == 'h' or parm == 'help') then
lapp.quit()
end
else -- a parameter
parm = parmlist[iparm]
if not parm then
-- extra unnamed parameters are indexed starting at 1
parm = iextra
ps = { type = 'string' }
parms[parm] = ps
iextra = iextra + 1
else
ps = parms[parm]
end
if not ps.varargs then
iparm = iparm + 1
end
val = theArg
end
ps = parms[parm]
if not ps then lapp.error("unrecognized parameter: "..parm) end
if ps.type ~= 'boolean' then -- we need a value! This should follow
if not val then
i = i + 1
val = arg[i]
theArg = val
end
lapp.assert(val,parm.." was expecting a value")
else -- toggle boolean flags (usually false -> true)
val = not ps.defval
end
ps.used = true
val = convert_parameter(ps,val)
set_result(ps,parm,val)
if builtin_types[ps.type] == 'file' then
set_result(ps,parm..'_name',theArg)
end
if lapp.callback then
lapp.callback(parm,theArg,res)
end
i = i + 1
val = nil
end
-- check unused parms, set defaults and check if any required parameters were missed
for parm,ps in pairs(parms) do
if not ps.used then
if ps.required then lapp.error("missing required parameter: "..parm) end
set_result(ps,parm,ps.defval)
end
end
return results
end
if arg then
script = arg[0]
script = script or rawget(_G,"LAPP_SCRIPT") or "unknown"
-- strip dir and extension to get current script name
script = script:gsub('.+[\\/]',''):gsub('%.%a+$','')
else
script = "inter"
end
setmetatable(lapp, {
__call = function(tbl,str,args) return lapp.process_options_string(str,args) end,
})
return lapp

View file

@ -0,0 +1,488 @@
--- Lexical scanner for creating a sequence of tokens from text.
-- `lexer.scan(s)` returns an iterator over all tokens found in the
-- string `s`. This iterator returns two values, a token type string
-- (such as 'string' for quoted string, 'iden' for identifier) and the value of the
-- token.
--
-- Versions specialized for Lua and C are available; these also handle block comments
-- and classify keywords as 'keyword' tokens. For example:
--
-- > s = 'for i=1,n do'
-- > for t,v in lexer.lua(s) do print(t,v) end
-- keyword for
-- iden i
-- = =
-- number 1
-- , ,
-- iden n
-- keyword do
--
-- See the Guide for further @{06-data.md.Lexical_Scanning|discussion}
-- @module pl.lexer
local yield,wrap = coroutine.yield,coroutine.wrap
local strfind = string.find
local strsub = string.sub
local append = table.insert
local function assert_arg(idx,val,tp)
if type(val) ~= tp then
error("argument "..idx.." must be "..tp, 2)
end
end
local lexer = {}
local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+'
local NUMBER2 = '^[%+%-]?%d+%.?%d*'
local NUMBER3 = '^0x[%da-fA-F]+'
local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+'
local NUMBER5 = '^%d+%.?%d*'
local IDEN = '^[%a_][%w_]*'
local WSPACE = '^%s+'
local STRING1 = "^(['\"])%1" -- empty string
local STRING2 = [[^(['"])(\*)%2%1]]
local STRING3 = [[^(['"]).-[^\](\*)%2%1]]
local CHAR1 = "^''"
local CHAR2 = [[^'(\*)%1']]
local CHAR3 = [[^'.-[^\](\*)%1']]
local PREPRO = '^#.-[^\\]\n'
local plain_matches,lua_matches,cpp_matches,lua_keyword,cpp_keyword
local function tdump(tok)
return yield(tok,tok)
end
local function ndump(tok,options)
if options and options.number then
tok = tonumber(tok)
end
return yield("number",tok)
end
-- regular strings, single or double quotes; usually we want them
-- without the quotes
local function sdump(tok,options)
if options and options.string then
tok = tok:sub(2,-2)
end
return yield("string",tok)
end
-- long Lua strings need extra work to get rid of the quotes
local function sdump_l(tok,options,findres)
if options and options.string then
local quotelen = 3
if findres[3] then
quotelen = quotelen + findres[3]:len()
end
tok = tok:sub(quotelen, -quotelen)
if tok:sub(1, 1) == "\n" then
tok = tok:sub(2)
end
end
return yield("string",tok)
end
local function chdump(tok,options)
if options and options.string then
tok = tok:sub(2,-2)
end
return yield("char",tok)
end
local function cdump(tok)
return yield('comment',tok)
end
local function wsdump (tok)
return yield("space",tok)
end
local function pdump (tok)
return yield('prepro',tok)
end
local function plain_vdump(tok)
return yield("iden",tok)
end
local function lua_vdump(tok)
if lua_keyword[tok] then
return yield("keyword",tok)
else
return yield("iden",tok)
end
end
local function cpp_vdump(tok)
if cpp_keyword[tok] then
return yield("keyword",tok)
else
return yield("iden",tok)
end
end
--- create a plain token iterator from a string or file-like object.
-- @tparam string|file s a string or a file-like object with `:read()` method returning lines.
-- @tab matches an optional match table - array of token descriptions.
-- A token is described by a `{pattern, action}` pair, where `pattern` should match
-- token body and `action` is a function called when a token of described type is found.
-- @tab[opt] filter a table of token types to exclude, by default `{space=true}`
-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
-- which means convert numbers and strip string quotes.
function lexer.scan(s,matches,filter,options)
local file = type(s) ~= 'string' and s
filter = filter or {space=true}
options = options or {number=true,string=true}
if filter then
if filter.space then filter[wsdump] = true end
if filter.comments then
filter[cdump] = true
end
end
if not matches then
if not plain_matches then
plain_matches = {
{WSPACE,wsdump},
{NUMBER3,ndump},
{IDEN,plain_vdump},
{NUMBER1,ndump},
{NUMBER2,ndump},
{STRING1,sdump},
{STRING2,sdump},
{STRING3,sdump},
{'^.',tdump}
}
end
matches = plain_matches
end
local function lex(first_arg)
local line_nr = 0
local next_line = file and file:read()
local sz = file and 0 or #s
local idx = 1
-- res is the value used to resume the coroutine.
local function handle_requests(res)
while res do
local tp = type(res)
-- insert a token list
if tp == 'table' then
res = yield('','')
for _,t in ipairs(res) do
res = yield(t[1],t[2])
end
elseif tp == 'string' then -- or search up to some special pattern
local i1,i2 = strfind(s,res,idx)
if i1 then
local tok = strsub(s,i1,i2)
idx = i2 + 1
res = yield('',tok)
else
res = yield('','')
idx = sz + 1
end
else
res = yield(line_nr,idx)
end
end
end
handle_requests(first_arg)
if not file then line_nr = 1 end
while true do
if idx > sz then
if file then
if not next_line then return end
s = next_line
line_nr = line_nr + 1
next_line = file:read()
if next_line then
s = s .. '\n'
end
idx, sz = 1, #s
else
while true do
handle_requests(yield())
end
end
end
for _,m in ipairs(matches) do
local pat = m[1]
local fun = m[2]
local findres = {strfind(s,pat,idx)}
local i1, i2 = findres[1], findres[2]
if i1 then
local tok = strsub(s,i1,i2)
idx = i2 + 1
local res
if not (filter and filter[fun]) then
lexer.finished = idx > sz
res = fun(tok, options, findres)
end
if not file and tok:find("\n") then
-- Update line number.
local _, newlines = tok:gsub("\n", {})
line_nr = line_nr + newlines
end
handle_requests(res)
break
end
end
end
end
return wrap(lex)
end
local function isstring (s)
return type(s) == 'string'
end
--- insert tokens into a stream.
-- @param tok a token stream
-- @param a1 a string is the type, a table is a token list and
-- a function is assumed to be a token-like iterator (returns type & value)
-- @string a2 a string is the value
function lexer.insert (tok,a1,a2)
if not a1 then return end
local ts
if isstring(a1) and isstring(a2) then
ts = {{a1,a2}}
elseif type(a1) == 'function' then
ts = {}
for t,v in a1() do
append(ts,{t,v})
end
else
ts = a1
end
tok(ts)
end
--- get everything in a stream upto a newline.
-- @param tok a token stream
-- @return a string
function lexer.getline (tok)
local _,v = tok('.-\n')
return v
end
--- get current line number.
-- @param tok a token stream
-- @return the line number.
-- if the input source is a file-like object,
-- also return the column.
function lexer.lineno (tok)
return tok(0)
end
--- get the rest of the stream.
-- @param tok a token stream
-- @return a string
function lexer.getrest (tok)
local _,v = tok('.+')
return v
end
--- get the Lua keywords as a set-like table.
-- So `res["and"]` etc would be `true`.
-- @return a table
function lexer.get_keywords ()
if not lua_keyword then
lua_keyword = {
["and"] = true, ["break"] = true, ["do"] = true,
["else"] = true, ["elseif"] = true, ["end"] = true,
["false"] = true, ["for"] = true, ["function"] = true,
["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true,
["not"] = true, ["or"] = true, ["repeat"] = true,
["return"] = true, ["then"] = true, ["true"] = true,
["until"] = true, ["while"] = true
}
end
return lua_keyword
end
--- create a Lua token iterator from a string or file-like object.
-- Will return the token type and value.
-- @string s the string
-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
-- which means convert numbers and strip string quotes.
function lexer.lua(s,filter,options)
filter = filter or {space=true,comments=true}
lexer.get_keywords()
if not lua_matches then
lua_matches = {
{WSPACE,wsdump},
{NUMBER3,ndump},
{IDEN,lua_vdump},
{NUMBER4,ndump},
{NUMBER5,ndump},
{STRING1,sdump},
{STRING2,sdump},
{STRING3,sdump},
{'^%-%-%[(=*)%[.-%]%1%]',cdump},
{'^%-%-.-\n',cdump},
{'^%[(=*)%[.-%]%1%]',sdump_l},
{'^==',tdump},
{'^~=',tdump},
{'^<=',tdump},
{'^>=',tdump},
{'^%.%.%.',tdump},
{'^%.%.',tdump},
{'^.',tdump}
}
end
return lexer.scan(s,lua_matches,filter,options)
end
--- create a C/C++ token iterator from a string or file-like object.
-- Will return the token type type and value.
-- @string s the string
-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
-- which means convert numbers and strip string quotes.
function lexer.cpp(s,filter,options)
filter = filter or {space=true,comments=true}
if not cpp_keyword then
cpp_keyword = {
["class"] = true, ["break"] = true, ["do"] = true, ["sizeof"] = true,
["else"] = true, ["continue"] = true, ["struct"] = true,
["false"] = true, ["for"] = true, ["public"] = true, ["void"] = true,
["private"] = true, ["protected"] = true, ["goto"] = true,
["if"] = true, ["static"] = true, ["const"] = true, ["typedef"] = true,
["enum"] = true, ["char"] = true, ["int"] = true, ["bool"] = true,
["long"] = true, ["float"] = true, ["true"] = true, ["delete"] = true,
["double"] = true, ["while"] = true, ["new"] = true,
["namespace"] = true, ["try"] = true, ["catch"] = true,
["switch"] = true, ["case"] = true, ["extern"] = true,
["return"] = true,["default"] = true,['unsigned'] = true,['signed'] = true,
["union"] = true, ["volatile"] = true, ["register"] = true,["short"] = true,
}
end
if not cpp_matches then
cpp_matches = {
{WSPACE,wsdump},
{PREPRO,pdump},
{NUMBER3,ndump},
{IDEN,cpp_vdump},
{NUMBER4,ndump},
{NUMBER5,ndump},
{CHAR1,chdump},
{CHAR2,chdump},
{CHAR3,chdump},
{STRING1,sdump},
{STRING2,sdump},
{STRING3,sdump},
{'^//.-\n',cdump},
{'^/%*.-%*/',cdump},
{'^==',tdump},
{'^!=',tdump},
{'^<=',tdump},
{'^>=',tdump},
{'^->',tdump},
{'^&&',tdump},
{'^||',tdump},
{'^%+%+',tdump},
{'^%-%-',tdump},
{'^%+=',tdump},
{'^%-=',tdump},
{'^%*=',tdump},
{'^/=',tdump},
{'^|=',tdump},
{'^%^=',tdump},
{'^::',tdump},
{'^.',tdump}
}
end
return lexer.scan(s,cpp_matches,filter,options)
end
--- get a list of parameters separated by a delimiter from a stream.
-- @param tok the token stream
-- @string[opt=')'] endtoken end of list. Can be '\n'
-- @string[opt=','] delim separator
-- @return a list of token lists.
function lexer.get_separated_list(tok,endtoken,delim)
endtoken = endtoken or ')'
delim = delim or ','
local parm_values = {}
local level = 1 -- used to count ( and )
local tl = {}
local function tappend (tl,t,val)
val = val or t
append(tl,{t,val})
end
local is_end
if endtoken == '\n' then
is_end = function(t,val)
return t == 'space' and val:find '\n'
end
else
is_end = function (t)
return t == endtoken
end
end
local token,value
while true do
token,value=tok()
if not token then return nil,'EOS' end -- end of stream is an error!
if is_end(token,value) and level == 1 then
append(parm_values,tl)
break
elseif token == '(' then
level = level + 1
tappend(tl,'(')
elseif token == ')' then
level = level - 1
if level == 0 then -- finished with parm list
append(parm_values,tl)
break
else
tappend(tl,')')
end
elseif token == delim and level == 1 then
append(parm_values,tl) -- a new parm
tl = {}
else
tappend(tl,token,value)
end
end
return parm_values,{token,value}
end
--- get the next non-space token from the stream.
-- @param tok the token stream.
function lexer.skipws (tok)
local t,v = tok()
while t == 'space' do
t,v = tok()
end
return t,v
end
local skipws = lexer.skipws
--- get the next token, which must be of the expected type.
-- Throws an error if this type does not match!
-- @param tok the token stream
-- @string expected_type the token type
-- @bool no_skip_ws whether we should skip whitespace
function lexer.expecting (tok,expected_type,no_skip_ws)
assert_arg(1,tok,'function')
assert_arg(2,expected_type,'string')
local t,v
if no_skip_ws then
t,v = tok()
else
t,v = skipws(tok)
end
if t ~= expected_type then error ("expecting "..expected_type,2) end
return v
end
return lexer

View file

@ -0,0 +1,264 @@
--- Extract delimited Lua sequences from strings.
-- Inspired by Damian Conway's Text::Balanced in Perl. <br/>
-- <ul>
-- <li>[1] <a href="http://lua-users.org/wiki/LuaBalanced">Lua Wiki Page</a></li>
-- <li>[2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm</li>
-- </ul> <br/>
-- <pre class=example>
-- local lb = require "pl.luabalanced"
-- --Extract Lua expression starting at position 4.
-- print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
-- --> x^2 + x > 5 16
-- --Extract Lua string starting at (default) position 1.
-- print(lb.match_string([["test\"123" .. "more"]]))
-- --> "test\"123" 12
-- </pre>
-- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license).
-- @class module
-- @name pl.luabalanced
local M = {}
local assert = assert
-- map opening brace <-> closing brace.
local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' }
local begins = {}; for k,v in pairs(ends) do begins[v] = k end
-- Match Lua string in string <s> starting at position <pos>.
-- Returns <string>, <posnew>, where <string> is the matched
-- string (or nil on no match) and <posnew> is the character
-- following the match (or <pos> on no match).
-- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc.
local function match_string(s, pos)
pos = pos or 1
local posa = pos
local c = s:sub(pos,pos)
if c == '"' or c == "'" then
pos = pos + 1
while 1 do
pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error')
if s:sub(pos,pos) == c then
local part = s:sub(posa, pos)
return part, pos + 1
else
pos = pos + 2
end
end
else
local sc = s:match("^%[(=*)%[", pos)
if sc then
local _; _, pos = s:find("%]" .. sc .. "%]", pos)
assert(pos)
local part = s:sub(posa, pos)
return part, pos + 1
else
return nil, pos
end
end
end
M.match_string = match_string
-- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]",
-- [=[...]=], etc.
-- Function interface is similar to match_string.
local function match_bracketed(s, pos)
pos = pos or 1
local posa = pos
local ca = s:sub(pos,pos)
if not ends[ca] then
return nil, pos
end
local stack = {}
while 1 do
pos = s:find('[%(%{%[%)%}%]\"\']', pos)
assert(pos, 'syntax error: unbalanced')
local c = s:sub(pos,pos)
if c == '"' or c == "'" then
local part; part, pos = match_string(s, pos)
assert(part)
elseif ends[c] then -- open
local mid, posb
if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end
if mid then
pos = s:match('%]' .. mid .. '%]()', posb)
assert(pos, 'syntax error: long string not terminated')
if #stack == 0 then
local part = s:sub(posa, pos-1)
return part, pos
end
else
stack[#stack+1] = c
pos = pos + 1
end
else -- close
assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced')
stack[#stack] = nil
if #stack == 0 then
local part = s:sub(posa, pos)
return part, pos+1
end
pos = pos + 1
end
end
end
M.match_bracketed = match_bracketed
-- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc.
-- Function interface is similar to match_string.
local function match_comment(s, pos)
pos = pos or 1
if s:sub(pos, pos+1) ~= '--' then
return nil, pos
end
pos = pos + 2
local partt, post = match_string(s, pos)
if partt then
return '--' .. partt, post
end
local part; part, pos = s:match('^([^\n]*\n?)()', pos)
return '--' .. part, pos
end
-- Match Lua expression, e.g. "a + b * c[e]".
-- Function interface is similar to match_string.
local wordop = {['and']=true, ['or']=true, ['not']=true}
local is_compare = {['>']=true, ['<']=true, ['~']=true}
local function match_expression(s, pos)
pos = pos or 1
local _
local posa = pos
local lastident
local poscs, posce
while pos do
local c = s:sub(pos,pos)
if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then
local part; part, pos = match_string(s, pos)
assert(part, 'syntax error')
elseif c == '-' and s:sub(pos+1,pos+1) == '-' then
-- note: handle adjacent comments in loop to properly support
-- backtracing (poscs/posce).
poscs = pos
while s:sub(pos,pos+1) == '--' do
local part; part, pos = match_comment(s, pos)
assert(part)
pos = s:match('^%s*()', pos)
posce = pos
end
elseif c == '(' or c == '{' or c == '[' then
_, pos = match_bracketed(s, pos)
elseif c == '=' and s:sub(pos+1,pos+1) == '=' then
pos = pos + 2 -- skip over two-char op containing '='
elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then
pos = pos + 1 -- skip over two-char op containing '='
elseif c:match'^[%)%}%];,=]' then
local part = s:sub(posa, pos-1)
return part, pos
elseif c:match'^[%w_]' then
local newident,newpos = s:match('^([%w_]+)()', pos)
if pos ~= posa and not wordop[newident] then -- non-first ident
local pose = ((posce == pos) and poscs or pos) - 1
while s:match('^%s', pose) do pose = pose - 1 end
local ce = s:sub(pose,pose)
if ce:match'[%)%}\'\"%]]' or
ce:match'[%w_]' and not wordop[lastident]
then
local part = s:sub(posa, pos-1)
return part, pos
end
end
lastident, pos = newident, newpos
else
pos = pos + 1
end
pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos)
end
local part = s:sub(posa, #s)
return part, #s+1
end
M.match_expression = match_expression
-- Match name list (zero or more names). E.g. "a,b,c"
-- Function interface is similar to match_string,
-- but returns array as match.
local function match_namelist(s, pos)
pos = pos or 1
local list = {}
while 1 do
local c = #list == 0 and '^' or '^%s*,%s*'
local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos)
if item then pos = post else break end
list[#list+1] = item
end
return list, pos
end
M.match_namelist = match_namelist
-- Match expression list (zero or more expressions). E.g. "a+b,b*c".
-- Function interface is similar to match_string,
-- but returns array as match.
local function match_explist(s, pos)
pos = pos or 1
local list = {}
while 1 do
if #list ~= 0 then
local post = s:match('^%s*,%s*()', pos)
if post then pos = post else break end
end
local item; item, pos = match_expression(s, pos)
assert(item, 'syntax error')
list[#list+1] = item
end
return list, pos
end
M.match_explist = match_explist
-- Replace snippets of code in Lua code string <s>
-- using replacement function f(u,sin) --> sout.
-- <u> is the type of snippet ('c' = comment, 's' = string,
-- 'e' = any other code).
-- Snippet is replaced with <sout> (unless <sout> is nil or false, in
-- which case the original snippet is kept)
-- This is somewhat analogous to string.gsub .
local function gsub(s, f)
local pos = 1
local posa = 1
local sret = ''
while 1 do
pos = s:find('[%-\'\"%[]', pos)
if not pos then break end
if s:match('^%-%-', pos) then
local exp = s:sub(posa, pos-1)
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
local comment; comment, pos = match_comment(s, pos)
sret = sret .. (f('c', assert(comment)) or comment)
posa = pos
else
local posb = s:find('^[\'\"%[]', pos)
local str
if posb then str, pos = match_string(s, posb) end
if str then
local exp = s:sub(posa, posb-1)
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
sret = sret .. (f('s', str) or str)
posa = pos
else
pos = pos + 1
end
end
end
local exp = s:sub(posa)
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
return sret
end
M.gsub = gsub
return M

Some files were not shown because too many files have changed in this diff Show more