[aegisub] bunch of scripts
This commit is contained in:
parent
cca2f1f756
commit
758e2410f0
75
.aegisub/automation/autoload/Flux.Selegator.moon
Normal file
75
.aegisub/automation/autoload/Flux.Selegator.moon
Normal 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 }
|
||||
}
|
727
.aegisub/automation/autoload/a-mo.Aegisub-Motion.moon
Normal file
727
.aegisub/automation/autoload/a-mo.Aegisub-Motion.moon
Normal file
File diff suppressed because it is too large
Load diff
25
.aegisub/automation/autoload/clean-k-tags.lua
Normal file
25
.aegisub/automation/autoload/clean-k-tags.lua
Normal 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)
|
178
.aegisub/automation/autoload/duetto-meika.lua
Normal file
178
.aegisub/automation/autoload/duetto-meika.lua
Normal 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)
|
398
.aegisub/automation/autoload/lyger.FbfTransform.moon
Normal file
398
.aegisub/automation/autoload/lyger.FbfTransform.moon
Normal 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
|
569
.aegisub/automation/autoload/lyger.Image2ASS.lua
Normal file
569
.aegisub/automation/autoload/lyger.Image2ASS.lua
Normal file
File diff suppressed because it is too large
Load diff
175
.aegisub/automation/autoload/lyger.KaraHelper.lua
Normal file
175
.aegisub/automation/autoload/lyger.KaraHelper.lua
Normal 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)
|
||||
|
||||
|
||||
|
224
.aegisub/automation/autoload/lyger.KaraReplacer.lua
Normal file
224
.aegisub/automation/autoload/lyger.KaraReplacer.lua
Normal 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)
|
633
.aegisub/automation/autoload/lyger.LuaInterpret.lua
Normal file
633
.aegisub/automation/autoload/lyger.LuaInterpret.lua
Normal file
File diff suppressed because it is too large
Load diff
591
.aegisub/automation/autoload/myaa.MergeScripts.moon
Normal file
591
.aegisub/automation/autoload/myaa.MergeScripts.moon
Normal file
File diff suppressed because it is too large
Load diff
1498
.aegisub/automation/autoload/ua.FadeWorks.lua
Normal file
1498
.aegisub/automation/autoload/ua.FadeWorks.lua
Normal file
File diff suppressed because it is too large
Load diff
3134
.aegisub/automation/autoload/ua.Significance.lua
Normal file
3134
.aegisub/automation/autoload/ua.Significance.lua
Normal file
File diff suppressed because it is too large
Load diff
185
.aegisub/automation/autoload/ua.TimeSigns.lua
Normal file
185
.aegisub/automation/autoload/ua.TimeSigns.lua
Normal 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
|
142
.aegisub/automation/include/a-mo/ConfigHandler.moon
Normal file
142
.aegisub/automation/include/a-mo/ConfigHandler.moon
Normal 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
|
132
.aegisub/automation/include/a-mo/DataHandler.moon
Normal file
132
.aegisub/automation/include/a-mo/DataHandler.moon
Normal 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
|
72
.aegisub/automation/include/a-mo/DataWrapper.moon
Normal file
72
.aegisub/automation/include/a-mo/DataWrapper.moon
Normal 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
|
483
.aegisub/automation/include/a-mo/Line.moon
Normal file
483
.aegisub/automation/include/a-mo/Line.moon
Normal 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
|
272
.aegisub/automation/include/a-mo/LineCollection.moon
Normal file
272
.aegisub/automation/include/a-mo/LineCollection.moon
Normal 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
|
63
.aegisub/automation/include/a-mo/Log.moon
Normal file
63
.aegisub/automation/include/a-mo/Log.moon
Normal 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!
|
||||
}
|
20
.aegisub/automation/include/a-mo/Math.moon
Normal file
20
.aegisub/automation/include/a-mo/Math.moon
Normal 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
|
||||
}
|
312
.aegisub/automation/include/a-mo/MotionHandler.moon
Normal file
312
.aegisub/automation/include/a-mo/MotionHandler.moon
Normal 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
|
130
.aegisub/automation/include/a-mo/ShakeShapeHandler.moon
Normal file
130
.aegisub/automation/include/a-mo/ShakeShapeHandler.moon
Normal 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
|
153
.aegisub/automation/include/a-mo/Statistics.moon
Normal file
153
.aegisub/automation/include/a-mo/Statistics.moon
Normal 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
|
193
.aegisub/automation/include/a-mo/Tags.moon
Normal file
193
.aegisub/automation/include/a-mo/Tags.moon
Normal 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
|
143
.aegisub/automation/include/a-mo/Transform.moon
Normal file
143
.aegisub/automation/include/a-mo/Transform.moon
Normal 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
|
185
.aegisub/automation/include/a-mo/TrimHandler.moon
Normal file
185
.aegisub/automation/include/a-mo/TrimHandler.moon
Normal 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
|
901
.aegisub/automation/include/l0/Functional.moon
Normal file
901
.aegisub/automation/include/l0/Functional.moon
Normal file
File diff suppressed because it is too large
Load diff
362
.aegisub/automation/include/lyger/LibLyger.moon
Normal file
362
.aegisub/automation/include/lyger/LibLyger.moon
Normal 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
|
387
.aegisub/automation/include/myaa/ASSParser.moon
Normal file
387
.aegisub/automation/include/myaa/ASSParser.moon
Normal 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
|
34
.aegisub/automation/include/myaa/pl.lua
Normal file
34
.aegisub/automation/include/myaa/pl.lua
Normal 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
|
663
.aegisub/automation/include/myaa/pl/Date.lua
Normal file
663
.aegisub/automation/include/myaa/pl/Date.lua
Normal file
File diff suppressed because it is too large
Load diff
566
.aegisub/automation/include/myaa/pl/List.lua
Normal file
566
.aegisub/automation/include/myaa/pl/List.lua
Normal file
File diff suppressed because it is too large
Load diff
113
.aegisub/automation/include/myaa/pl/Map.lua
Normal file
113
.aegisub/automation/include/myaa/pl/Map.lua
Normal 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
|
54
.aegisub/automation/include/myaa/pl/MultiMap.lua
Normal file
54
.aegisub/automation/include/myaa/pl/MultiMap.lua
Normal 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
|
167
.aegisub/automation/include/myaa/pl/OrderedMap.lua
Normal file
167
.aegisub/automation/include/myaa/pl/OrderedMap.lua
Normal 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
|
||||
|
||||
|
||||
|
222
.aegisub/automation/include/myaa/pl/Set.lua
Normal file
222
.aegisub/automation/include/myaa/pl/Set.lua
Normal 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
|
159
.aegisub/automation/include/myaa/pl/app.lua
Normal file
159
.aegisub/automation/include/myaa/pl/app.lua
Normal 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
|
493
.aegisub/automation/include/myaa/pl/array2d.lua
Normal file
493
.aegisub/automation/include/myaa/pl/array2d.lua
Normal 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
|
||||
|
||||
|
261
.aegisub/automation/include/myaa/pl/class.lua
Normal file
261
.aegisub/automation/include/myaa/pl/class.lua
Normal 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
|
||||
|
165
.aegisub/automation/include/myaa/pl/compat.lua
Normal file
165
.aegisub/automation/include/myaa/pl/compat.lua
Normal 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
|
285
.aegisub/automation/include/myaa/pl/comprehension.lua
Normal file
285
.aegisub/automation/include/myaa/pl/comprehension.lua
Normal 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
|
207
.aegisub/automation/include/myaa/pl/config.lua
Normal file
207
.aegisub/automation/include/myaa/pl/config.lua
Normal 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
|
654
.aegisub/automation/include/myaa/pl/data.lua
Normal file
654
.aegisub/automation/include/myaa/pl/data.lua
Normal file
File diff suppressed because it is too large
Load diff
470
.aegisub/automation/include/myaa/pl/dir.lua
Normal file
470
.aegisub/automation/include/myaa/pl/dir.lua
Normal 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
|
62
.aegisub/automation/include/myaa/pl/file.lua
Normal file
62
.aegisub/automation/include/myaa/pl/file.lua
Normal 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
|
393
.aegisub/automation/include/myaa/pl/func.lua
Normal file
393
.aegisub/automation/include/myaa/pl/func.lua
Normal 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
|
||||
|
||||
|
171
.aegisub/automation/include/myaa/pl/input.lua
Normal file
171
.aegisub/automation/include/myaa/pl/input.lua
Normal 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
|
||||
|
451
.aegisub/automation/include/myaa/pl/lapp.lua
Normal file
451
.aegisub/automation/include/myaa/pl/lapp.lua
Normal 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
|
||||
|
||||
|
488
.aegisub/automation/include/myaa/pl/lexer.lua
Normal file
488
.aegisub/automation/include/myaa/pl/lexer.lua
Normal 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
|
264
.aegisub/automation/include/myaa/pl/luabalanced.lua
Normal file
264
.aegisub/automation/include/myaa/pl/luabalanced.lua
Normal 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
Loading…
Reference in a new issue