[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:
odrling 2019-07-31 22:28:40 +02:00
parent 398ce7b105
commit 92f84239ae
69 changed files with 14729 additions and 66 deletions

@ -0,0 +1 @@
Subproject commit 95b9be7a326078588f76993b15110a060dfe737c

View file

@ -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)

View file

@ -0,0 +1 @@
../amoegisub/karaoke-split.lua

View 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"

View file

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

View file

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

View file

@ -1 +0,0 @@
../unanimated/ua.BlurAndGlow.lua

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
../unanimated/ua.ChangeCase.lua

View 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

View file

@ -1 +0,0 @@
../unanimated/ua.Colorize.lua

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
../unanimated/ua.Cycles.lua

View 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

View file

@ -1 +0,0 @@
../unanimated/ua.EncodeHardsub.lua

View file

@ -1 +0,0 @@
../unanimated/ua.FadeWorks.lua

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
../unanimated/ua.HYDRA.lua

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
../unanimated/ua.JoinSplitSnap.lua

View 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

View file

@ -1 +0,0 @@
../unanimated/ua.JumpToNext.lua

View file

@ -1 +0,0 @@
../unanimated/ua.LineBreaker.lua

View file

@ -1 +0,0 @@
../unanimated/ua.Masquerade.lua

View file

@ -1 +0,0 @@
../unanimated/ua.MultiCopy.lua

View file

@ -1 +0,0 @@
../unanimated/ua.MultiLineEditor.lua

View file

@ -1 +0,0 @@
../unanimated/ua.Multiplexer.lua

View file

@ -1 +0,0 @@
../unanimated/ua.NecrosCopy.lua

View file

@ -1 +0,0 @@
../unanimated/ua.QC.lua

View file

@ -1 +0,0 @@
../unanimated/ua.Recalculator.lua

View file

@ -1 +0,0 @@
../unanimated/ua.Relocator.lua

View file

@ -1 +0,0 @@
../unanimated/ua.ScriptCleanup.lua

View file

@ -1 +0,0 @@
../unanimated/ua.Selectrix.lua

View file

@ -1 +0,0 @@
../unanimated/ua.ShiftCut.lua

View file

@ -1 +0,0 @@
../unanimated/ua.Significance.lua

View file

@ -1 +0,0 @@
../unanimated/ua.TimeSigns.lua

View file

@ -1 +0,0 @@
../unanimated/ua.iBus.lua

View 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

View 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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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

View 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

View 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