script_name = "Rewriting Tools" script_author = "arch1t3cht" script_version = "1.3.2" script_namespace = "arch.RWTools" script_description = "Shortcuts for managing multiple rewrites of a line in one .ass event line." haveDepCtrl, DependencyControl = pcall(require, "l0.DependencyControl") default_config = { default_signature = "OG", personal_signature = "", auto_fixes = true, forbid_nested = true, } if haveDepCtrl then depctrl = DependencyControl({ feed = "https://raw.githubusercontent.com/TypesettingTools/arch1t3cht-Aegisub-Scripts/main/DependencyControl.json", }) config = depctrl:getConfigHandler(default_config, "config") else id = function() return nil end config = {c = default_config, load = id, write = id} end -- This script automates the process of commenting and uncommenting subitle lines during rewrites, -- using conventions used in some of the groups I'm working in. -- -- Line format (formal-ish specification - scroll down for the functions and explicit examples): -- Every line of a subtitle file is a sequence of sections of one of the following forms: -- - Global styling tags: A brace block whose content starts with a backslash and ends with a pipe ('|'). -- These are styling overrides which should globally apply to any rewrite (e.g. position, alignment, font size, etc) -- as opposed to local styling tags like those italicizing certain words. -- -- Each (contiguous set) of global styling tags starts a new text section, each of which will have its self-contained set of rewrites. -- To start such a section without changing styles, simply use an empty tag of this format like {\|}. -- -- - Text sections: The text before, after, or in between two global styling tags. -- Each of these sections reprents one bit of text, which is open for rewrites. -- Such a section should be a sequence of blocks of one of the following forms: -- - Inactive line: A brace block not started by a backslash or asterisk (Asterisks are allowed for compatibility with Dialog Swapper). -- This can be any brace block matching this format (so also miscellaneous comments that aren't actual lines), -- But in order for the script to interact with them, they should have the format -- {<proposed line> - <author>} , -- where the <author> signature could potentially also contain other comments. -- In the <proposed line>, should be escaped by replacing their braces with square brackets, -- and the backslashes by harmless forward slashes. -- -- - Active line: A block of the form -- <proposed line>[{<author>}] , -- where the <proposed line> is allowed to contain (local) styling tags, and the <author> signature -- must not begin with a backslash or asterisk and is only allowed to be omitted when this block is the last -- in its section (since otherwise the following inactive line would be incorrectly recognized as the signature) -- -- Only one of the blocks in a section should be an active line at any time. -- -- In most cases, there will be only one non-empty text section. -- -- The script's main function "Switch Active Lines" will, for each section: -- - Deactivate any active lines, signing them with the default signature (defaults to "OG") if no signature is present. -- - Activate any inactive lines where the " - " separating the line from the signature has been marked by replacing it with " !- ". -- Thus, there should ideally be just one line per section marked this way. -- - If there is a block that has been deactivated, but no block has been activated (suggesting that the user wants to write a new suggested line), -- it will add a new signature at the end of the section, providing that one is specified in the configuration. -- -- The function "Prepare Rewrite" will do all of the above, but also -- - Copy the line that was just deactivated to the end of the section, before the added signature -- That way, the user can quickly propose a small change in the line. -- - Always add a signature, as long as one is configured. -- -- The functions "Shift Line Break Forward" and "Shift Line Break Backward" will shift the line break in the currently active line(s) -- forward or backward respectively, by one word (i.e. one contiguous sequence of non-space characters). -- -- The function "Clean Up Styling Tag Escapes" will simply remove all the pipe ('|') characters from global styling tags. -- -- Examples: -- - Deactivating the line -- {\i1}Hello{\i0}, world!{author} -- Will turn it into -- {[/i1]Hello[/i0], world! - author} -- If a personal signature like "me" is set in the configuration, it will automatically be added: -- {[/i1]Hello[/i0], world! - author}{me} -- -- - Deactivating the line -- {\an8|}Hello, {\i1}world{\i0}! -- Will turn it into -- {\an8|}{Hello,[/i1]world[/i0]! - OG} -- where the default signature "OG" can be set to a different on in the configuration. -- -- - Applying "Switch Lines" to the line -- {Hello, [/i1]world[/i0]! !- OG}{foo - bar} . -- Will turn it into -- Hello, {\i1}world{\i0}!{OG}{foo - bar} . -- Applying "Switch Lines" again will turn this line into -- {Hello, [/i1]world[/i0]! - OG}{foo - bar} , -- which is just the beginning line without the marker. -- On the other hand, applying "Prepare Rewrite" to this second line will turn it into -- {Hello, [/i1]world[/i0]! - OG}{foo - bar}Hello, {\i1}world{\i0}!{me} , -- where "me" is the personal signature set in the configuration. -- -- - A more complex example containing multiple sections: Consider a line where two people speak simultaneously, -- which is represented using en dashes (represented as hyphens here): -- - foo!\N- bar! -- To make a rewrite for only one of these lines, add a separator after \N: -- - foo!\N{\|}- bar! -- Now these can be both be rewritten with "Prepare Rewrite": -- {- foo!\N - OG}- foo!\N{me}{\|}{- bar! - OG}- bar!{me} -- Say we want to rewrite the first line to "- foo2!\N", and don't rewrite the second line: -- {- foo!\N - OG}- foo2!\N{me}{\|}{- bar! - OG} -- Apply "Switch Lines" again: -- {- foo!\N - OG}{- foo2!\N - me}{me}{\|}{- bar! - OG} -- This will cause a duplicate signature, but preventing this would require a lot more macro options. Remove this duplicate, and mark both lines: -- {- foo!\N - OG}{- foo2!\N !- me}{\|}{- bar! !- OG} -- Finally, apply "Switch Lines": -- {- foo!\N - OG}- foo2!{me}\N{\|}- bar!{OG} -- This specific application is very tedious for small rewrites, but can still greatly speed up the process for longer sections with formatting. -- -- It is strongly recommended to bind the macros for rewriting and shifting line breaks to keybinds. -- I use Ctrl+K and Ctrl+Shift+K respectively for "Switch Active Lines" and "Prepare Rewrite", -- as well as Ctrl+, and Ctrl+. respectively for "Shift Line Break Backward" and "Shift Line Break Forward". function unreachable() if not val then aegisub.log("Incorrect line format! Aborting.") aegisub.cancel() end end -- These two functions are split up because signed deactivated lines like -- {Upper line\N - author} would be broken by simply stripping spaces -- surrounding \N tags everywhere. -- An unintended but welcome side effect is that only those lines which the -- user touches will have text fixes applied - this ensures a basic level -- of reversibility. function fix_text(line) -- Applies some basic formatting fixes: -- Removes spaces surrounding \N tags while true do local newline = line :gsub(" *\\N *([^!])", "\\N%1") :gsub(" *\\n *", "\\n") if newline == line then return line end line = newline end end function fix_line_format(line) -- Applies some basic formatting fixes: -- Removes spaces surrounding or padding {} blocks while true do local newline = line :gsub("{ *", "{") :gsub("({[^}]-) *}", "%1}") -- make sure '}' is closing a tag here (still breaks if there's nested tags, but this is the best lua patterns can do.) if newline == line then return line end line = newline end end function fix_text_checked(line) if config.c.auto_fixes then return fix_text(line) else return line end end function fix_line_format_checked(line) if config.c.auto_fixes then return fix_line_format(line) else return line end end function stripstart(intext, out) local ws = intext:match("^ *") intext = intext:sub(#ws + 1) if not config.c.auto_fixes then out = out .. ws end return intext, out end function appendstripend(text, out) if config.c.auto_fixes then local ws = text:match(" *$") text = text:sub(1, #text - #ws) end return out .. text end function get_signature(sign) if sign ~= "" then return "{" .. sign .. "}" end return "" end -- Arguments -- shift: True if shifting line breaks, false otherwise -- shift_dir: If shifting line breaks, true if shifting forward, else false -- rewrite: If not shifting line breaks, true if the line being deactivated should be copied for a rewrite. function switch_lines_proper(subs, sel, rewrite, shift, shift_dir) local insertion_point_marker = "{/|\\}" local new_insertion_point = nil for _, i in ipairs(sel) do local line = subs[i] if config.c.forbid_nested then if line.text:match("{[^}]+{") then aegisub.log("Nested braces detected! Please fix them before running the script. Aborting...") return end end -- local intext = fix_line_format_checked(line.text) local intext = line.text local out = "" local deactivated = false local reactivated = false local newline = "" while true do -- parse components of the line intext, out = stripstart(intext, out) local escaped_braceblock = intext:match("^{\\[^}]-|}") local deactive_line = intext:match("^{[^\\*][^}]-}") -- and some edge cases: local empty_block = intext:match("^{}") local unterminated_brace = intext:match("^{[^}]*$") if escaped_braceblock ~= nil or intext == "" then if rewrite then newline, out = stripstart(newline, out) newline = fix_text_checked(newline) out = appendstripend(newline, out) end out = out .. insertion_point_marker if (deactivated and not reactivated) or (rewrite and newline ~= "") then out = out .. get_signature(config.c.personal_signature) end if intext == "" then break end out = out .. escaped_braceblock intext = intext:sub(#escaped_braceblock + 1) deactivated = false reactivated = false newline = "" elseif deactive_line ~= nil then if shift or deactive_line:match(" !%- ") == nil then out = out .. deactive_line else local reactivate_line = deactive_line -- evil capture hacks to replace only those forward slashes -- by backslashes, that are contained in square brackets. while true do local r = reactivate_line:gsub("(%[[^%]]-)/([^%]]-%])", "%1%\\%2") if r == reactivate_line then break end reactivate_line = r end if config.c.auto_fixes then reactivate_line = reactivate_line:gsub(" * !%- ", " !- ") end out = out .. fix_text_checked(reactivate_line:gsub("^{", ""):gsub("%[", "{"):gsub("%]", "}"):gsub(" !%- ", "{")) reactivated = true end intext = intext:sub(#deactive_line + 1) elseif empty_block ~= nil then -- just ignore it out = out .. empty_block intext = intext:sub(#empty_block + 1) elseif unterminated_brace ~= nil then -- terminate it and (effectively) abort out = out .. unterminated_brace intext = intext:sub(#unterminated_brace + 1) else -- beginning of an active line local linetext = "" local linesignature = config.c.default_signature -- this could be done with a bigger regex and some gsubs, -- but in edge cases with broken nested braces this might hold up better while intext ~= "" do local escaped_braceblock = intext:match("^{[\\*][^}]-|}") local styling_braceblock = intext:match("^{[\\*][^}]-}") local cleartext = intext:match("^[^{]+") local signature = intext:match("^{[^}]-}") if escaped_braceblock ~= nil then break elseif styling_braceblock ~= nil then newline = newline .. styling_braceblock if not shift then linetext = linetext .. styling_braceblock:gsub("{", "["):gsub("}", "]"):gsub("\\", "/") else linetext = linetext .. styling_braceblock end intext = intext:sub(#styling_braceblock + 1) elseif cleartext ~= nil then newline = newline .. cleartext if not shift then linetext = linetext .. cleartext else ctext = cleartext if shift_dir then if not ctext:match("\\N") then ctext = "\\N" .. ctext end ctext = ctext -- Shift a newline forward that's in somewhere in the middle of the line. Won't shift a new line to the very left. :gsub("([^ ]+) *\\N *([^ ]+) *", "%1 %2\\N") -- Shift a newline forward that's to the very left of the line. :gsub("^ *\\N *([^ ]+) *", "%1\\N") -- Remove a newline to the very right of the line. :gsub(" *\\N *$", "") else if not ctext:match("\\N") then ctext = ctext .. "\\N" end ctext = ctext :gsub(" *([^ ]+) *\\N *([^ ]+)", "\\N%1 %2") :gsub(" *([^ ]+) *\\N *$", "\\N%1") :gsub("^ *\\N *", "") end linetext = linetext .. ctext end intext = intext:sub(#cleartext + 1) elseif signature ~= nil then if #signature >= 2 then assert(signature[2] ~= "|" and signature[2] ~= "\\") end if not shift then linesignature = signature:gsub("[{}]", "") else linetext = linetext .. signature end intext = intext:sub(#signature + 1) break else assert(false) end end if not shift then out = out .. "{" .. fix_text_checked(linetext) if linesignature ~= "" then out = out .. " - " .. linesignature end out = out .. "}" else out = out .. linetext end deactivated = not shift end end out = fix_line_format_checked(out) new_insertion_point = out:find(insertion_point_marker) out = out:gsub(insertion_point_marker, "") line.text = out subs[i] = line end if new_insertion_point ~= nil and aegisub.gui ~= nil then aegisub.gui.set_cursor(new_insertion_point) end end function clean_lines(subs, sel) load_config() for _, i in ipairs(sel) do local line = subs[i] while true do local newtext = line.text:gsub("({\\[^}]-)|}", "%1}") if newtext == line.text then break end line.text = newtext end subs[i] = line end end function switch_lines(subs, sel) switch_lines_proper(subs, sel, false) end function rewrite_line(subs, sel) switch_lines_proper(subs, sel, true) end function shift_forward(subs, sel) switch_lines_proper(subs, sel, false, true, true) end function shift_backward(subs, sel) switch_lines_proper(subs, sel, false, true, false) end function configure() config:load() local diag = { {class = 'label', label = 'Default signature', x = 0, y = 0, width = 1, height = 1}, { class = 'edit', name = 'default_signature', hint = "Signature to sign unsigned active lines with. Leave blank to deactivate.", value = config.c.default_signature, x = 1, y = 0, width = 1, height = 1, }, {class = 'label', label = 'Your signature', x = 0, y = 1, width = 1, height = 1}, { class = 'edit', name = 'personal_signature', hint = "Signature to automatically insert when deactivating a line. Leave blank to deactivate.", value = config.c.personal_signature, x = 1, y = 1, width = 1, height = 1, }, {class = 'label', label = 'Apply auto fixes', x = 0, y = 2, width = 1, height = 1}, { class = 'checkbox', name = 'auto_fixes', hint = "Whether to automatically remove unnecessary spaces. Deactivate if you know what you're doing (e.g. for special typesetting).", value = config.c.auto_fixes, x = 1, y = 2, width = 1, height = 1, }, {class = 'label', label = 'Check for nested braces', x = 0, y = 3, width = 1, height = 1}, { class = 'checkbox', name = 'forbid_nested', hint = "Whether to abort if nested braces are found. Deactivate if you know what you're doing.", value = config.c.forbid_nested, x = 1, y = 3, width = 1, height = 1, }, } local buttons = {'OK', 'Cancel'} local button_ids = {ok = 'OK', cancel = 'Cancel'} local button, results = aegisub.dialog.display(diag, buttons, button_ids) if button == false then aegisub.cancel() end for i,v in ipairs({"personal_signature", "default_signature", "auto_fixes", "forbid_nested"}) do if results[v] ~= config.c[v] then config.c[v] = results[v] end end config:write() return results end local mymacros = {} function wrap_register_macro(name, ...) if haveDepCtrl then table.insert(mymacros, {name, ...}) else aegisub.register_macro(script_name .. "/" .. name, ...) end end wrap_register_macro("Switch Active Lines", "Deactivates the active line and activates any inactive lines marked with !- .", switch_lines) wrap_register_macro("Prepare Rewrite", "Deactivates the active line and copies it to a new line for rewriting.", rewrite_line) wrap_register_macro("Shift Line Break Forward", "Shifts the line break in the currently active line forward by one word.", shift_forward) wrap_register_macro("Shift Line Break Backward", "Shifts the line break in the currently active line backward by one word.", shift_backward) wrap_register_macro("Clean Up Styling Tag Escapes", "Removes all pipe ('|') characters from the end of styling blocks.", clean_lines) wrap_register_macro("Configure", "Configure Rewriting Tools", configure) if haveDepCtrl then depctrl:registerMacros(mymacros) end