[aegisub] cleanup scripts
now I use DependencyControl because makes it a bit easier to install new scripts (at least when the installation works). you'll need to install ffi-experiments yourself for now (i don't really want to keep .so files here)
This commit is contained in:
parent
398ce7b105
commit
92f84239ae
1
.aegisub/automation/amoegisub
Submodule
1
.aegisub/automation/amoegisub
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 95b9be7a326078588f76993b15110a060dfe737c
|
|
@ -1,41 +0,0 @@
|
|||
local tr = aegisub.gettext
|
||||
|
||||
script_name = tr"Karaoke split line"
|
||||
script_description = tr"split line at marker according to ktags"
|
||||
script_author = "amoethyst"
|
||||
script_version = "1.0"
|
||||
|
||||
function split_line(subs, sel)
|
||||
|
||||
function getduration(line)
|
||||
d = 0
|
||||
|
||||
kduration = "{[^}]-\\[kK][fo]?(%d+)[^}]-}"
|
||||
_, iend, match = line:find(kduration)
|
||||
while match do
|
||||
d = d + tonumber(match)
|
||||
line = line:sub(iend + 1) -- keep looking for more
|
||||
_, iend, match = line:find(kduration)
|
||||
end
|
||||
|
||||
return d * 10
|
||||
end
|
||||
|
||||
for _, i in ipairs(sel) do
|
||||
line1 = subs[i]
|
||||
line2 = subs[i]
|
||||
|
||||
line1.text, line2.text = line1.text:match("(.-){split}(.*)")
|
||||
|
||||
if line1.text ~= nil then
|
||||
line1.end_time = line1.start_time + getduration(line1.text)
|
||||
line2.start_time = line1.end_time
|
||||
end
|
||||
|
||||
subs[i] = line1
|
||||
subs.insert(i+1, line2)
|
||||
end
|
||||
aegisub.set_undo_point(tr"Karaoke split")
|
||||
end
|
||||
|
||||
aegisub.register_macro(script_name, script_description, split_line)
|
1
.aegisub/automation/autoload/karaoke-split.lua
Symbolic link
1
.aegisub/automation/autoload/karaoke-split.lua
Symbolic link
|
@ -0,0 +1 @@
|
|||
../amoegisub/karaoke-split.lua
|
221
.aegisub/automation/autoload/l0.DependencyControl.Toolbox.moon
Normal file
221
.aegisub/automation/autoload/l0.DependencyControl.Toolbox.moon
Normal file
|
@ -0,0 +1,221 @@
|
|||
export script_name = "DependencyControl Toolbox"
|
||||
export script_description = "Provides DependencyControl maintenance and configuration tools."
|
||||
export script_version = "0.1.3"
|
||||
export script_author = "line0"
|
||||
export script_namespace = "l0.DependencyControl.Toolbox"
|
||||
|
||||
DepCtrl = require "l0.DependencyControl"
|
||||
depRec = DepCtrl feed: "https://raw.githubusercontent.com/TypesettingTools/DependencyControl/master/DependencyControl.json"
|
||||
logger = DepCtrl.logger
|
||||
logger.usePrefixWindow = false
|
||||
|
||||
msgs = {
|
||||
install: {
|
||||
scanning: "Scanning %d available feeds..."
|
||||
}
|
||||
uninstall: {
|
||||
running: "Uninstalling %s '%s'..."
|
||||
success: "%s '%s' was removed sucessfully. Reload your automation scripts or restart Aegisub for the changes to take effect."
|
||||
lockedFiles: "%s Some script files are still in use and will be deleted during the next restart/reload:\n%s"
|
||||
error: "Error: %s"
|
||||
}
|
||||
macroConfig: {
|
||||
hints: {
|
||||
customMenu: "Lets you sort your automation macros into submenus. Use / to denote submenu levels."
|
||||
userFeed: "When set the updater will use this feed exclusively to update the script in question."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-- Shared Functions
|
||||
|
||||
buildInstalledDlgList = (scriptType, config, isUninstall) ->
|
||||
list, map, protectedModules = {}, {}, {}
|
||||
if isUninstall
|
||||
protectedModules[mdl.moduleName] = true for mdl in *DepCtrl.version.requiredModules
|
||||
protectedModules[DepCtrl.version.moduleName] = true
|
||||
|
||||
for namespace, script in pairs config.c[scriptType]
|
||||
continue if protectedModules[namespace]
|
||||
item = "%s v%s%s"\format script.name, depRec\getVersionString(script.version),
|
||||
script.activeChannel and " [#{script.activeChannel}]" or ""
|
||||
list[#list+1] = item
|
||||
table.sort list, (a, b) -> a\lower! < b\lower!
|
||||
map[item] = script
|
||||
return list, map
|
||||
|
||||
getConfig = (section) ->
|
||||
config = DepCtrl.config\getSectionHandler section
|
||||
config.c.macros or= {} if not section or #section == 0
|
||||
return config
|
||||
|
||||
getKnownFeeds = (config) ->
|
||||
getScriptFeeds = (t) -> [v.userFeed or v.feed for _,v in pairs config.c[t] when v.feed or v.userFeed]
|
||||
|
||||
-- fetch all feeds and look for further known feeds
|
||||
recurse = (feeds, knownFeeds = {}, feedList = {}) ->
|
||||
for url in *feeds
|
||||
feed = DepCtrl.UpdateFeed url
|
||||
continue if knownFeeds[url] or not feed.data
|
||||
feedList[#feedList+1], knownFeeds[url] = feed, true
|
||||
recurse feed\getKnownFeeds!, knownFeeds, feedList
|
||||
return knownFeeds, feedList
|
||||
|
||||
-- get additional feeds added by the user
|
||||
knownFeeds, feedList = recurse DepCtrl.config.c.extraFeeds
|
||||
-- collect feeds from all installed automation scripts and modules
|
||||
recurse getScriptFeeds("modules"), knownFeeds, feedList
|
||||
recurse getScriptFeeds("macros"), knownFeeds, feedList
|
||||
|
||||
return feedList
|
||||
|
||||
getScriptListDlg = (macros, modules) ->
|
||||
{
|
||||
{label: "Automation Scripts: ", class: "label", x: 0, y: 0, width: 1, height: 1 },
|
||||
{name: "macro", class: "dropdown", x: 1, y: 0, width: 1, height: 1, items: macros, value: "" },
|
||||
{label: "Modules: ", class: "label", x: 0, y: 1, width: 1, height: 1 },
|
||||
{name: "module", class: "dropdown", x: 1, y: 1, width: 1, height: 1, items: modules, value: "" }
|
||||
}
|
||||
|
||||
runUpdaterTask = (scriptData, exhaustive) ->
|
||||
return unless scriptData
|
||||
task, err = DepCtrl.updater\addTask scriptData, nil, nil, exhaustive, scriptData.channel
|
||||
if task then task\run!
|
||||
else logger\log err
|
||||
|
||||
|
||||
-- Macros
|
||||
|
||||
install = ->
|
||||
config = getConfig!
|
||||
|
||||
addAvailableToInstall = (tbl, feed, scriptType) ->
|
||||
for namespace, data in pairs feed.data[scriptType]
|
||||
scriptData = feed\getScript namespace, scriptType == "modules", nil, false
|
||||
channels, defaultChannel = scriptData\getChannels!
|
||||
tbl[namespace] or= {}
|
||||
for channel in *channels
|
||||
record = scriptData.data.channels[channel]
|
||||
verNum = depRec\getVersionNumber record.version
|
||||
unless config.c[scriptType][namespace] or (tbl[namespace][channel] and verNum < tbl[namespace][channel].verNum)
|
||||
tbl[namespace][channel] = { name: scriptData.name, version: record.version, verNum: verNum, feed: feed.url,
|
||||
default: defaultChannel == channel, moduleName: scriptType == "modules" and namespace }
|
||||
return tbl
|
||||
|
||||
buildDlgList = (tbl) ->
|
||||
list, map = {}, {}
|
||||
for namespace, channels in pairs tbl
|
||||
for channel, rec in pairs channels
|
||||
item = "%s v%s%s"\format rec.name, rec.version, rec.default and "" or " [#{channel}]"
|
||||
list[#list+1] = item
|
||||
table.sort list, (a, b) -> a\lower! < b\lower!
|
||||
map[item] = { :namespace, :channel, feed: rec.feed, name: rec.name, virtual: true,
|
||||
moduleName: rec.moduleName }
|
||||
|
||||
return list, map
|
||||
|
||||
-- get a list of the highest versions of automation scripts and modules
|
||||
-- we can install but wich are not yet installed
|
||||
macros, modules, feeds = {}, {}, getKnownFeeds config
|
||||
|
||||
logger\log msgs.install.scanning, #feeds
|
||||
for feed in *feeds
|
||||
macros = addAvailableToInstall macros, feed, "macros"
|
||||
modules = addAvailableToInstall modules, feed, "modules"
|
||||
|
||||
-- build macro and module lists as well as reverse mappings
|
||||
moduleList, moduleMap = buildDlgList modules
|
||||
macroList, macroMap = buildDlgList macros
|
||||
|
||||
btn, res = aegisub.dialog.display getScriptListDlg macroList, moduleList
|
||||
return unless btn
|
||||
|
||||
-- create and run the update tasks
|
||||
macro, mdl = macroMap[res.macro], moduleMap[res.module]
|
||||
runUpdaterTask mdl, false
|
||||
runUpdaterTask macro, false
|
||||
|
||||
uninstall = ->
|
||||
doUninstall = (script) ->
|
||||
return unless script
|
||||
scriptType = script.moduleName and "Module" or "Macro"
|
||||
logger\log msgs.uninstall.running, scriptType, script.name
|
||||
success, details = DepCtrl(script)\uninstall!
|
||||
if success == nil
|
||||
if "table" == type details
|
||||
-- error may be a string or a file list
|
||||
details = table.concat ["#{path}: #{res[2]}" for path, res in pairs details when res[1] == nil], "\n"
|
||||
logger\log msgs.uninstall.error, details
|
||||
else
|
||||
msg = msgs.uninstall.success\format scriptType, script.name
|
||||
logger\log if success
|
||||
msg
|
||||
else
|
||||
fileList = table.concat ["#{path} (#{res[2]})" for path, res in pairs details when res[1] != true], "\n"
|
||||
msgs.uninstall.lockedFiles\format msg, fileList
|
||||
|
||||
return success
|
||||
|
||||
config = getConfig!
|
||||
|
||||
-- build macro and module lists as well as reverse mappings
|
||||
moduleList, moduleMap = buildInstalledDlgList "modules", config, true
|
||||
macroList, macroMap = buildInstalledDlgList "macros", config, true
|
||||
|
||||
btn, res = aegisub.dialog.display getScriptListDlg macroList, moduleList
|
||||
return unless btn
|
||||
|
||||
macro, mdl = macroMap[res.macro], moduleMap[res.module]
|
||||
doUninstall mdl
|
||||
doUninstall macro
|
||||
|
||||
update = ->
|
||||
config = getConfig!
|
||||
|
||||
-- build macro and module lists as well as reverse mappings
|
||||
moduleList, moduleMap = buildInstalledDlgList "modules", config
|
||||
macroList, macroMap = buildInstalledDlgList "macros", config
|
||||
|
||||
dlg = getScriptListDlg macroList, moduleList
|
||||
dlg[5] = {name: "exhaustive", label: "Exhaustive Mode", class: "checkbox", x: 0, y: 2, width: 1, height: 1}
|
||||
btn, res = aegisub.dialog.display dlg
|
||||
return unless btn
|
||||
|
||||
-- create and run the update tasks
|
||||
macro, mdl = macroMap[res.macro], moduleMap[res.module]
|
||||
runUpdaterTask mdl, res.exhaustive
|
||||
runUpdaterTask macro, res.exhaustive
|
||||
|
||||
macroConfig = ->
|
||||
config = getConfig "macros"
|
||||
|
||||
dlg, i = {}, 1
|
||||
for nsp, macro in pairs config.userConfig
|
||||
dlg[i*5+t-1] = tbl for t, tbl in ipairs {
|
||||
{label: macro.name, class: "label", x: 0, y: i, width: 1, height: 1 },
|
||||
{label: "Menu Group: ", class: "label", x: 1, y: i, width: 1, height: 1 },
|
||||
{name: "#{nsp}.customMenu", class: "edit", x: 2, y: i, width: 1, height: 1,
|
||||
text: macro.customMenu or "", hint: msgs.macroConfig.hints.customMenu },
|
||||
{label: "Custom Update Feed: ", class: "label", x: 3, y: i, width: 1, height: 1 },
|
||||
{name: "#{nsp}.userFeed", class: "edit", x: 4, y: i, width: 1, height: 1,
|
||||
text: macro.userFeed or "", hint: msgs.macroConfig.hints.userFeed }
|
||||
}
|
||||
i += 1
|
||||
btn, res = aegisub.dialog.display dlg
|
||||
return unless btn
|
||||
|
||||
for k, v in pairs res
|
||||
nsp, prop = k\match "(.+)%.(.+)"
|
||||
if config.c[nsp][prop] and v == ""
|
||||
config.c[nsp][prop] = nil
|
||||
elseif v != ""
|
||||
config.c[nsp][prop] = v
|
||||
|
||||
config\write!
|
||||
|
||||
depRec\registerMacros{
|
||||
{"Install Script", "Installs an automation script or module on your system.", install},
|
||||
{"Update Script", "Manually check and perform updates to any installed script.", update},
|
||||
{"Uninstall Script", "Removes an automation script or module from your system.", uninstall},
|
||||
{"Macro Configuration", "Lets you change per-automation script settings.", macroConfig},
|
||||
}, "DependencyControl"
|
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)
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.BlurAndGlow.lua
|
523
.aegisub/automation/autoload/ua.BlurAndGlow.lua
Normal file
523
.aegisub/automation/autoload/ua.BlurAndGlow.lua
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.ChangeCase.lua
|
148
.aegisub/automation/autoload/ua.ChangeCase.lua
Normal file
148
.aegisub/automation/autoload/ua.ChangeCase.lua
Normal file
|
@ -0,0 +1,148 @@
|
|||
script_name="Change Case"
|
||||
script_description="Capitalises text or makes it lowercase / uppercase"
|
||||
script_author="unanimated"
|
||||
script_version="3.0"
|
||||
script_namespace="ua.ChangeCase"
|
||||
|
||||
local haveDepCtrl,DependencyControl,depRec=pcall(require,"l0.DependencyControl")
|
||||
if haveDepCtrl then
|
||||
script_version="3.0.0"
|
||||
depRec=DependencyControl{feed="https://raw.githubusercontent.com/TypesettingTools/unanimated-Aegisub-Scripts/master/DependencyControl.json"}
|
||||
end
|
||||
|
||||
re=require'aegisub.re'
|
||||
unicode=require'aegisub.unicode'
|
||||
|
||||
function case(subs,sel)
|
||||
for z,i in ipairs(sel) do
|
||||
line=subs[i]
|
||||
t=line.text
|
||||
if P=="lowercase" then t=lowercase(t) end
|
||||
if P=="UPPERCASE" then t=uppercase(t) end
|
||||
if P=="Lines" then t=capitalines(t) end
|
||||
if P=="Sentences" then
|
||||
if res.mod then res.mod=false t=lowercase(t) res.mod=true end
|
||||
t=sentences(t)
|
||||
end
|
||||
if P=="Words" then
|
||||
if not res.mod then t=lowercase(t) end
|
||||
t=capitalise(t)
|
||||
end
|
||||
line.text=t
|
||||
subs[i]=line
|
||||
end
|
||||
end
|
||||
|
||||
function lowercase(t)
|
||||
t=t
|
||||
:gsub("\\[Nnh]","{%1}")
|
||||
:gsub("^([^{]*)",function(l)
|
||||
if res.mod then l=re.sub(l,[[\b(\u\u+'?\u*)]],function(u) return ulower(u) end) return l
|
||||
else return ulower(l) end end)
|
||||
:gsub("}([^{]*)",function(l)
|
||||
if res.mod then l=re.sub(l,[[\b(\u\u+'?\u*)]],function(u) return ulower(u) end) return "}"..l
|
||||
else return "}"..ulower(l) end end)
|
||||
:gsub("{(\\[Nnh])}","%1")
|
||||
return t
|
||||
end
|
||||
|
||||
function uppercase(t)
|
||||
t=t
|
||||
:gsub("\\[Nnh]","{%1}")
|
||||
:gsub("^([^{]*)",function(u) return uupper(u) end)
|
||||
:gsub("}([^{]*)",function(u) return "}"..uupper(u) end)
|
||||
:gsub("{(\\[Nnh])}","%1")
|
||||
return t
|
||||
end
|
||||
|
||||
function capitalines(t)
|
||||
t=re.sub(t,[[^(["']?\l)]],function(l) return uupper(l) end)
|
||||
t=re.sub(t,[[^\{[^}]*\}(["']?\l)]],function(l) return uupper(l) end)
|
||||
if not res.mod then
|
||||
t=t:gsub(" i([' %?!%.,])"," I%1"):gsub("\\Ni([' ])","\\NI%1")
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function sentences(t)
|
||||
somewords={"English","Japanese","American","British","German","French","Spanish","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday","January","February","April","June","July","August","September","October","November","December"}
|
||||
hnrfx={"%-san","%-kun","%-chan","%-sama","%-dono","%-se[nm]pai","%-on%a+an"}
|
||||
t=re.sub(t,[[^(["']?\l)]],function(l) return uupper(l) end)
|
||||
t=re.sub(t,[[^\{[^}]*\}(["']?\l)]],function(l) return uupper(l) end)
|
||||
t=re.sub(t,[[[\.\?!](\s|\s\\N|\\N)["']?(\l)]],function(l) return uupper(l) end)
|
||||
t=t
|
||||
:gsub(" i([' %?!%.,])"," I%1")
|
||||
:gsub("\\Ni([' ])","\\NI%1")
|
||||
:gsub(" m(arch %d)"," M%1")
|
||||
:gsub(" a(pril %d)"," A%1")
|
||||
for l=1,#somewords do t=t:gsub(somewords[l]:lower(),somewords[l]) end
|
||||
for h=1,#hnrfx do
|
||||
t=t:gsub("([ %p]%l)(%l*"..hnrfx[h]..")",function(h,f) return h:upper()..f end)
|
||||
t=t:gsub("(\\N%l)(%l*"..hnrfx[h]..")",function(h,f) return h:upper()..f end)
|
||||
end
|
||||
t=re.sub(t,"\\b(of|in|from|\\d+st|\\d+nd|\\d+rd|\\d+th) m(arch|ay)\\b","\\1 M\\2")
|
||||
t=re.sub(t,"\\bm(r|rs|s)\\.","M\\1.")
|
||||
t=re.sub(t,"\\bdr\\.","Dr.")
|
||||
return t
|
||||
end
|
||||
|
||||
function capitalise(txt)
|
||||
word={"A","About","Above","Across","After","Against","Along","Among","Amongst","An","And","Around","As","At","Before","Behind","Below","Beneath","Beside","Between","Beyond","But","By","Despite","During","Except","For","From","In","Inside","Into","Near","Nor","Of","On","Onto","Or","Over","Per","Sans","Since","Than","The","Through","Throughout","Till","To","Toward","Towards","Under","Underneath","Unlike","Until","Unto","Upon","Versus","Via","With","Within","Without","According to","Ahead of","Apart from","Aside from","Because of","Inside of","Instead of","Next to","Owing to","Prior to","Rather than","Regardless of","Such as","Thanks to","Up to","and Yet"}
|
||||
onore={"%-San","%-Kun","%-Chan","%-Sama","%-Dono","%-Se[nm]pai","%-On%a+an"}
|
||||
nokom={"^( ?)([^{]*)","(})([^{]*)"}
|
||||
for n=1,2 do
|
||||
txt=txt:gsub(nokom[n],function(no_t,t)
|
||||
t=t:gsub("\\[Nnh]","{%1}")
|
||||
t=re.sub(t,[[\b\l]],function(l) return uupper(l) end)
|
||||
t=re.sub(t,[[[I\l]'(\u)]],function(l) return ulower(l) end)
|
||||
|
||||
for r=1,#word do w=word[r]
|
||||
t=t
|
||||
:gsub("^ "..w.." "," "..w:lower().." ")
|
||||
:gsub("([^%.:%?!]) "..w.." ","%1 "..w:lower().." ")
|
||||
:gsub("([^%.:%?!]) (%b{})"..w.." ","%1 %2"..w:lower().." ")
|
||||
:gsub("([^%.:%?!]) (%*Large_break%* ?)"..w.." ","%1 %2"..w:lower().." ")
|
||||
end
|
||||
|
||||
-- Roman numbers (this may mismatch some legit words - sometimes there just are 2 options and it's a guess)
|
||||
t=t
|
||||
:gsub("$","#")
|
||||
:gsub("(%s?)([IVXLCDM])([ivxlcdm]+)([%s%p#])",function(s,r,m,e) return s..r..m:upper()..e end)
|
||||
:gsub("([DLM])ID","%1id")
|
||||
:gsub("DIM","Dim")
|
||||
:gsub("MIX","Mix")
|
||||
:gsub("Ok([%s%p#])","OK%1")
|
||||
for h=1,#onore do
|
||||
t=t:gsub(onore[h].."([%s%p#])",onore[h]:lower().."%1")
|
||||
end
|
||||
t=t
|
||||
:gsub("#$","")
|
||||
:gsub("{(\\[Nnh])}","%1")
|
||||
return no_t..t end)
|
||||
end
|
||||
return txt
|
||||
end
|
||||
|
||||
ulower=unicode.to_lower_case
|
||||
uupper=unicode.to_upper_case
|
||||
|
||||
function logg(m) m=m or "nil" aegisub.log("\n "..m) end
|
||||
|
||||
function capital(subs,sel)
|
||||
GUI={
|
||||
{x=1,y=0,class="label",label="Words - Capitalise Words Like in Titles"},
|
||||
{x=1,y=1,class="label",label=" Lines - Capitalise first word in selected lines"},
|
||||
{x=1,y=2,class="label",label=" Sentences - Capitalise first word in each sentence"},
|
||||
{x=1,y=3,class="label",label=" Lowercase - make text in selected lines lowercase"},
|
||||
{x=1,y=4,class="label",label=" Uppercase - MAKE TEXT IN SELECTED LINES UPPERCASE"},
|
||||
{x=2,y=5,class="label",label=script_name.." v "..script_version},
|
||||
{x=1,y=5,class="checkbox",name="mod",label="mod",hint="Words - leave uppercase words\nLines - don't capitalize 'i'\nSentences - run lowercase first\nlowercase - only for uppercase words"},
|
||||
}
|
||||
P,res=aegisub.dialog.display(GUI,{"Words","Lines","Sentences","lowercase","UPPERCASE","Cancel"},{ok='Words',close='Cancel'})
|
||||
if P=="Cancel" then aegisub.cancel() end
|
||||
case(subs,sel)
|
||||
aegisub.set_undo_point(script_name)
|
||||
return sel
|
||||
end
|
||||
|
||||
if haveDepCtrl then depRec:registerMacro(capital) else aegisub.register_macro(script_name,script_description,capital) end
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Colorize.lua
|
1422
.aegisub/automation/autoload/ua.Colorize.lua
Normal file
1422
.aegisub/automation/autoload/ua.Colorize.lua
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Cycles.lua
|
116
.aegisub/automation/autoload/ua.Cycles.lua
Normal file
116
.aegisub/automation/autoload/ua.Cycles.lua
Normal file
|
@ -0,0 +1,116 @@
|
|||
-- Manual: http://unanimated.hostfree.pw/ts/scripts-manuals.htm#cycle
|
||||
|
||||
script_name="Cycles"
|
||||
script_description="Cycles blur, border, shadow, alpha, alignment, font spacing"
|
||||
script_author="unanimated"
|
||||
script_version="2.0"
|
||||
script_namespace="ua.Cycles"
|
||||
|
||||
local haveDepCtrl,DependencyControl,depRec=pcall(require,"l0.DependencyControl")
|
||||
if haveDepCtrl then
|
||||
script_version="2.0.0"
|
||||
depRec=DependencyControl{feed="https://raw.githubusercontent.com/unanimated/luaegisub/master/DependencyControl.json"}
|
||||
end
|
||||
|
||||
-- SETTINGS - You can change these sequences
|
||||
blur_sequence={"0.6","0.8","1","1.2","1.5","2","2.5","3","4","5","6","8","10","0.4","0.5"}
|
||||
bord_sequence={"0","1","1.5","2","2.5","3","4","5","6","7","8","9","10","11","12","15","20"}
|
||||
shad_sequence={"0","1","1.5","2","2.5","3","4","5","6","7","8","9","10","11","12"}
|
||||
alpha_sequence={"FF","00","10","30","60","80","A0","C0","E0"}
|
||||
align_sequence={"1","2","3","4","5","6","7","8","9"}
|
||||
fsp_sequence={"0","1","2","3","4","5","6","7","8","10","12","15","20","30"}
|
||||
|
||||
--[[ Adding more tags
|
||||
You could make this also work for the following tags: frz, frx, fry, fax, fay, fs, fscx, fscy, be, xbord, xshad, ybord, yshad
|
||||
by doing 3 things:
|
||||
1. add a new sequence to the settings above for the tag you want to add
|
||||
2. add a function below here based on what the others look like (it's adjusted for negative values too)
|
||||
3. add "aegisub.register_macro("Cycles/YOUR_SCRIPT_NAME","Cycles WHATEVER_YOU_CHOOSE",FUNCTION_NAME_HERE)" at the end of the script
|
||||
If you at least roughly understand the basics, this should be easy. The main cycle function remains the same for all tags.
|
||||
Should you want to add other tags with different value patterns, check the existing exceptions for alpha in the cycle function.]]
|
||||
|
||||
function blur(subs,sel) cycle(subs,sel,"blur",blur_sequence) end
|
||||
function bord(subs,sel) cycle(subs,sel,"bord",bord_sequence) end
|
||||
function shad(subs,sel) cycle(subs,sel,"shad",shad_sequence) end
|
||||
function alph(subs,sel) cycle(subs,sel,"alpha",alpha_sequence) end
|
||||
function algn(subs,sel) cycle(subs,sel,"an",align_sequence) end
|
||||
function fsp(subs,sel) cycle(subs,sel,"fsp",fsp_sequence) end
|
||||
|
||||
function cycle(subs,sel,tag,sequence)
|
||||
if tag=="alpha" then base=16 else base=10 end
|
||||
for z,i in ipairs(sel) do
|
||||
line=subs[i]
|
||||
text=line.text
|
||||
local back
|
||||
if line.comment or text:match'{switch}$' then back=true end
|
||||
text=text:gsub("\\t(%b())",function(t) return "\\t"..t:gsub("\\","|") end)
|
||||
|
||||
if tag=="alpha" then val1=text:match("^{[^}]-\\alpha&H(%x%x)&") else val1=text:match("^{[^}]-\\"..tag.."(%-?[%d%.]+)") end
|
||||
if val1 then
|
||||
for n=1,#sequence do
|
||||
N=n+1
|
||||
if back then N=n-1 end
|
||||
if N==0 then N=#sequence end
|
||||
if val1==sequence[n] then val2=sequence[N] or sequence[1] break end
|
||||
end
|
||||
if val2==nil then
|
||||
for n=1,#sequence do
|
||||
if n>1 or sequence[1]~="FF" then
|
||||
local N=n
|
||||
if back then N=n-1 end
|
||||
if N==0 then N=#sequence end
|
||||
if tonumber(val1,base)<tonumber(sequence[n],base) then val2=sequence[N] break end
|
||||
end
|
||||
end
|
||||
end
|
||||
if val2==nil then if back then val2=sequence[#sequence] else val2=sequence[1] end end
|
||||
if tag=="alpha" then
|
||||
text=text:gsub("^({[^}]-\\alpha&H)%x%x","%1"..val2)
|
||||
else
|
||||
text=text:gsub("^({[^}]-\\"..tag..")%-?[%d%.]+","%1"..val2)
|
||||
end
|
||||
val2=nil
|
||||
else
|
||||
text="{\\"..tag..sequence[1].."}"..text
|
||||
text=text:gsub("alpha(%x%x)}","alpha&H%1&}")
|
||||
:gsub("{(\\.-)}{\\","{%1\\")
|
||||
end
|
||||
|
||||
text=text:gsub("{\\[^}]-}",function(t) return t:gsub("|","\\") end)
|
||||
line.text=text
|
||||
subs[i]=line
|
||||
end
|
||||
end
|
||||
|
||||
function switch(subs,sel)
|
||||
for z,i in ipairs(sel) do
|
||||
l=subs[i]
|
||||
t=l.text
|
||||
t=t.."{switch}"
|
||||
t=t:gsub("{switch}{switch}$","")
|
||||
l.text=t
|
||||
subs[i]=l
|
||||
end
|
||||
end
|
||||
|
||||
function logg(m) m=m or "nil" aegisub.log("\n "..m) end
|
||||
|
||||
if haveDepCtrl then
|
||||
depRec:registerMacros({
|
||||
{"Cycles/Blur Cycle","Cycles Blur",blur},
|
||||
{"Cycles/Border Cycle","Cycles Border",bord},
|
||||
{"Cycles/Shadow Cycle","Cycles Shadow",shad},
|
||||
{"Cycles/Alpha Cycle","Cycles Alpha",alph},
|
||||
{"Cycles/Alignment Cycle","Cycles Alignment",algn},
|
||||
{"Cycles/FontSpacing Cycle","Cycles Font Spacing",fsp},
|
||||
{"Cycles/Switch","Switches sequence direction",switch},
|
||||
},false)
|
||||
else
|
||||
aegisub.register_macro("Cycles/Blur Cycle","Cycles Blur",blur)
|
||||
aegisub.register_macro("Cycles/Border Cycle","Cycles Border",bord)
|
||||
aegisub.register_macro("Cycles/Shadow Cycle","Cycles Shadow",shad)
|
||||
aegisub.register_macro("Cycles/Alpha Cycle","Cycles Alpha",alph)
|
||||
aegisub.register_macro("Cycles/Alignment Cycle","Cycles Alignment",algn)
|
||||
aegisub.register_macro("Cycles/FontSpacing Cycle","Cycles Font Spacing",fsp)
|
||||
aegisub.register_macro("Cycles/Switch","Switches sequence direction",switch)
|
||||
end
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.EncodeHardsub.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.FadeWorks.lua
|
1407
.aegisub/automation/autoload/ua.FadeWorks.lua
Normal file
1407
.aegisub/automation/autoload/ua.FadeWorks.lua
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.HYDRA.lua
|
2064
.aegisub/automation/autoload/ua.HYDRA.lua
Normal file
2064
.aegisub/automation/autoload/ua.HYDRA.lua
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.JoinSplitSnap.lua
|
320
.aegisub/automation/autoload/ua.JoinSplitSnap.lua
Normal file
320
.aegisub/automation/autoload/ua.JoinSplitSnap.lua
Normal file
|
@ -0,0 +1,320 @@
|
|||
-- Manual: http://unanimated.xtreemhost.com/ts/scripts-manuals.htm#join
|
||||
|
||||
-- JOIN
|
||||
-- joins selected lines or one selected line with the following line. it's a combination of "Join (concatenate)" and "Join (keep first)"
|
||||
-- if the text (without tags) is the same on all lines, then it's "keep first"
|
||||
-- if different, it's "concatenate" for 2 lines, but it nukes some redundant tags from the 2nd line
|
||||
-- if it's more than 2 lines with different text, you get to choose to join them with only tags from the first one, or from all
|
||||
-- when keeping tags, it nukes ones that should only be once in a line plus some others detected as redundant (not very sophisticated)
|
||||
-- set a simple hotkey to use when timing
|
||||
|
||||
-- SPLIT
|
||||
-- splits a line at linebreak (use together with Line Breaker with simple hotkeys under Subtitle Grid)
|
||||
-- it's similar to "Split at cursor (estimate times)", but uses \N as "cursor"
|
||||
-- compared to the inbuilt tool, the times estimation works better, you keep tags for both resulting lines, and it snaps to keyframes (6-frame range)
|
||||
|
||||
-- SNAP
|
||||
-- snaps to keyframes or adjacent lines based on the settings below
|
||||
|
||||
|
||||
-- KF SNAPPING SETTINGS
|
||||
|
||||
kfsb=6 -- starts before
|
||||
kfeb=10 -- ends before
|
||||
kfsa=8 -- starts after
|
||||
kfea=15 -- ends after
|
||||
|
||||
-- END OF SETTINGS
|
||||
|
||||
script_name="Join / Split / Snap"
|
||||
script_description="Joins lines / splits lines / snaps to keyframes"
|
||||
script_author="unanimated"
|
||||
script_version="1.2"
|
||||
script_namespace="ua.JoinSplitSnap"
|
||||
|
||||
local haveDepCtrl,DependencyControl,depRec=pcall(require,"l0.DependencyControl")
|
||||
if haveDepCtrl then
|
||||
script_version="1.2.0"
|
||||
depRec=DependencyControl{feed="https://raw.githubusercontent.com/TypesettingTools/unanimated-Aegisub-Scripts/master/DependencyControl.json"}
|
||||
end
|
||||
|
||||
function join(subs,sel)
|
||||
go=0
|
||||
morethan2=nil
|
||||
if #sel>1 then
|
||||
go=1 st=10000000 et=0
|
||||
for z,i in ipairs(sel) do
|
||||
l=subs[i] t=l.text ct=t:gsub("%b{}","")
|
||||
stm=l.start_time etm=l.end_time
|
||||
if stm<st then st=stm end
|
||||
if etm>et then et=etm end
|
||||
if z>1 and ct~=ref then go=0 end
|
||||
ref=ct
|
||||
end
|
||||
end
|
||||
if go==1 then
|
||||
l=subs[sel[1]]
|
||||
l.start_time=st l.end_time=et
|
||||
subs[sel[1]]=l
|
||||
for i=#sel,2,-1 do subs.delete(sel[i]) end
|
||||
sel={sel[1]}
|
||||
else
|
||||
if sel[1]==#subs then aegisub.log("Nothing to join with.") aegisub.cancel() end
|
||||
if #sel==1 then table.insert(sel,sel[1]+1) end
|
||||
if #sel>2 then morethan2=true
|
||||
dialog={{class="label",label="Join "..#sel.." lines with different text?\n\nOption 1: join, keeping only tags from first line\n\nOption 2: join and try to preserve relevant tags"}}
|
||||
buttons={"Join, no tags","Join, with tags","Cancel"}
|
||||
press=aegisub.dialog.display(dialog,buttons,{close='Cancel'})
|
||||
if press=="Join, no tags" then nt="" et=0
|
||||
for i=2,#sel do
|
||||
l=subs[sel[i]] t=l.text:gsub("%b{}",""):gsub(" *\\N *"," ") nt=nt.." "..t
|
||||
if l.end_time>et then et=l.end_time end
|
||||
end
|
||||
l=subs[sel[1]] l.text=l.text:gsub(" *\\N *"," ")..nt l.end_time=et subs[sel[1]]=l
|
||||
for i=#sel,2,-1 do subs.delete(sel[i]) table.remove(sel,i) end
|
||||
end
|
||||
if press=="Join, with tags" then
|
||||
repeat join2(subs,sel) until #sel==1
|
||||
end
|
||||
else
|
||||
join2(subs,sel)
|
||||
end
|
||||
end
|
||||
aegisub.set_undo_point(script_name)
|
||||
return sel
|
||||
end
|
||||
|
||||
function join2(subs,sel)
|
||||
l=subs[sel[1]] t=l.text ct=t:gsub("%b{}","")
|
||||
l2=subs[sel[2]] t2=l2.text ct2=t2:gsub("%b{}","") ct3=t2:gsub("^{[^}]-}",""):gsub("^%- ","")
|
||||
if ct~=ct2 or morethan2 then
|
||||
t=t:gsub("{[Jj][Oo][Ii][Nn]}%s*$","")
|
||||
tt=t:match("^{\\[^}]-}") or ""
|
||||
tt2=t2:match("^{\\[^}]-}") or ""
|
||||
tt1=tt:gsub("\\an?%d",""):gsub("\\%a%a+%b()",""):gsub("\\q%d",""):gsub("\\t%([^\\]*%)",""):gsub("{}","")
|
||||
tt2=tt2:gsub("\\an?%d",""):gsub("\\%a%a+%b()",""):gsub("\\q%d",""):gsub("\\t%([^\\]*%)",""):gsub("{}","")
|
||||
if tt==tt2 then t=t.." "..ct3
|
||||
elseif not tt1:match("\\t") and not tt2:match("\\t") then
|
||||
for tag in tt1:gmatch("\\[^\\}]+") do
|
||||
tt2=tt2:gsub(esc(tag),"")
|
||||
end
|
||||
t=t.." "..tt2..ct3
|
||||
else
|
||||
t=t.." "..tt2..ct3
|
||||
end
|
||||
t=t:gsub("%.%.%. %.%.%."," "):gsub("(%a%-) (%a)","%1%2"):gsub("\" \""," "):gsub(" *\\N *"," ")
|
||||
:gsub("(\\i1.-)\\i(%d)",function(i,n) if n=="1" then return i else return i..n end end)
|
||||
:gsub("{}","")
|
||||
end
|
||||
if l2.end_time>l.end_time then l.end_time=l2.end_time end
|
||||
subs.delete(sel[2])
|
||||
l.text=t
|
||||
subs[sel[1]]=l
|
||||
if #sel>1 then table.remove(sel,2)
|
||||
if #sel>1 then for s=2,#sel do sel[s]=sel[s]-1 end end
|
||||
end
|
||||
return subs[sel[1]]
|
||||
end
|
||||
|
||||
function split(subs,sel)
|
||||
for i=#sel,1,-1 do
|
||||
line=subs[sel[i]]
|
||||
text=line.text
|
||||
c=0
|
||||
|
||||
if not text:match("\\N") and sel[i]<#subs then
|
||||
nextline=subs[sel[i]+1]
|
||||
if text:match(" that$") then text=text:gsub(" that$","") nextline.text="that "..nextline.text c=1 end
|
||||
if text:match(" and$") then text=text:gsub(" and$","") nextline.text="and "..nextline.text c=1 end
|
||||
if text:match(" but$") then text=text:gsub(" but$","") nextline.text="but "..nextline.text c=1 end
|
||||
if text:match(" so$") then text=text:gsub(" so$","") nextline.text="so "..nextline.text c=1 end
|
||||
if text:match(" to$") then text=text:gsub(" to$","") nextline.text="to "..nextline.text c=1 end
|
||||
if text:match(" when$") then text=text:gsub(" when$","") nextline.text="when "..nextline.text c=1 end
|
||||
if text:match(" with$") then text=text:gsub(" with$","") nextline.text="with "..nextline.text c=1 end
|
||||
if text:match(" the$") then text=text:gsub(" the$","") nextline.text="the "..nextline.text c=1 end
|
||||
subs[sel[i]+1]=nextline
|
||||
end
|
||||
|
||||
if c==0 then
|
||||
text=text:gsub("{SPLIT}","{split}")
|
||||
if not text:match("\\N") and text:match("{split}") then text=text:gsub("{split}","\\N") end
|
||||
if not text:match("\\N") and text:match("%- ") then text=text:gsub("(.)%- (.-)$","%1\\N- %2") end
|
||||
if not text:match("\\N") and text:match("%. [{\\\"]?%w") then
|
||||
text=text
|
||||
:gsub("([MD][rs]s?)%. ","%1## ")
|
||||
:gsub("^(.-)%. ","%1. \\N")
|
||||
:gsub("## ",". ") end
|
||||
if not text:match("\\N") and text:match("[%?!] {?%w") then text=text:gsub("^(.-)([%?!]) ","%1%2 \\N") end
|
||||
if not text:match("\\N") and text:match(", {?%w") then text=text:gsub("^(.-), ","%1, \\N") end
|
||||
if text:match("\\N") and text:match("{split}") then text=text:gsub("\\N","/N"):gsub("{split}","\\N") end
|
||||
end
|
||||
|
||||
if not text:match("\\N") and not text:match(" ") then text=text.."\\N"..text end
|
||||
|
||||
if text:match("\\N") then
|
||||
text=text:gsub("^%- (.-\\N)%- ","%1"):gsub("^({\\i1})%- (.-\\N)%- ","%1%2"):gsub("({\\i1})%- ","%1"):gsub("{add}","")
|
||||
line2=line
|
||||
start=line.start_time
|
||||
endt=line.end_time
|
||||
dur=endt-start
|
||||
ms2fr=aegisub.frame_from_ms
|
||||
fr2ms=aegisub.ms_from_frame
|
||||
keyframes=aegisub.keyframes()
|
||||
startf=ms2fr(start)
|
||||
endf=ms2fr(endt)
|
||||
diff=250
|
||||
diffe=250
|
||||
startkf=keyframes[1]
|
||||
endkf=keyframes[#keyframes]
|
||||
txt=text:gsub("%b{}","")
|
||||
one,two=txt:match("^(.-)\\N(.*)")
|
||||
c1=one:len()
|
||||
c2=two:len()
|
||||
f=c1/(c1+c2)
|
||||
if dur<3200 then f=(f+0.5)/2 end
|
||||
if dur<2000 then f=0.5 end
|
||||
if f<0.2 then f=0.2 end
|
||||
if f>0.8 then f=0.8 end
|
||||
|
||||
-- line 2
|
||||
aftern=text:match("\\N%s*(.*)")
|
||||
tags=text:match("^{\\[^}]-}") if tags and not aftern:match("^{\\[^}]-}") then aftern=tags..aftern end
|
||||
line2.text=aftern:gsub("/N","\\N")
|
||||
line2.start_time=start+dur*f
|
||||
start2f=ms2fr(line2.start_time)
|
||||
for k,kf in ipairs(keyframes) do
|
||||
if kf>=start2f-6 and kf<=start2f+6 then
|
||||
tdiff=math.abs(start2f-kf)
|
||||
if tdiff<=diff then diff=tdiff startkf=kf end
|
||||
start2=fr2ms(startkf)
|
||||
line2.start_time=start2
|
||||
end
|
||||
end
|
||||
subs.insert(sel[i]+1,line2)
|
||||
|
||||
-- line 1
|
||||
text=text:gsub("^(.-)%s?\\N(.*)","%1"):gsub("/N","\\N")
|
||||
line.start_time=start
|
||||
line.end_time=start+dur*f
|
||||
end1f=ms2fr(line.end_time)
|
||||
for k,kf in ipairs(keyframes) do
|
||||
if kf>=end1f-12 and kf<=end1f+6 then
|
||||
tdiff=math.abs(end1f-kf)
|
||||
if tdiff<diffe then diffe=tdiff endkf=kf end
|
||||
endt=fr2ms(endkf)
|
||||
if endt-start>500 then line.end_time=endt end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
line.text=text
|
||||
subs[sel[i]]=line
|
||||
end
|
||||
for s=#sel,1,-1 do sel[s]=sel[s]+s-1 end
|
||||
aegisub.set_undo_point(script_name)
|
||||
return sel
|
||||
end
|
||||
|
||||
function keyframesnap(subs,sel)
|
||||
keyframes=aegisub.keyframes()
|
||||
ms2fr=aegisub.frame_from_ms
|
||||
fr2ms=aegisub.ms_from_frame
|
||||
if subs[sel[1]].effect=="gui" then
|
||||
gui={
|
||||
{x=0,y=0,class="label",label="Starts before "},
|
||||
{x=0,y=1,class="label",label="Ends before "},
|
||||
{x=0,y=2,class="label",label="Starts after "},
|
||||
{x=0,y=3,class="label",label="Ends after "},
|
||||
{x=1,y=0,class="floatedit",name="sb",value=6},
|
||||
{x=1,y=1,class="floatedit",name="eb",value=10},
|
||||
{x=1,y=2,class="floatedit",name="sa",value=8},
|
||||
{x=1,y=3,class="floatedit",name="ea",value=15}
|
||||
}
|
||||
buttons={"OK","Cancel"}
|
||||
pressed,res=aegisub.dialog.display(gui,buttons,{ok='OK',close='Cancel'})
|
||||
if pressed=="Cancel" then aegisub.cancel() end
|
||||
kfsb=res.sb
|
||||
kfeb=res.eb
|
||||
kfsa=res.sa
|
||||
kfea=res.ea
|
||||
end
|
||||
|
||||
for z,i in ipairs(sel) do
|
||||
line=subs[i]
|
||||
start=line.start_time
|
||||
endt=line.end_time
|
||||
startn=start
|
||||
endtn=endt
|
||||
startf=ms2fr(start)
|
||||
endf=ms2fr(endt)
|
||||
diff=250 diffe=250
|
||||
KS=0 KE=0
|
||||
startkf=keyframes[1]
|
||||
endkf=keyframes[#keyframes]
|
||||
|
||||
-- snap to keyframes
|
||||
for k,kf in ipairs(keyframes) do
|
||||
if kf>=startf-kfsa and kf<=startf+kfsb then
|
||||
sdiff=math.abs(startf-kf)
|
||||
if sdiff<=diff then diff=sdiff startkf=kf startn=fr2ms(startkf) KS=1 end
|
||||
end
|
||||
if kf>=endf-kfea and kf<=endf+kfeb then
|
||||
ediff=math.abs(endf-kf)
|
||||
if ediff<diffe then diffe=ediff endkf=kf endtn=fr2ms(endkf) KE=1 end
|
||||
end
|
||||
end
|
||||
|
||||
-- snap to adjacent lines
|
||||
if KS==0 then
|
||||
if subs[i-1].class=="dialogue" then
|
||||
l2=subs[i-1]
|
||||
prevend=l2.end_time
|
||||
pref=ms2fr(prevend)
|
||||
sdiff=startf-pref
|
||||
if sdiff<=kfsa and sdiff>0 or sdiff<0 and sdiff<=kfsb then
|
||||
startn=prevend
|
||||
l2.end_time=fr2ms(ms2fr(prevend))
|
||||
subs[i-1]=l2
|
||||
end
|
||||
end
|
||||
end
|
||||
if KE==0 then
|
||||
if subs[i+1] then
|
||||
l2=subs[i+1]
|
||||
nextart=l2.start_time
|
||||
nesf=ms2fr(nextart)
|
||||
ediff=nesf-endf
|
||||
if ediff<=kfea and ediff>0 or ediff<0 and ediff<=kfeb then
|
||||
endtn=nextart
|
||||
l2.start_time=fr2ms(ms2fr(nextart))
|
||||
subs[i+1]=l2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if startn==nil then startn=start end
|
||||
if endtn==nil then endtn=endt end
|
||||
if startn~=line.start_time then startn=fr2ms(ms2fr(startn)) end
|
||||
if endtn~=line.end_time then endtn=fr2ms(ms2fr(endtn)) end
|
||||
line.start_time=startn
|
||||
line.end_time=endtn
|
||||
subs[i]=line
|
||||
end
|
||||
aegisub.set_undo_point(script_name)
|
||||
return sel
|
||||
end
|
||||
|
||||
function esc(str) str=str:gsub("[%%%(%)%[%]%.%-%+%*%?%^%$]","%%%1") return str end
|
||||
function logg(m) m=m or "nil" aegisub.log("\n "..m) end
|
||||
|
||||
if haveDepCtrl then
|
||||
depRec:registerMacros({
|
||||
{"Join-Split-Snap/Join","Joins lines",join},
|
||||
{"Join-Split-Snap/Split","Splits Lines",split},
|
||||
{"Join-Split-Snap/Snap to keyframes","Snaps to nearby keyframes",keyframesnap},
|
||||
},false)
|
||||
else
|
||||
aegisub.register_macro("Join-Split-Snap/Join","Joins lines",join)
|
||||
aegisub.register_macro("Join-Split-Snap/Split","Splits Lines",split)
|
||||
aegisub.register_macro("Join-Split-Snap/Snap to keyframes","Snaps to nearby keyframes",keyframesnap)
|
||||
end
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.JumpToNext.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.LineBreaker.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Masquerade.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.MultiCopy.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.MultiLineEditor.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Multiplexer.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.NecrosCopy.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.QC.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Recalculator.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Relocator.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.ScriptCleanup.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Selectrix.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.ShiftCut.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.Significance.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.TimeSigns.lua
|
|
@ -1 +0,0 @@
|
|||
../unanimated/ua.iBus.lua
|
39
.aegisub/automation/include/BM/BadMutex.lua
Normal file
39
.aegisub/automation/include/BM/BadMutex.lua
Normal file
|
@ -0,0 +1,39 @@
|
|||
local ffi = require("ffi")
|
||||
local requireffi = require("requireffi.requireffi")
|
||||
ffi.cdef([[ void lock( void );
|
||||
bool try_lock( void );
|
||||
void unlock( void );
|
||||
unsigned int version( void );
|
||||
]])
|
||||
local BM, loadedLibraryPath = requireffi("BM.BadMutex.BadMutex")
|
||||
local BMVersion = 0x000100
|
||||
local libVer = BM.version()
|
||||
if libVer < BMVersion or math.floor(libVer / 65536 % 256) > math.floor(BMVersion / 65536 % 256) then
|
||||
error("Library version mismatch. Wanted " .. tostring(BMVersion) .. ", got " .. tostring(libVer) .. ".")
|
||||
end
|
||||
local BadMutex
|
||||
BadMutex = {
|
||||
lock = function()
|
||||
return BM.lock()
|
||||
end,
|
||||
tryLock = function()
|
||||
return BM.try_lock()
|
||||
end,
|
||||
unlock = function()
|
||||
return BM.unlock()
|
||||
end,
|
||||
version = 0x000103,
|
||||
__depCtrlInit = function(DependencyControl)
|
||||
BadMutex.version = DependencyControl({
|
||||
name = "BadMutex",
|
||||
version = BadMutex.version,
|
||||
description = "A global mutex.",
|
||||
author = "torque",
|
||||
url = "https://github.com/TypesettingTools/ffi-experiments",
|
||||
moduleName = "BM.BadMutex",
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/ffi-experiments/master/DependencyControl.json"
|
||||
})
|
||||
end,
|
||||
loadedLibraryPath = loadedLibraryPath
|
||||
}
|
||||
return BadMutex
|
420
.aegisub/automation/include/DM/DownloadManager.lua
Normal file
420
.aegisub/automation/include/DM/DownloadManager.lua
Normal file
|
@ -0,0 +1,420 @@
|
|||
local _ = [[---------- Usage ----------
|
||||
|
||||
1. Create a DownloadManager:
|
||||
|
||||
manager = DownloadManager!
|
||||
|
||||
You can supply a single library search path or a table of search paths for the DownloadManager library.
|
||||
On Aegisub, this will default to the system and user DownloadManager module directories.
|
||||
Otherwise the default library search path will be used.
|
||||
|
||||
2. Add some downloads:
|
||||
|
||||
manager\addDownload "https://a.real.website", "out3"
|
||||
|
||||
If you have a SHA-1 hash to check the downloaded file against, use:
|
||||
manager\addDownload "https://a.real.website", "out2", "b52854d1f79de5ebeebf0160447a09c7a8c2cde4"
|
||||
|
||||
You may want to keep a reference of your download to check its result later:
|
||||
myDownload = manager\addDownload "https://a.real.website", "out2",
|
||||
|
||||
Downloads will start immediately. Do whatever you want here while downloads happen in the background.
|
||||
The output file must contain a full path and file name. There is no working directory and automatic file naming is unsupported.
|
||||
|
||||
3. Wait for downloads to finish:
|
||||
|
||||
Call manager\waitForFinish(cb) to loop until remaining downloads finish.
|
||||
The progress callback can call manager\cancel! or manager\clear! to interrupt and break open connections.
|
||||
|
||||
The current overall progress will be passed to the provided callback as a number in range 0-100:
|
||||
manager\waitForFinish ( progress ) ->
|
||||
print tostring progress
|
||||
|
||||
4. Check for download errors:
|
||||
|
||||
Check a specific download:
|
||||
error dl.error if dl.error
|
||||
|
||||
Print all failed downloads:
|
||||
for dl in *manager.failedDownloads
|
||||
print "Download ##{dl.id} error: #{dl.error}"
|
||||
|
||||
Get a descriptive overall error message:
|
||||
error = table.concat ["#{dl.url}: #{dl.error}" for dl in *manager.failedDownloads], "\n"
|
||||
|
||||
5. Clear all downloads:
|
||||
|
||||
manager\clear!
|
||||
|
||||
Removes all downloads from the downloader and resets all counters.
|
||||
|
||||
|
||||
Error Handling:
|
||||
Errors are handled in typical Lua fashion.
|
||||
DownloadManager will only throw an error in case the library failed to load.
|
||||
Errors will also be thrown if the wrong type is passed in to certain functions to avoid
|
||||
missing incorrect usage.
|
||||
If any other error is encountered the script will return nil along with an error message.
|
||||
|
||||
]]
|
||||
local havelfs, lfs = pcall(require, "lfs")
|
||||
local ffi = require("ffi")
|
||||
local requireffi = require("requireffi.requireffi")
|
||||
ffi.cdef([[struct CDlM;
|
||||
typedef struct CDlM CDlM;
|
||||
typedef unsigned int uint;
|
||||
CDlM* CDlM_new ( void );
|
||||
uint CDlM_addDownload ( CDlM *mgr, const char *url,
|
||||
const char *outputFile, const char *expectedHash,
|
||||
const char *expectedETag );
|
||||
double CDlM_progress ( CDlM *mgr );
|
||||
int CDlM_busy ( CDlM *mgr );
|
||||
int CDlM_checkDownload ( CDlM *mgr, uint i );
|
||||
const char* CDlM_getError ( CDlM *mgr, uint i );
|
||||
_Bool CDlM_fileWasCached ( CDlM *mgr, uint i );
|
||||
const char* CDlM_getETag ( CDlM *mgr, uint i );
|
||||
void CDlM_terminate ( CDlM *mgr );
|
||||
void CDlM_clear ( CDlM *mgr );
|
||||
const char* CDlM_getFileSHA1 ( const char *filename );
|
||||
const char* CDlM_getStringSHA1 ( const char *string );
|
||||
uint CDlM_version ( void );
|
||||
void CDlM_freeDM ( CDlM *mgr );
|
||||
_Bool CDlM_isInternetConnected( void );
|
||||
int usleep(unsigned int);
|
||||
void Sleep(unsigned long);
|
||||
]])
|
||||
local DMVersion = 0x000400
|
||||
local DM, loadedLibraryPath = requireffi("DM.DownloadManager.DownloadManager")
|
||||
local libVer = DM.CDlM_version()
|
||||
if libVer < DMVersion or math.floor(libVer / 65536 % 256) > math.floor(DMVersion / 65536 % 256) then
|
||||
error("Library version mismatch. Wanted " .. tostring(DMVersion) .. ", got " .. tostring(libVer) .. ".")
|
||||
end
|
||||
local sleep = ffi.os == "Windows" and (function(ms)
|
||||
if ms == nil then
|
||||
ms = 100
|
||||
end
|
||||
return ffi.C.Sleep(ms)
|
||||
end) or (function(ms)
|
||||
if ms == nil then
|
||||
ms = 100
|
||||
end
|
||||
return ffi.C.usleep(ms * 1000)
|
||||
end)
|
||||
local msgs = {
|
||||
notInitialized = "%s not initialized.",
|
||||
addMissingArgs = "Required arguments #1 (url) or #2 (outfile) had the wrong type. Expected string, got '%s' and '%s'.",
|
||||
getMissingArg = "Required argument (filename) was either absent or the wrong type. Expected string, got '%s'.",
|
||||
checkMissingArgs = "Required arguments #1 (filename/string) and #2 (expected) or the wrong type. Expected string, got '%s' and '%s'.",
|
||||
outNoFullPath = "Argument #2 (outfile) must contain a full path (relative paths not supported), got %s.",
|
||||
outNoFile = "Argument #2 (outfile) must contain a full path with file name, got %s.",
|
||||
failedToOpen = "Could not open file %s.",
|
||||
hashMismatch = "Hash mismatch. Got %s, expected %s.",
|
||||
horriblyWrong = "Something has gone horribly wrong???",
|
||||
cacheFailure = "Couldn't use cache. Message: %s",
|
||||
failedToAdd = "Could not add download for some reason."
|
||||
}
|
||||
local sanitizeFile
|
||||
sanitizeFile = function(filename, acceptDir)
|
||||
do
|
||||
local homeDir = os.getenv("HOME")
|
||||
if homeDir then
|
||||
filename = filename:gsub("^~/", homeDir .. "/")
|
||||
end
|
||||
end
|
||||
local dev, dir, file = filename:match("^(" .. tostring(ffi.os == 'Windows' and '%a:[/\\]' or '/') .. ")(.*[/\\])(.*)$")
|
||||
if not dev or #dir < 1 then
|
||||
return nil, msgs.outNoFullPath:format(filename)
|
||||
elseif not acceptDir and #file < 1 then
|
||||
return nil, msgs.outNoFile:format(filename)
|
||||
end
|
||||
dir = dev .. dir
|
||||
if havelfs then
|
||||
local mode, err = lfs.attributes(dir, "mode")
|
||||
if mode ~= "directory" then
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
local res
|
||||
res, err = lfs.mkdir(dir)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
else
|
||||
os.execute("mkdir " .. tostring(ffi.os == 'Windows' and '' or '-p ') .. "\"" .. tostring(dir) .. "\"")
|
||||
end
|
||||
return dir .. file
|
||||
end
|
||||
local ETagCache
|
||||
do
|
||||
local _class_0
|
||||
local copyFile
|
||||
local _base_0 = {
|
||||
cachedFile = function(self, cacheName)
|
||||
return self.cacheDir .. cacheName
|
||||
end,
|
||||
cachedFileExists = function(self, cacheName)
|
||||
if cacheName == nil then
|
||||
return false
|
||||
end
|
||||
local file = io.open(self:cachedFile(cacheName), 'rb')
|
||||
if file == nil then
|
||||
return false
|
||||
end
|
||||
file:close()
|
||||
return true
|
||||
end,
|
||||
useCache = function(self, download)
|
||||
local err, msg = copyFile(self:cachedFile(download.etag), download.outfile)
|
||||
if err == nil then
|
||||
return err, msg
|
||||
end
|
||||
return true
|
||||
end,
|
||||
cache = function(self, download)
|
||||
local err, msg = copyFile(download.outfile, self:cachedFile(download.etag))
|
||||
if err == nil then
|
||||
return err, msg
|
||||
end
|
||||
return true
|
||||
end
|
||||
}
|
||||
_base_0.__index = _base_0
|
||||
_class_0 = setmetatable({
|
||||
__init = function(self, cacheDir)
|
||||
self.cacheDir = cacheDir
|
||||
end,
|
||||
__base = _base_0,
|
||||
__name = "ETagCache"
|
||||
}, {
|
||||
__index = _base_0,
|
||||
__call = function(cls, ...)
|
||||
local _self_0 = setmetatable({}, _base_0)
|
||||
cls.__init(_self_0, ...)
|
||||
return _self_0
|
||||
end
|
||||
})
|
||||
_base_0.__class = _class_0
|
||||
local self = _class_0
|
||||
copyFile = function(source, target)
|
||||
local input, msg = io.open(source, 'rb')
|
||||
if input == nil then
|
||||
return input, msg
|
||||
end
|
||||
local output
|
||||
output, msg = io.open(target, 'wb')
|
||||
if output == nil then
|
||||
input:close()
|
||||
return output, msg
|
||||
end
|
||||
local err
|
||||
err, msg = output:write(input:read('*a'))
|
||||
if err == nil then
|
||||
input:close()
|
||||
output:close()
|
||||
return err, msg
|
||||
end
|
||||
input:close()
|
||||
output:close()
|
||||
return true
|
||||
end
|
||||
ETagCache = _class_0
|
||||
end
|
||||
local DownloadManager
|
||||
do
|
||||
local _class_0
|
||||
local freeManager
|
||||
local _base_0 = {
|
||||
loadedLibraryPath = loadedLibraryPath,
|
||||
addDownload = function(self, url, outfile, sha1, etag)
|
||||
if not (DM) then
|
||||
return nil, msgs.notInitialized:format(self.__name)
|
||||
end
|
||||
local urlType, outfileType = type(url), type(outfile)
|
||||
if urlType ~= "string" or outfileType ~= "string" then
|
||||
return nil, msgs.addMissingArgs:format(urlType, outfileType)
|
||||
end
|
||||
local msg
|
||||
outfile, msg = sanitizeFile(outfile)
|
||||
if outfile == nil then
|
||||
return outfile, msg
|
||||
end
|
||||
if "string" == type(sha1) then
|
||||
sha1 = sha1:lower()
|
||||
else
|
||||
sha1 = nil
|
||||
end
|
||||
if etag and "string" ~= type(etag) then
|
||||
etag = nil
|
||||
end
|
||||
if etag and self.cache and not self.cache:cachedFileExists(etag) then
|
||||
etag = nil
|
||||
end
|
||||
local id = DM.CDlM_addDownload(self.manager, url, outfile, sha1, etag)
|
||||
if id == 0 then
|
||||
return nil, msgs.failedToAdd
|
||||
end
|
||||
local download = {
|
||||
id = id,
|
||||
url = url,
|
||||
outfile = outfile,
|
||||
sha1 = sha1,
|
||||
etag = etag
|
||||
}
|
||||
table.insert(self.downloads, download)
|
||||
return download
|
||||
end,
|
||||
progress = function(self)
|
||||
if not (DM) then
|
||||
return nil, msgs.notInitialized:format(self.__name)
|
||||
end
|
||||
return math.floor(100 * DM.CDlM_progress(self.manager))
|
||||
end,
|
||||
cancel = function(self)
|
||||
if not (DM) then
|
||||
return nil, msgs.notInitialized:format(self.__name)
|
||||
end
|
||||
return DM.CDlM_terminate(self.manager)
|
||||
end,
|
||||
clear = function(self)
|
||||
if not (DM) then
|
||||
return nil, msgs.notInitialized:format(self.__name)
|
||||
end
|
||||
DM.CDlM_clear(self.manager)
|
||||
self.downloads = { }
|
||||
self.failedDownloads = { }
|
||||
end,
|
||||
waitForFinish = function(self, callback)
|
||||
if not (DM) then
|
||||
return nil, msgs.notInitialized:format(self.__name)
|
||||
end
|
||||
while 0 ~= DM.CDlM_busy(self.manager) do
|
||||
if callback and not callback(self:progress()) then
|
||||
return
|
||||
end
|
||||
sleep()
|
||||
end
|
||||
local pushFailed
|
||||
pushFailed = function(download, message)
|
||||
download.failed = true
|
||||
download.error = message
|
||||
return table.insert(self.failedDownloads, download)
|
||||
end
|
||||
local _list_0 = self.downloads
|
||||
for _index_0 = 1, #_list_0 do
|
||||
local download = _list_0[_index_0]
|
||||
local err = DM.CDlM_getError(self.manager, download.id)
|
||||
if err ~= nil then
|
||||
pushFailed(download, ffi.string(err))
|
||||
end
|
||||
if self.cache then
|
||||
if DM.CDlM_fileWasCached(self.manager, download.id) then
|
||||
local msg
|
||||
err, msg = self.cache:useCache(download)
|
||||
if err == nil then
|
||||
pushFailed(download, msgs.cacheFailure:format(msg))
|
||||
end
|
||||
else
|
||||
local newETag = DM.CDlM_getETag(self.manager, download.id)
|
||||
if newETag ~= nil then
|
||||
download.etag = ffi.string(newETag)
|
||||
self.cache:cache(download)
|
||||
end
|
||||
end
|
||||
end
|
||||
if "function" == type(download.callback) then
|
||||
download:callback(self)
|
||||
end
|
||||
end
|
||||
end,
|
||||
getFileSHA1 = function(self, filename)
|
||||
local filenameType = type(filename)
|
||||
if filenameType ~= "string" then
|
||||
return nil, msgs.getMissingArg:format(filenameType)
|
||||
end
|
||||
local result = DM.CDlM_getFileSHA1(filename)
|
||||
if result == nil then
|
||||
return nil, msgs.failedToOpen:format(filename)
|
||||
else
|
||||
result = ffi.string(result)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
checkFileSHA1 = function(self, filename, expected)
|
||||
local filenameType, expectedType = type(filename), type(expected)
|
||||
if filenameType ~= "string" or expectedType ~= "string" then
|
||||
return nil, msgs.checkMissingArgs:format(filenameType, expectedType)
|
||||
end
|
||||
local result, msg = self:getFileSHA1(filename)
|
||||
if result == nil then
|
||||
return result, msg
|
||||
elseif result == expected:lower() then
|
||||
return true
|
||||
else
|
||||
return false, msgs.hashMismatch:format(result, expected)
|
||||
end
|
||||
end,
|
||||
checkStringSHA1 = function(self, string, expected)
|
||||
local stringType, expectedType = type(string), type(expected)
|
||||
if stringType ~= "string" or expectedType ~= "string" then
|
||||
msgs.checkMissingArgs:format(stringType, expectedType)
|
||||
end
|
||||
local result = DM.CDlM_getStringSHA1(string)
|
||||
if result == nil then
|
||||
return nil, msgs.horriblyWrong
|
||||
else
|
||||
result = ffi.string(result)
|
||||
end
|
||||
if result == expected:lower() then
|
||||
return true
|
||||
else
|
||||
return false, msgs.hashMismatch:format(result, expected)
|
||||
end
|
||||
end,
|
||||
isInternetConnected = function(self)
|
||||
return DM.CDlM_isInternetConnected()
|
||||
end
|
||||
}
|
||||
_base_0.__index = _base_0
|
||||
_class_0 = setmetatable({
|
||||
__init = function(self, etagCacheDir)
|
||||
self.manager = ffi.gc(DM.CDlM_new(), freeManager)
|
||||
self.downloads = { }
|
||||
self.failedDownloads = { }
|
||||
if etagCacheDir then
|
||||
local result, message = sanitizeFile(etagCacheDir:gsub("[/\\]*$", "/", 1), true)
|
||||
assert(message == nil, message)
|
||||
self.cache = ETagCache(result)
|
||||
end
|
||||
end,
|
||||
__base = _base_0,
|
||||
__name = "DownloadManager"
|
||||
}, {
|
||||
__index = _base_0,
|
||||
__call = function(cls, ...)
|
||||
local _self_0 = setmetatable({}, _base_0)
|
||||
cls.__init(_self_0, ...)
|
||||
return _self_0
|
||||
end
|
||||
})
|
||||
_base_0.__class = _class_0
|
||||
local self = _class_0
|
||||
self.version = 0x000500
|
||||
self.version_string = "0.5.0"
|
||||
self.__depCtrlInit = function(DependencyControl)
|
||||
self.version = DependencyControl({
|
||||
name = tostring(self.__name),
|
||||
version = self.version_string,
|
||||
description = "Download things with libcurl without blocking Lua.",
|
||||
author = "torque",
|
||||
url = "https://github.com/TypesettingTools/ffi-experiments",
|
||||
moduleName = "DM." .. tostring(self.__name),
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/ffi-experiments/master/DependencyControl.json"
|
||||
})
|
||||
end
|
||||
freeManager = function(manager)
|
||||
return DM.CDlM_freeDM(manager)
|
||||
end
|
||||
DownloadManager = _class_0
|
||||
return _class_0
|
||||
end
|
74
.aegisub/automation/include/PT/PreciseTimer.lua
Normal file
74
.aegisub/automation/include/PT/PreciseTimer.lua
Normal file
|
@ -0,0 +1,74 @@
|
|||
local ffi = require("ffi")
|
||||
local requireffi = require("requireffi.requireffi")
|
||||
ffi.cdef([[struct CPT;
|
||||
typedef struct CPT CPT;
|
||||
CPT* startTimer( void );
|
||||
double getDuration( CPT *pt );
|
||||
unsigned int version( void );
|
||||
void freeTimer( CPT *pt );
|
||||
int usleep(unsigned int);
|
||||
void Sleep(unsigned long);
|
||||
]])
|
||||
local PTVersion = 0x000100
|
||||
local PT, loadedLibraryPath = requireffi("PT.PreciseTimer.PreciseTimer")
|
||||
local libVer = PT.version()
|
||||
if libVer < PTVersion or math.floor(libVer / 65536 % 256) > math.floor(PTVersion / 65536 % 256) then
|
||||
error("Library version mismatch. Wanted " .. tostring(PTVersion) .. ", got " .. tostring(libVer) .. ".")
|
||||
end
|
||||
local PreciseTimer
|
||||
do
|
||||
local _class_0
|
||||
local freeTimer
|
||||
local _base_0 = {
|
||||
loadedLibraryPath = loadedLibraryPath,
|
||||
timeElapsed = function(self)
|
||||
return PT.getDuration(self.timer)
|
||||
end,
|
||||
sleep = ffi.os == "Windows" and (function(ms)
|
||||
if ms == nil then
|
||||
ms = 100
|
||||
end
|
||||
return ffi.C.Sleep(ms)
|
||||
end) or (function(ms)
|
||||
if ms == nil then
|
||||
ms = 100
|
||||
end
|
||||
return ffi.C.usleep(ms * 1000)
|
||||
end)
|
||||
}
|
||||
_base_0.__index = _base_0
|
||||
_class_0 = setmetatable({
|
||||
__init = function(self)
|
||||
self.timer = ffi.gc(PT.startTimer(), freeTimer)
|
||||
end,
|
||||
__base = _base_0,
|
||||
__name = "PreciseTimer"
|
||||
}, {
|
||||
__index = _base_0,
|
||||
__call = function(cls, ...)
|
||||
local _self_0 = setmetatable({}, _base_0)
|
||||
cls.__init(_self_0, ...)
|
||||
return _self_0
|
||||
end
|
||||
})
|
||||
_base_0.__class = _class_0
|
||||
local self = _class_0
|
||||
self.version = 0x000106
|
||||
self.version_string = "0.1.6"
|
||||
self.__depCtrlInit = function(DependencyControl)
|
||||
self.version = DependencyControl({
|
||||
name = tostring(self.__name),
|
||||
version = self.version_string,
|
||||
description = "Measure times down to the nanosecond. Except not really.",
|
||||
author = "torque",
|
||||
url = "https://github.com/TypesettingTools/ffi-experiments",
|
||||
moduleName = "PT." .. tostring(self.__name),
|
||||
feed = "https://raw.githubusercontent.com/TypesettingTools/ffi-experiments/master/DependencyControl.json"
|
||||
})
|
||||
end
|
||||
freeTimer = function(timer)
|
||||
return PT.freeTimer(timer)
|
||||
end
|
||||
PreciseTimer = _class_0
|
||||
return _class_0
|
||||
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
|
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
|
||||
}
|
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
|
24
.aegisub/automation/include/json.lua
Normal file
24
.aegisub/automation/include/json.lua
Normal file
|
@ -0,0 +1,24 @@
|
|||
--[[
|
||||
Licensed according to the included 'LICENSE' document
|
||||
Author: Thomas Harning Jr <harningt@gmail.com>
|
||||
]]
|
||||
local decode = require("json.decode")
|
||||
local encode = require("json.encode")
|
||||
local util = require("json.util")
|
||||
|
||||
local _G = _G
|
||||
|
||||
local _ENV = nil
|
||||
|
||||
local json = {
|
||||
_VERSION = "1.3.4",
|
||||
_DESCRIPTION = "LuaJSON : customizable JSON decoder/encoder",
|
||||
_COPYRIGHT = "Copyright (c) 2007-2014 Thomas Harning Jr. <harningt@gmail.com>",
|
||||
decode = decode,
|
||||
encode = encode,
|
||||
util = util
|
||||
}
|
||||
|
||||
_G.json = json
|
||||
|
||||
return json
|
171
.aegisub/automation/include/json/decode.lua
Normal file
171
.aegisub/automation/include/json/decode.lua
Normal file
|
@ -0,0 +1,171 @@
|
|||
--[[
|
||||
Licensed according to the included 'LICENSE' document
|
||||
Author: Thomas Harning Jr <harningt@gmail.com>
|
||||
]]
|
||||
local lpeg = require("lpeg")
|
||||
|
||||
local error = error
|
||||
local pcall = pcall
|
||||
|
||||
local jsonutil = require("json.util")
|
||||
local merge = jsonutil.merge
|
||||
local util = require("json.decode.util")
|
||||
|
||||
local decode_state = require("json.decode.state")
|
||||
|
||||
local setmetatable, getmetatable = setmetatable, getmetatable
|
||||
local assert = assert
|
||||
local ipairs, pairs = ipairs, pairs
|
||||
local string_char = require("string").char
|
||||
|
||||
local type = type
|
||||
|
||||
local require = require
|
||||
|
||||
local _ENV = nil
|
||||
|
||||
local modulesToLoad = {
|
||||
"composite",
|
||||
"strings",
|
||||
"number",
|
||||
"others"
|
||||
}
|
||||
local loadedModules = {
|
||||
}
|
||||
|
||||
local json_decode = {}
|
||||
|
||||
json_decode.default = {
|
||||
unicodeWhitespace = true,
|
||||
initialObject = false,
|
||||
nothrow = false
|
||||
}
|
||||
|
||||
local modes_defined = { "default", "strict", "simple" }
|
||||
|
||||
json_decode.simple = {}
|
||||
|
||||
json_decode.strict = {
|
||||
unicodeWhitespace = true,
|
||||
initialObject = true,
|
||||
nothrow = false
|
||||
}
|
||||
|
||||
for _,name in ipairs(modulesToLoad) do
|
||||
local mod = require("json.decode." .. name)
|
||||
if mod.mergeOptions then
|
||||
for _, mode in pairs(modes_defined) do
|
||||
mod.mergeOptions(json_decode[mode], mode)
|
||||
end
|
||||
end
|
||||
loadedModules[#loadedModules + 1] = mod
|
||||
end
|
||||
|
||||
-- Shift over default into defaultOptions to permit build optimization
|
||||
local defaultOptions = json_decode.default
|
||||
json_decode.default = nil
|
||||
|
||||
local function generateDecoder(lexer, options)
|
||||
-- Marker to permit detection of final end
|
||||
local marker = {}
|
||||
local parser = lpeg.Ct((options.ignored * lexer)^0 * lpeg.Cc(marker)) * options.ignored * (lpeg.P(-1) + util.unexpected())
|
||||
local decoder = function(data)
|
||||
local state = decode_state.create(options)
|
||||
local parsed = parser:match(data)
|
||||
assert(parsed, "Invalid JSON data")
|
||||
local i = 0
|
||||
while true do
|
||||
i = i + 1
|
||||
local item = parsed[i]
|
||||
if item == marker then break end
|
||||
if type(item) == 'function' and item ~= jsonutil.undefined and item ~= jsonutil.null then
|
||||
item(state)
|
||||
else
|
||||
state:set_value(item)
|
||||
end
|
||||
end
|
||||
if options.initialObject then
|
||||
assert(type(state.previous) == 'table', "Initial value not an object or array")
|
||||
end
|
||||
-- Make sure stack is empty
|
||||
assert(state.i == 0, "Unclosed elements present")
|
||||
return state.previous
|
||||
end
|
||||
if options.nothrow then
|
||||
return function(data)
|
||||
local status, rv = pcall(decoder, data)
|
||||
if status then
|
||||
return rv
|
||||
else
|
||||
return nil, rv
|
||||
end
|
||||
end
|
||||
end
|
||||
return decoder
|
||||
end
|
||||
|
||||
local function buildDecoder(mode)
|
||||
mode = mode and merge({}, defaultOptions, mode) or defaultOptions
|
||||
for _, mod in ipairs(loadedModules) do
|
||||
if mod.mergeOptions then
|
||||
mod.mergeOptions(mode)
|
||||
end
|
||||
end
|
||||
local ignored = mode.unicodeWhitespace and util.unicode_ignored or util.ascii_ignored
|
||||
-- Store 'ignored' in the global options table
|
||||
mode.ignored = ignored
|
||||
|
||||
--local grammar = {
|
||||
-- [1] = mode.initialObject and (ignored * (object_type + array_type)) or value_type
|
||||
--}
|
||||
local lexer
|
||||
for _, mod in ipairs(loadedModules) do
|
||||
local new_lexer = mod.generateLexer(mode)
|
||||
lexer = lexer and lexer + new_lexer or new_lexer
|
||||
end
|
||||
return generateDecoder(lexer, mode)
|
||||
end
|
||||
|
||||
-- Since 'default' is nil, we cannot take map it
|
||||
local defaultDecoder = buildDecoder(json_decode.default)
|
||||
local prebuilt_decoders = {}
|
||||
for _, mode in pairs(modes_defined) do
|
||||
if json_decode[mode] ~= nil then
|
||||
prebuilt_decoders[json_decode[mode]] = buildDecoder(json_decode[mode])
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Options:
|
||||
number => number decode options
|
||||
string => string decode options
|
||||
array => array decode options
|
||||
object => object decode options
|
||||
initialObject => whether or not to require the initial object to be a table/array
|
||||
allowUndefined => whether or not to allow undefined values
|
||||
]]
|
||||
local function getDecoder(mode)
|
||||
mode = mode == true and json_decode.strict or mode or json_decode.default
|
||||
local decoder = mode == nil and defaultDecoder or prebuilt_decoders[mode]
|
||||
if decoder then
|
||||
return decoder
|
||||
end
|
||||
return buildDecoder(mode)
|
||||
end
|
||||
|
||||
local function decode(data, mode)
|
||||
local decoder = getDecoder(mode)
|
||||
return decoder(data)
|
||||
end
|
||||
|
||||
local mt = {}
|
||||
mt.__call = function(self, ...)
|
||||
return decode(...)
|
||||
end
|
||||
|
||||
json_decode.getDecoder = getDecoder
|
||||
json_decode.decode = decode
|
||||
json_decode.util = util
|
||||
setmetatable(json_decode, mt)
|
||||
|
||||
return json_decode
|
190
.aegisub/automation/include/json/decode/composite.lua
Normal file
190
.aegisub/automation/include/json/decode/composite.lua
Normal file
|
@ -0,0 +1,190 @@
|
|||
--[[
|
||||
Licensed according to the included 'LICENSE' document
|
||||
Author: Thomas Harning Jr <harningt@gmail.com>
|
||||
]]
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
|
||||
local lpeg = require("lpeg")
|
||||
|
||||
local util = require("json.decode.util")
|
||||
local jsonutil = require("json.util")
|
||||
|
||||
local rawset = rawset
|
||||
|
||||
local assert = assert
|
||||
local tostring = tostring
|
||||
|
||||
local error = error
|
||||
local getmetatable = getmetatable
|
||||
|
||||
local _ENV = nil
|
||||
|
||||
local defaultOptions = {
|
||||
array = {
|
||||
trailingComma = true
|
||||
},
|
||||
object = {
|
||||
trailingComma = true,
|
||||
number = true,
|
||||
identifier = true,
|
||||
setObjectKey = rawset
|
||||
},
|
||||
calls = {
|
||||
defs = nil,
|
||||
-- By default, do not allow undefined calls to be de-serialized as call objects
|
||||
allowUndefined = false
|
||||
}
|
||||
}
|
||||
|
||||
local modeOptions = {
|
||||
default = nil,
|
||||
strict = {
|
||||
array = {
|
||||
trailingComma = false
|
||||
},
|
||||
object = {
|
||||
trailingComma = false,
|
||||
number = false,
|
||||
identifier = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local function BEGIN_ARRAY(state)
|
||||
state:push()
|
||||
state:new_array()
|
||||
end
|
||||
local function END_ARRAY(state)
|
||||
state:end_array()
|
||||
state:pop()
|
||||
end
|
||||
|
||||
local function BEGIN_OBJECT(state)
|
||||
state:push()
|
||||
state:new_object()
|
||||
end
|
||||
local function END_OBJECT(state)
|
||||
state:end_object()
|
||||
state:pop()
|
||||
end
|
||||
|
||||
local function END_CALL(state)
|
||||
state:end_call()
|
||||
state:pop()
|
||||
end
|
||||
|
||||
local function SET_KEY(state)
|
||||
state:set_key()
|
||||
end
|
||||
|
||||
local function NEXT_VALUE(state)
|
||||
state:put_value()
|
||||
end
|
||||
|
||||
local function mergeOptions(options, mode)
|
||||
jsonutil.doOptionMerge(options, true, 'array', defaultOptions, mode and modeOptions[mode])
|
||||
jsonutil.doOptionMerge(options, true, 'object', defaultOptions, mode and modeOptions[mode])
|
||||
jsonutil.doOptionMerge(options, true, 'calls', defaultOptions, mode and modeOptions[mode])
|
||||
end
|
||||
|
||||
|
||||
local isPattern
|
||||
if lpeg.type then
|
||||
function isPattern(value)
|
||||
return lpeg.type(value) == 'pattern'
|
||||
end
|
||||
else
|
||||
local metaAdd = getmetatable(lpeg.P("")).__add
|
||||
function isPattern(value)
|
||||
return getmetatable(value).__add == metaAdd
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function generateSingleCallLexer(name, func)
|
||||
if type(name) ~= 'string' and not isPattern(name) then
|
||||
error("Invalid functionCalls name: " .. tostring(name) .. " not a string or LPEG pattern")
|
||||
end
|
||||
-- Allow boolean or function to match up w/ encoding permissions
|
||||
if type(func) ~= 'boolean' and type(func) ~= 'function' then
|
||||
error("Invalid functionCalls item: " .. name .. " not a function")
|
||||
end
|
||||
local function buildCallCapture(name)
|
||||
return function(state)
|
||||
if func == false then
|
||||
error("Function call on '" .. name .. "' not permitted")
|
||||
end
|
||||
state:push()
|
||||
state:new_call(name, func)
|
||||
end
|
||||
end
|
||||
local nameCallCapture
|
||||
if type(name) == 'string' then
|
||||
nameCallCapture = lpeg.P(name .. "(") * lpeg.Cc(name) / buildCallCapture
|
||||
else
|
||||
-- Name matcher expected to produce a capture
|
||||
nameCallCapture = name * "(" / buildCallCapture
|
||||
end
|
||||
-- Call func over nameCallCapture and value to permit function receiving name
|
||||
return nameCallCapture
|
||||
end
|
||||
|
||||
local function generateNamedCallLexers(options)
|
||||
if not options.calls or not options.calls.defs then
|
||||
return
|
||||
end
|
||||
local callCapture
|
||||
for name, func in pairs(options.calls.defs) do
|
||||
local newCapture = generateSingleCallLexer(name, func)
|
||||
if not callCapture then
|
||||
callCapture = newCapture
|
||||
else
|
||||
callCapture = callCapture + newCapture
|
||||
end
|
||||
end
|
||||
return callCapture
|
||||
end
|
||||
|
||||
local function generateCallLexer(options)
|
||||
local lexer
|
||||
local namedCapture = generateNamedCallLexers(options)
|
||||
if options.calls and options.calls.allowUndefined then
|
||||
lexer = generateSingleCallLexer(lpeg.C(util.identifier), true)
|
||||
end
|
||||
if namedCapture then
|
||||
lexer = lexer and lexer + namedCapture or namedCapture
|
||||
end
|
||||
if lexer then
|
||||
lexer = lexer + lpeg.P(")") * lpeg.Cc(END_CALL)
|
||||
end
|
||||
return lexer
|
||||
end
|
||||
|
||||
local function generateLexer(options)
|
||||
local ignored = options.ignored
|
||||
local array_options, object_options = options.array, options.object
|
||||
local lexer =
|
||||
lpeg.P("[") * lpeg.Cc(BEGIN_ARRAY)
|
||||
+ lpeg.P("]") * lpeg.Cc(END_ARRAY)
|
||||
+ lpeg.P("{") * lpeg.Cc(BEGIN_OBJECT)
|
||||
+ lpeg.P("}") * lpeg.Cc(END_OBJECT)
|
||||
+ lpeg.P(":") * lpeg.Cc(SET_KEY)
|
||||
+ lpeg.P(",") * lpeg.Cc(NEXT_VALUE)
|
||||
if object_options.identifier then
|
||||
-- Add identifier match w/ validation check that it is in key
|
||||
lexer = lexer + lpeg.C(util.identifier) * ignored * lpeg.P(":") * lpeg.Cc(SET_KEY)
|
||||
end
|
||||
local callLexers = generateCallLexer(options)
|
||||
if callLexers then
|
||||
lexer = lexer + callLexers
|
||||
end
|
||||
return lexer
|
||||
end
|
||||
|
||||
local composite = {
|
||||
mergeOptions = mergeOptions,
|
||||
generateLexer = generateLexer
|
||||
}
|
||||
|
||||
return composite
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue