├── img ├── RtlEditor.jpg ├── AddBackground.jpg └── MasafAutomation.png ├── LICENSE ├── README.md ├── utf8.lua └── MasafAutomation.lua /img/RtlEditor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Majid110/MasafAutomation/HEAD/img/RtlEditor.jpg -------------------------------------------------------------------------------- /img/AddBackground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Majid110/MasafAutomation/HEAD/img/AddBackground.jpg -------------------------------------------------------------------------------- /img/MasafAutomation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Majid110/MasafAutomation/HEAD/img/MasafAutomation.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Majid Shamkhani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MasafAutomation 2 | 3 | Some Aegisub automation scripts specially designed for Right-To-Left language subtitles 4 | 5 | ![Masaf Automation](img/MasafAutomation.png) 6 | 7 | ## Scripts: 8 | 9 | - Add Backgrounds 10 | - Remove all Background lines 11 | - Rtl Correction - All lines 12 | - Rtl Correction - Selected 13 | - Rtl Correction without normalize 14 | - Add RLE - Selected lines 15 | - Undo Rtl Correction - Selected 16 | - Numbers to English 17 | - Numbers to Arabic 18 | - Numbers to Persian 19 | - Selected Numbers to English 20 | - Selected Numbers to Arabic 21 | - Selected Numbers to Persian 22 | - Shift start line forward 23 | - Shift start line backward 24 | - Shift end line forward 25 | - Shift end line backward 26 | - Make next line continuous 27 | - Make Same time 28 | - Make Same Start time 29 | - Make Same End time 30 | - Move last text part 31 | - Move first part of next 32 | - Move last word 33 | - Move first word of next 34 | - Shift Linebreak 35 | - Shift Linebreak Back 36 | - Split line 37 | - Split line at Index 38 | - Break Semi Long lines 39 | - Break Selected line 40 | - Show Rtl Editor 41 | - Remove line Breaks 42 | - Remove Position tags 43 | - Select playing line 44 | - Fix line Position 45 | - Set line as No Background 46 | - Set line as Don't Correct RTL 47 | - Set line as Don't Remove 48 | - Generate SRT like text 49 | - Unify Background lines 50 | - Add Code to Selected lines 51 | - Import text to selected Lines 52 | - Display sum of times 53 | - Go to line 54 | 55 | ## Screenshots: 56 | 57 | Transparent background for subtitle 58 | 59 | ![Add Background](img/AddBackground.jpg) 60 | 61 | RTL editor 62 | 63 | ![Rtl Editor](img/RtlEditor.jpg) 64 | -------------------------------------------------------------------------------- /utf8.lua: -------------------------------------------------------------------------------- 1 | -- $Id: utf8.lua 179 2009-04-03 18:10:03Z pasta $ 2 | -- 3 | -- Provides UTF-8 aware string functions implemented in pure lua: 4 | -- * utf8len(s) 5 | -- * utf8sub(s, i, j) 6 | -- * utf8reverse(s) 7 | -- * utf8char(unicode) 8 | -- * utf8unicode(s, i, j) 9 | -- * utf8gensub(s, sub_len) 10 | -- * utf8find(str, regex, init, plain) 11 | -- * utf8match(str, regex, init) 12 | -- * utf8gmatch(str, regex, all) 13 | -- * utf8gsub(str, regex, repl, limit) 14 | -- 15 | -- If utf8data.lua (containing the lower<->upper case mappings) is loaded, these 16 | -- additional functions are available: 17 | -- * utf8upper(s) 18 | -- * utf8lower(s) 19 | -- 20 | -- All functions behave as their non UTF-8 aware counterparts with the exception 21 | -- that UTF-8 characters are used instead of bytes for all units. 22 | 23 | --[[ 24 | Copyright (c) 2006-2007, Kyle Smith 25 | All rights reserved. 26 | 27 | Contributors: 28 | Alimov Stepan 29 | 30 | Redistribution and use in source and binary forms, with or without 31 | modification, are permitted provided that the following conditions are met: 32 | 33 | * Redistributions of source code must retain the above copyright notice, 34 | this list of conditions and the following disclaimer. 35 | * Redistributions in binary form must reproduce the above copyright 36 | notice, this list of conditions and the following disclaimer in the 37 | documentation and/or other materials provided with the distribution. 38 | * Neither the name of the author nor the names of its contributors may be 39 | used to endorse or promote products derived from this software without 40 | specific prior written permission. 41 | 42 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 43 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 44 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 45 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 46 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 47 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 48 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 49 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 50 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 51 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 52 | --]] 53 | 54 | -- ABNF from RFC 3629 55 | -- 56 | -- UTF8-octets = *( UTF8-char ) 57 | -- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4 58 | -- UTF8-1 = %x00-7F 59 | -- UTF8-2 = %xC2-DF UTF8-tail 60 | -- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / 61 | -- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) 62 | -- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / 63 | -- %xF4 %x80-8F 2( UTF8-tail ) 64 | -- UTF8-tail = %x80-BF 65 | -- 66 | 67 | local byte = string.byte 68 | local char = string.char 69 | local dump = string.dump 70 | local find = string.find 71 | local format = string.format 72 | local len = string.len 73 | local lower = string.lower 74 | local rep = string.rep 75 | local sub = string.sub 76 | local upper = string.upper 77 | 78 | -- returns the number of bytes used by the UTF-8 character at byte i in s 79 | -- also doubles as a UTF-8 character validator 80 | local function utf8charbytes (s, i) 81 | -- argument defaults 82 | i = i or 1 83 | 84 | -- argument checking 85 | if type(s) ~= "string" then 86 | error("bad argument #1 to 'utf8charbytes' (string expected, got ".. type(s).. ")") 87 | end 88 | if type(i) ~= "number" then 89 | error("bad argument #2 to 'utf8charbytes' (number expected, got ".. type(i).. ")") 90 | end 91 | 92 | local c = byte(s, i) 93 | 94 | -- determine bytes needed for character, based on RFC 3629 95 | -- validate byte 1 96 | if c > 0 and c <= 127 then 97 | -- UTF8-1 98 | return 1 99 | 100 | elseif c >= 194 and c <= 223 then 101 | -- UTF8-2 102 | local c2 = byte(s, i + 1) 103 | 104 | if not c2 then 105 | error("UTF-8 string terminated early") 106 | end 107 | 108 | -- validate byte 2 109 | if c2 < 128 or c2 > 191 then 110 | error("Invalid UTF-8 character") 111 | end 112 | 113 | return 2 114 | 115 | elseif c >= 224 and c <= 239 then 116 | -- UTF8-3 117 | local c2 = byte(s, i + 1) 118 | local c3 = byte(s, i + 2) 119 | 120 | if not c2 or not c3 then 121 | error("UTF-8 string terminated early") 122 | end 123 | 124 | -- validate byte 2 125 | if c == 224 and (c2 < 160 or c2 > 191) then 126 | error("Invalid UTF-8 character") 127 | elseif c == 237 and (c2 < 128 or c2 > 159) then 128 | error("Invalid UTF-8 character") 129 | elseif c2 < 128 or c2 > 191 then 130 | error("Invalid UTF-8 character") 131 | end 132 | 133 | -- validate byte 3 134 | if c3 < 128 or c3 > 191 then 135 | error("Invalid UTF-8 character") 136 | end 137 | 138 | return 3 139 | 140 | elseif c >= 240 and c <= 244 then 141 | -- UTF8-4 142 | local c2 = byte(s, i + 1) 143 | local c3 = byte(s, i + 2) 144 | local c4 = byte(s, i + 3) 145 | 146 | if not c2 or not c3 or not c4 then 147 | error("UTF-8 string terminated early") 148 | end 149 | 150 | -- validate byte 2 151 | if c == 240 and (c2 < 144 or c2 > 191) then 152 | error("Invalid UTF-8 character") 153 | elseif c == 244 and (c2 < 128 or c2 > 143) then 154 | error("Invalid UTF-8 character") 155 | elseif c2 < 128 or c2 > 191 then 156 | error("Invalid UTF-8 character") 157 | end 158 | 159 | -- validate byte 3 160 | if c3 < 128 or c3 > 191 then 161 | error("Invalid UTF-8 character") 162 | end 163 | 164 | -- validate byte 4 165 | if c4 < 128 or c4 > 191 then 166 | error("Invalid UTF-8 character") 167 | end 168 | 169 | return 4 170 | 171 | else 172 | error("Invalid UTF-8 character") 173 | end 174 | end 175 | 176 | -- returns the number of characters in a UTF-8 string 177 | local function utf8len (s) 178 | -- argument checking 179 | if type(s) ~= "string" then 180 | for k,v in pairs(s) do print('"',tostring(k),'"',tostring(v),'"') end 181 | error("bad argument #1 to 'utf8len' (string expected, got ".. type(s).. ")") 182 | end 183 | 184 | local pos = 1 185 | local bytes = len(s) 186 | local length = 0 187 | 188 | while pos <= bytes do 189 | length = length + 1 190 | pos = pos + utf8charbytes(s, pos) 191 | end 192 | 193 | return length 194 | end 195 | 196 | -- functions identically to string.sub except that i and j are UTF-8 characters 197 | -- instead of bytes 198 | local function utf8sub (s, i, j) 199 | -- argument defaults 200 | j = j or -1 201 | 202 | local pos = 1 203 | local bytes = len(s) 204 | local length = 0 205 | 206 | -- only set l if i or j is negative 207 | local l = (i >= 0 and j >= 0) or utf8len(s) 208 | local startChar = (i >= 0) and i or l + i + 1 209 | local endChar = (j >= 0) and j or l + j + 1 210 | 211 | -- can't have start before end! 212 | if startChar > endChar then 213 | return "" 214 | end 215 | 216 | -- byte offsets to pass to string.sub 217 | local startByte,endByte = 1,bytes 218 | 219 | while pos <= bytes do 220 | length = length + 1 221 | 222 | if length == startChar then 223 | startByte = pos 224 | end 225 | 226 | pos = pos + utf8charbytes(s, pos) 227 | 228 | if length == endChar then 229 | endByte = pos - 1 230 | break 231 | end 232 | end 233 | 234 | if startChar > length then startByte = bytes+1 end 235 | if endChar < 1 then endByte = 0 end 236 | 237 | return sub(s, startByte, endByte) 238 | end 239 | 240 | --[[ 241 | -- replace UTF-8 characters based on a mapping table 242 | local function utf8replace (s, mapping) 243 | -- argument checking 244 | if type(s) ~= "string" then 245 | error("bad argument #1 to 'utf8replace' (string expected, got ".. type(s).. ")") 246 | end 247 | if type(mapping) ~= "table" then 248 | error("bad argument #2 to 'utf8replace' (table expected, got ".. type(mapping).. ")") 249 | end 250 | 251 | local pos = 1 252 | local bytes = len(s) 253 | local charbytes 254 | local newstr = "" 255 | 256 | while pos <= bytes do 257 | charbytes = utf8charbytes(s, pos) 258 | local c = sub(s, pos, pos + charbytes - 1) 259 | 260 | newstr = newstr .. (mapping[c] or c) 261 | 262 | pos = pos + charbytes 263 | end 264 | 265 | return newstr 266 | end 267 | 268 | 269 | -- identical to string.upper except it knows about unicode simple case conversions 270 | local function utf8upper (s) 271 | return utf8replace(s, utf8_lc_uc) 272 | end 273 | 274 | -- identical to string.lower except it knows about unicode simple case conversions 275 | local function utf8lower (s) 276 | return utf8replace(s, utf8_uc_lc) 277 | end 278 | ]] 279 | 280 | -- identical to string.reverse except that it supports UTF-8 281 | local function utf8reverse (s) 282 | -- argument checking 283 | if type(s) ~= "string" then 284 | error("bad argument #1 to 'utf8reverse' (string expected, got ".. type(s).. ")") 285 | end 286 | 287 | local bytes = len(s) 288 | local pos = bytes 289 | local charbytes 290 | local newstr = "" 291 | 292 | while pos > 0 do 293 | local c = byte(s, pos) 294 | while c >= 128 and c <= 191 do 295 | pos = pos - 1 296 | c = byte(s, pos) 297 | end 298 | 299 | charbytes = utf8charbytes(s, pos) 300 | 301 | newstr = newstr .. sub(s, pos, pos + charbytes - 1) 302 | 303 | pos = pos - 1 304 | end 305 | 306 | return newstr 307 | end 308 | 309 | -- http://en.wikipedia.org/wiki/Utf8 310 | -- http://developer.coronalabs.com/code/utf-8-conversion-utility 311 | local function utf8char(unicode) 312 | if unicode <= 0x7F then return char(unicode) end 313 | 314 | if (unicode <= 0x7FF) then 315 | local Byte0 = 0xC0 + math.floor(unicode / 0x40); 316 | local Byte1 = 0x80 + (unicode % 0x40); 317 | return char(Byte0, Byte1); 318 | end; 319 | 320 | if (unicode <= 0xFFFF) then 321 | local Byte0 = 0xE0 + math.floor(unicode / 0x1000); 322 | local Byte1 = 0x80 + (math.floor(unicode / 0x40) % 0x40); 323 | local Byte2 = 0x80 + (unicode % 0x40); 324 | return char(Byte0, Byte1, Byte2); 325 | end; 326 | 327 | if (unicode <= 0x10FFFF) then 328 | local code = unicode 329 | local Byte3= 0x80 + (code % 0x40); 330 | code = math.floor(code / 0x40) 331 | local Byte2= 0x80 + (code % 0x40); 332 | code = math.floor(code / 0x40) 333 | local Byte1= 0x80 + (code % 0x40); 334 | code = math.floor(code / 0x40) 335 | local Byte0= 0xF0 + code; 336 | 337 | return char(Byte0, Byte1, Byte2, Byte3); 338 | end; 339 | 340 | error 'Unicode cannot be greater than U+10FFFF!' 341 | end 342 | 343 | local shift_6 = 2^6 344 | local shift_12 = 2^12 345 | local shift_18 = 2^18 346 | 347 | local utf8unicode 348 | utf8unicode = function(str, i, j, byte_pos) 349 | i = i or 1 350 | j = j or i 351 | 352 | if i > j then return end 353 | 354 | local ch,bytes 355 | 356 | if byte_pos then 357 | bytes = utf8charbytes(str,byte_pos) 358 | ch = sub(str,byte_pos,byte_pos-1+bytes) 359 | else 360 | ch,byte_pos = utf8sub(str,i,i), 0 361 | bytes = #ch 362 | end 363 | 364 | local unicode 365 | 366 | if bytes == 1 then unicode = byte(ch) end 367 | if bytes == 2 then 368 | local byte0,byte1 = byte(ch,1,2) 369 | local code0,code1 = byte0-0xC0,byte1-0x80 370 | unicode = code0*shift_6 + code1 371 | end 372 | if bytes == 3 then 373 | local byte0,byte1,byte2 = byte(ch,1,3) 374 | local code0,code1,code2 = byte0-0xE0,byte1-0x80,byte2-0x80 375 | unicode = code0*shift_12 + code1*shift_6 + code2 376 | end 377 | if bytes == 4 then 378 | local byte0,byte1,byte2,byte3 = byte(ch,1,4) 379 | local code0,code1,code2,code3 = byte0-0xF0,byte1-0x80,byte2-0x80,byte3-0x80 380 | unicode = code0*shift_18 + code1*shift_12 + code2*shift_6 + code3 381 | end 382 | 383 | return unicode,utf8unicode(str, i+1, j, byte_pos+bytes) 384 | end 385 | 386 | -- Returns an iterator which returns the next substring and its byte interval 387 | local function utf8gensub(str, sub_len) 388 | sub_len = sub_len or 1 389 | local byte_pos = 1 390 | local length = #str 391 | return function(skip) 392 | if skip then byte_pos = byte_pos + skip end 393 | local char_count = 0 394 | local start = byte_pos 395 | repeat 396 | if byte_pos > length then return end 397 | char_count = char_count + 1 398 | local bytes = utf8charbytes(str,byte_pos) 399 | byte_pos = byte_pos+bytes 400 | 401 | until char_count == sub_len 402 | 403 | local last = byte_pos-1 404 | local slice = sub(str,start,last) 405 | return slice, start, last 406 | end 407 | end 408 | 409 | local function binsearch(sortedTable, item, comp) 410 | local head, tail = 1, #sortedTable 411 | local mid = math.floor((head + tail)/2) 412 | if not comp then 413 | while (tail - head) > 1 do 414 | if sortedTable[tonumber(mid)] > item then 415 | tail = mid 416 | else 417 | head = mid 418 | end 419 | mid = math.floor((head + tail)/2) 420 | end 421 | end 422 | if sortedTable[tonumber(head)] == item then 423 | return true, tonumber(head) 424 | elseif sortedTable[tonumber(tail)] == item then 425 | return true, tonumber(tail) 426 | else 427 | return false 428 | end 429 | end 430 | local function classMatchGenerator(class, plain) 431 | local codes = {} 432 | local ranges = {} 433 | local ignore = false 434 | local range = false 435 | local firstletter = true 436 | local unmatch = false 437 | 438 | local it = utf8gensub(class) 439 | 440 | local skip 441 | for c, _, be in it do 442 | skip = be 443 | if not ignore and not plain then 444 | if c == "%" then 445 | ignore = true 446 | elseif c == "-" then 447 | table.insert(codes, utf8unicode(c)) 448 | range = true 449 | elseif c == "^" then 450 | if not firstletter then 451 | error('!!!') 452 | else 453 | unmatch = true 454 | end 455 | elseif c == ']' then 456 | break 457 | else 458 | if not range then 459 | table.insert(codes, utf8unicode(c)) 460 | else 461 | table.remove(codes) -- removing '-' 462 | table.insert(ranges, {table.remove(codes), utf8unicode(c)}) 463 | range = false 464 | end 465 | end 466 | elseif ignore and not plain then 467 | if c == 'a' then -- %a: represents all letters. (ONLY ASCII) 468 | table.insert(ranges, {65, 90}) -- A - Z 469 | table.insert(ranges, {97, 122}) -- a - z 470 | elseif c == 'c' then -- %c: represents all control characters. 471 | table.insert(ranges, {0, 31}) 472 | table.insert(codes, 127) 473 | elseif c == 'd' then -- %d: represents all digits. 474 | table.insert(ranges, {48, 57}) -- 0 - 9 475 | elseif c == 'g' then -- %g: represents all printable characters except space. 476 | table.insert(ranges, {1, 8}) 477 | table.insert(ranges, {14, 31}) 478 | table.insert(ranges, {33, 132}) 479 | table.insert(ranges, {134, 159}) 480 | table.insert(ranges, {161, 5759}) 481 | table.insert(ranges, {5761, 8191}) 482 | table.insert(ranges, {8203, 8231}) 483 | table.insert(ranges, {8234, 8238}) 484 | table.insert(ranges, {8240, 8286}) 485 | table.insert(ranges, {8288, 12287}) 486 | elseif c == 'l' then -- %l: represents all lowercase letters. (ONLY ASCII) 487 | table.insert(ranges, {97, 122}) -- a - z 488 | elseif c == 'p' then -- %p: represents all punctuation characters. (ONLY ASCII) 489 | table.insert(ranges, {33, 47}) 490 | table.insert(ranges, {58, 64}) 491 | table.insert(ranges, {91, 96}) 492 | table.insert(ranges, {123, 126}) 493 | elseif c == 's' then -- %s: represents all space characters. 494 | table.insert(ranges, {9, 13}) 495 | table.insert(codes, 32) 496 | table.insert(codes, 133) 497 | table.insert(codes, 160) 498 | table.insert(codes, 5760) 499 | table.insert(ranges, {8192, 8202}) 500 | table.insert(codes, 8232) 501 | table.insert(codes, 8233) 502 | table.insert(codes, 8239) 503 | table.insert(codes, 8287) 504 | table.insert(codes, 12288) 505 | elseif c == 'u' then -- %u: represents all uppercase letters. (ONLY ASCII) 506 | table.insert(ranges, {65, 90}) -- A - Z 507 | elseif c == 'w' then -- %w: represents all alphanumeric characters. (ONLY ASCII) 508 | table.insert(ranges, {48, 57}) -- 0 - 9 509 | table.insert(ranges, {65, 90}) -- A - Z 510 | table.insert(ranges, {97, 122}) -- a - z 511 | elseif c == 'x' then -- %x: represents all hexadecimal digits. 512 | table.insert(ranges, {48, 57}) -- 0 - 9 513 | table.insert(ranges, {65, 70}) -- A - F 514 | table.insert(ranges, {97, 102}) -- a - f 515 | else 516 | if not range then 517 | table.insert(codes, utf8unicode(c)) 518 | else 519 | table.remove(codes) -- removing '-' 520 | table.insert(ranges, {table.remove(codes), utf8unicode(c)}) 521 | range = false 522 | end 523 | end 524 | ignore = false 525 | else 526 | if not range then 527 | table.insert(codes, utf8unicode(c)) 528 | else 529 | table.remove(codes) -- removing '-' 530 | table.insert(ranges, {table.remove(codes), utf8unicode(c)}) 531 | range = false 532 | end 533 | ignore = false 534 | end 535 | 536 | firstletter = false 537 | end 538 | 539 | table.sort(codes) 540 | 541 | local function inRanges(charCode) 542 | for _,r in ipairs(ranges) do 543 | if r[1] <= charCode and charCode <= r[2] then 544 | return true 545 | end 546 | end 547 | return false 548 | end 549 | if not unmatch then 550 | return function(charCode) 551 | return binsearch(codes, charCode) or inRanges(charCode) 552 | end, skip 553 | else 554 | return function(charCode) 555 | return charCode ~= -1 and not (binsearch(codes, charCode) or inRanges(charCode)) 556 | end, skip 557 | end 558 | end 559 | 560 | --[[ 561 | -- utf8sub with extra argument, and extra result value 562 | local function utf8subWithBytes (s, i, j, sb) 563 | -- argument defaults 564 | j = j or -1 565 | 566 | local pos = sb or 1 567 | local bytes = len(s) 568 | local length = 0 569 | 570 | -- only set l if i or j is negative 571 | local l = (i >= 0 and j >= 0) or utf8len(s) 572 | local startChar = (i >= 0) and i or l + i + 1 573 | local endChar = (j >= 0) and j or l + j + 1 574 | 575 | -- can't have start before end! 576 | if startChar > endChar then 577 | return "" 578 | end 579 | 580 | -- byte offsets to pass to string.sub 581 | local startByte,endByte = 1,bytes 582 | 583 | while pos <= bytes do 584 | length = length + 1 585 | 586 | if length == startChar then 587 | startByte = pos 588 | end 589 | 590 | pos = pos + utf8charbytes(s, pos) 591 | 592 | if length == endChar then 593 | endByte = pos - 1 594 | break 595 | end 596 | end 597 | 598 | if startChar > length then startByte = bytes+1 end 599 | if endChar < 1 then endByte = 0 end 600 | 601 | return sub(s, startByte, endByte), endByte + 1 602 | end 603 | ]] 604 | 605 | local cache = setmetatable({},{ 606 | __mode = 'kv' 607 | }) 608 | local cachePlain = setmetatable({},{ 609 | __mode = 'kv' 610 | }) 611 | local function matcherGenerator(regex, plain) 612 | local matcher = { 613 | functions = {}, 614 | captures = {} 615 | } 616 | if not plain then 617 | cache[regex] = matcher 618 | else 619 | cachePlain[regex] = matcher 620 | end 621 | local function simple(func) 622 | return function(cC) 623 | if func(cC) then 624 | matcher:nextFunc() 625 | matcher:nextStr() 626 | else 627 | matcher:reset() 628 | end 629 | end 630 | end 631 | local function star(func) 632 | return function(cC) 633 | if func(cC) then 634 | matcher:fullResetOnNextFunc() 635 | matcher:nextStr() 636 | else 637 | matcher:nextFunc() 638 | end 639 | end 640 | end 641 | local function minus(func) 642 | return function(cC) 643 | if func(cC) then 644 | matcher:fullResetOnNextStr() 645 | end 646 | matcher:nextFunc() 647 | end 648 | end 649 | local function question(func) 650 | return function(cC) 651 | if func(cC) then 652 | matcher:fullResetOnNextFunc() 653 | matcher:nextStr() 654 | end 655 | matcher:nextFunc() 656 | end 657 | end 658 | 659 | local function capture(id) 660 | return function(_) 661 | local l = matcher.captures[id][2] - matcher.captures[id][1] 662 | local captured = utf8sub(matcher.string, matcher.captures[id][1], matcher.captures[id][2]) 663 | local check = utf8sub(matcher.string, matcher.str, matcher.str + l) 664 | if captured == check then 665 | for _ = 0, l do 666 | matcher:nextStr() 667 | end 668 | matcher:nextFunc() 669 | else 670 | matcher:reset() 671 | end 672 | end 673 | end 674 | local function captureStart(id) 675 | return function(_) 676 | matcher.captures[id][1] = matcher.str 677 | matcher:nextFunc() 678 | end 679 | end 680 | local function captureStop(id) 681 | return function(_) 682 | matcher.captures[id][2] = matcher.str - 1 683 | matcher:nextFunc() 684 | end 685 | end 686 | 687 | local function balancer(str) 688 | local sum = 0 689 | local bc, ec = utf8sub(str, 1, 1), utf8sub(str, 2, 2) 690 | local skip = len(bc) + len(ec) 691 | bc, ec = utf8unicode(bc), utf8unicode(ec) 692 | return function(cC) 693 | if cC == ec and sum > 0 then 694 | sum = sum - 1 695 | if sum == 0 then 696 | matcher:nextFunc() 697 | end 698 | matcher:nextStr() 699 | elseif cC == bc then 700 | sum = sum + 1 701 | matcher:nextStr() 702 | else 703 | if sum == 0 or cC == -1 then 704 | sum = 0 705 | matcher:reset() 706 | else 707 | matcher:nextStr() 708 | end 709 | end 710 | end, skip 711 | end 712 | 713 | matcher.functions[1] = function(_) 714 | matcher:fullResetOnNextStr() 715 | matcher.seqStart = matcher.str 716 | matcher:nextFunc() 717 | if (matcher.str > matcher.startStr and matcher.fromStart) or matcher.str >= matcher.stringLen then 718 | matcher.stop = true 719 | matcher.seqStart = nil 720 | end 721 | end 722 | 723 | local lastFunc 724 | local ignore = false 725 | local skip = nil 726 | local it = (function() 727 | local gen = utf8gensub(regex) 728 | return function() 729 | return gen(skip) 730 | end 731 | end)() 732 | local cs = {} 733 | for c, bs, be in it do 734 | skip = nil 735 | if plain then 736 | table.insert(matcher.functions, simple(classMatchGenerator(c, plain))) 737 | else 738 | if ignore then 739 | if find('123456789', c, 1, true) then 740 | if lastFunc then 741 | table.insert(matcher.functions, simple(lastFunc)) 742 | lastFunc = nil 743 | end 744 | table.insert(matcher.functions, capture(tonumber(c))) 745 | elseif c == 'b' then 746 | if lastFunc then 747 | table.insert(matcher.functions, simple(lastFunc)) 748 | lastFunc = nil 749 | end 750 | local b 751 | b, skip = balancer(sub(regex, be + 1, be + 9)) 752 | table.insert(matcher.functions, b) 753 | else 754 | lastFunc = classMatchGenerator('%' .. c) 755 | end 756 | ignore = false 757 | else 758 | if c == '*' then 759 | if lastFunc then 760 | table.insert(matcher.functions, star(lastFunc)) 761 | lastFunc = nil 762 | else 763 | error('invalid regex after ' .. sub(regex, 1, bs)) 764 | end 765 | elseif c == '+' then 766 | if lastFunc then 767 | table.insert(matcher.functions, simple(lastFunc)) 768 | table.insert(matcher.functions, star(lastFunc)) 769 | lastFunc = nil 770 | else 771 | error('invalid regex after ' .. sub(regex, 1, bs)) 772 | end 773 | elseif c == '-' then 774 | if lastFunc then 775 | table.insert(matcher.functions, minus(lastFunc)) 776 | lastFunc = nil 777 | else 778 | error('invalid regex after ' .. sub(regex, 1, bs)) 779 | end 780 | elseif c == '?' then 781 | if lastFunc then 782 | table.insert(matcher.functions, question(lastFunc)) 783 | lastFunc = nil 784 | else 785 | error('invalid regex after ' .. sub(regex, 1, bs)) 786 | end 787 | elseif c == '^' then 788 | if bs == 1 then 789 | matcher.fromStart = true 790 | else 791 | error('invalid regex after ' .. sub(regex, 1, bs)) 792 | end 793 | elseif c == '$' then 794 | if be == len(regex) then 795 | matcher.toEnd = true 796 | else 797 | error('invalid regex after ' .. sub(regex, 1, bs)) 798 | end 799 | elseif c == '[' then 800 | if lastFunc then 801 | table.insert(matcher.functions, simple(lastFunc)) 802 | end 803 | lastFunc, skip = classMatchGenerator(sub(regex, be + 1)) 804 | elseif c == '(' then 805 | if lastFunc then 806 | table.insert(matcher.functions, simple(lastFunc)) 807 | lastFunc = nil 808 | end 809 | table.insert(matcher.captures, {}) 810 | table.insert(cs, #matcher.captures) 811 | table.insert(matcher.functions, captureStart(cs[#cs])) 812 | if sub(regex, be + 1, be + 1) == ')' then matcher.captures[#matcher.captures].empty = true end 813 | elseif c == ')' then 814 | if lastFunc then 815 | table.insert(matcher.functions, simple(lastFunc)) 816 | lastFunc = nil 817 | end 818 | local cap = table.remove(cs) 819 | if not cap then 820 | error('invalid capture: "(" missing') 821 | end 822 | table.insert(matcher.functions, captureStop(cap)) 823 | elseif c == '.' then 824 | if lastFunc then 825 | table.insert(matcher.functions, simple(lastFunc)) 826 | end 827 | lastFunc = function(cC) return cC ~= -1 end 828 | elseif c == '%' then 829 | ignore = true 830 | else 831 | if lastFunc then 832 | table.insert(matcher.functions, simple(lastFunc)) 833 | end 834 | lastFunc = classMatchGenerator(c) 835 | end 836 | end 837 | end 838 | end 839 | if #cs > 0 then 840 | error('invalid capture: ")" missing') 841 | end 842 | if lastFunc then 843 | table.insert(matcher.functions, simple(lastFunc)) 844 | end 845 | 846 | table.insert(matcher.functions, function() 847 | if matcher.toEnd and matcher.str ~= matcher.stringLen then 848 | matcher:reset() 849 | else 850 | matcher.stop = true 851 | end 852 | end) 853 | 854 | matcher.nextFunc = function(self) 855 | self.func = self.func + 1 856 | end 857 | matcher.nextStr = function(self) 858 | self.str = self.str + 1 859 | end 860 | matcher.strReset = function(self) 861 | local oldReset = self.reset 862 | local str = self.str 863 | self.reset = function(s) 864 | s.str = str 865 | s.reset = oldReset 866 | end 867 | end 868 | matcher.fullResetOnNextFunc = function(self) 869 | local oldReset = self.reset 870 | local func = self.func +1 871 | local str = self.str 872 | self.reset = function(s) 873 | s.func = func 874 | s.str = str 875 | s.reset = oldReset 876 | end 877 | end 878 | matcher.fullResetOnNextStr = function(self) 879 | local oldReset = self.reset 880 | local str = self.str + 1 881 | local func = self.func 882 | self.reset = function(s) 883 | s.func = func 884 | s.str = str 885 | s.reset = oldReset 886 | end 887 | end 888 | 889 | matcher.process = function(self, str, start) 890 | 891 | self.func = 1 892 | start = start or 1 893 | self.startStr = (start >= 0) and start or utf8len(str) + start + 1 894 | self.seqStart = self.startStr 895 | self.str = self.startStr 896 | self.stringLen = utf8len(str) + 1 897 | self.string = str 898 | self.stop = false 899 | 900 | self.reset = function(s) 901 | s.func = 1 902 | end 903 | 904 | -- local lastPos = self.str 905 | -- local lastByte 906 | local ch 907 | while not self.stop do 908 | if self.str < self.stringLen then 909 | --[[ if lastPos < self.str then 910 | print('last byte', lastByte) 911 | ch, lastByte = utf8subWithBytes(str, 1, self.str - lastPos - 1, lastByte) 912 | ch, lastByte = utf8subWithBytes(str, 1, 1, lastByte) 913 | lastByte = lastByte - 1 914 | else 915 | ch, lastByte = utf8subWithBytes(str, self.str, self.str) 916 | end 917 | lastPos = self.str ]] 918 | ch = utf8sub(str, self.str,self.str) 919 | --print('char', ch, utf8unicode(ch)) 920 | self.functions[self.func](utf8unicode(ch)) 921 | else 922 | self.functions[self.func](-1) 923 | end 924 | end 925 | 926 | if self.seqStart then 927 | local captures = {} 928 | for _,pair in pairs(self.captures) do 929 | if pair.empty then 930 | table.insert(captures, pair[1]) 931 | else 932 | table.insert(captures, utf8sub(str, pair[1], pair[2])) 933 | end 934 | end 935 | return self.seqStart, self.str - 1, unpack(captures) 936 | end 937 | end 938 | 939 | return matcher 940 | end 941 | 942 | -- string.find 943 | local function utf8find(str, regex, init, plain) 944 | local matcher = cache[regex] or matcherGenerator(regex, plain) 945 | return matcher:process(str, init) 946 | end 947 | 948 | -- string.match 949 | local function utf8match(str, regex, init) 950 | init = init or 1 951 | local found = {utf8find(str, regex, init)} 952 | if found[1] then 953 | if found[3] then 954 | return unpack(found, 3) 955 | end 956 | return utf8sub(str, found[1], found[2]) 957 | end 958 | end 959 | 960 | -- string.gmatch 961 | local function utf8gmatch(str, regex, all) 962 | regex = (utf8sub(regex,1,1) ~= '^') and regex or '%' .. regex 963 | local lastChar = 1 964 | return function() 965 | local found = {utf8find(str, regex, lastChar)} 966 | if found[1] then 967 | lastChar = found[2] + 1 968 | if found[all and 1 or 3] then 969 | return unpack(found, all and 1 or 3) 970 | end 971 | return utf8sub(str, found[1], found[2]) 972 | end 973 | end 974 | end 975 | 976 | local function replace(repl, args) 977 | local ret = '' 978 | if type(repl) == 'string' then 979 | local ignore = false 980 | local num 981 | for c in utf8gensub(repl) do 982 | if not ignore then 983 | if c == '%' then 984 | ignore = true 985 | else 986 | ret = ret .. c 987 | end 988 | else 989 | num = tonumber(c) 990 | if num then 991 | ret = ret .. args[num] 992 | else 993 | ret = ret .. c 994 | end 995 | ignore = false 996 | end 997 | end 998 | elseif type(repl) == 'table' then 999 | ret = repl[args[1] or args[0]] or '' 1000 | elseif type(repl) == 'function' then 1001 | if #args > 0 then 1002 | ret = repl(unpack(args, 1)) or '' 1003 | else 1004 | ret = repl(args[0]) or '' 1005 | end 1006 | end 1007 | return ret 1008 | end 1009 | -- string.gsub 1010 | local function utf8gsub(str, regex, repl, limit) 1011 | limit = limit or -1 1012 | local ret = '' 1013 | local prevEnd = 1 1014 | local it = utf8gmatch(str, regex, true) 1015 | local found = {it()} 1016 | local n = 0 1017 | while #found > 0 and limit ~= n do 1018 | local args = {[0] = utf8sub(str, found[1], found[2]), unpack(found, 3)} 1019 | ret = ret .. utf8sub(str, prevEnd, found[1] - 1) 1020 | .. replace(repl, args) 1021 | prevEnd = found[2] + 1 1022 | n = n + 1 1023 | found = {it()} 1024 | end 1025 | return ret .. utf8sub(str, prevEnd), n 1026 | end 1027 | 1028 | local utf8 = {} 1029 | utf8.len = utf8len 1030 | utf8.sub = utf8sub 1031 | utf8.reverse = utf8reverse 1032 | utf8.char = utf8char 1033 | utf8.unicode = utf8unicode 1034 | utf8.gensub = utf8gensub 1035 | utf8.byte = utf8unicode 1036 | utf8.find = utf8find 1037 | utf8.match = utf8match 1038 | utf8.gmatch = utf8gmatch 1039 | utf8.gsub = utf8gsub 1040 | utf8.dump = dump 1041 | utf8.format = format 1042 | utf8.lower = lower 1043 | utf8.upper = upper 1044 | utf8.rep = rep 1045 | utf8.charbytes = utf8charbytes 1046 | return utf8 1047 | -------------------------------------------------------------------------------- /MasafAutomation.lua: -------------------------------------------------------------------------------- 1 | include("karaskel.lua") 2 | 3 | local tr = aegisub.gettext 4 | local utf8 = require "utf8" 5 | local re = require "aegisub.re" 6 | --local inspect = require "inspect" 7 | 8 | add_background = tr "Masaf/Add Backgrounds" 9 | remove_background_lines = tr "Masaf/Remove all Background lines" 10 | 11 | ------------ Corrections ------------- 12 | rtl_correction_script_name = tr "Masaf/Correction/Rtl Correction - All lines" 13 | rtl_correction_selected_line = tr "Masaf/Correction/Rtl Correction - Selected lines" 14 | rtl_correction_without_normalize = tr "Masaf/Correction/Rtl Correction without normalize" 15 | add_rle = tr "Masaf/Correction/Add RLE - Selected lines" 16 | undo_rtl_correction = tr "Masaf/Correction/Undo Rtl Correction - Selected lines" 17 | convert_numbers_to_english = tr "Masaf/Correction/Numbers to English" 18 | convert_numbers_to_arabic = tr "Masaf/Correction/Numbers to Arabic" 19 | convert_numbers_to_persian = tr "Masaf/Correction/Numbers to Persian" 20 | selected_numbers_to_english = tr "Masaf/Correction/Selected Numbers to English" 21 | selected_numbers_to_arabic = tr "Masaf/Correction/Selected Numbers to Arabic" 22 | selected_numbers_to_persian = tr "Masaf/Correction/Selected Numbers to Persian" 23 | 24 | ------------ Timing ------------- 25 | shift_start_line_forward = tr "Masaf/Timing/Shift start line forward" 26 | shift_start_line_backward = tr "Masaf/Timing/Shift start line backward" 27 | shift_end_line_forward = tr "Masaf/Timing/Shift end line forward" 28 | shift_end_line_backward = tr "Masaf/Timing/Shift end line backward" 29 | make_next_line_continuous = tr "Masaf/Timing/Make next line continuous" 30 | make_same_time = tr "Masaf/Timing/Make Same time" 31 | make_same_start_time = tr "Masaf/Timing/Make Same Start time" 32 | make_same_end_time = tr "Masaf/Timing/Make Same End time" 33 | 34 | ------------ Text Movements ------------- 35 | move_last_text_part = tr "Masaf/Text Movement/Move last text part" 36 | move_first_part_of_next = tr "Masaf/Text Movement/Move first part of next" 37 | move_last_word = tr "Masaf/Text Movement/Move last word" 38 | move_first_word_of_next = tr "Masaf/Text Movement/Move first word of next" 39 | shift_line_break = tr "Masaf/Text Movement/Shift Linebreak" 40 | shift_line_break_back = tr "Masaf/Text Movement/Shift Linebreak Back" 41 | 42 | split_script_name = tr "Masaf/Split line" 43 | split_at_index_script_name = tr "Masaf/Split line at Index" 44 | break_semi_long_lines = tr "Masaf/Break Semi Long lines" 45 | break_selected_line = tr "Masaf/Break Selected line" 46 | show_rtl_editor_script_name = tr "Masaf/Show Rtl Editor" 47 | remove_line_break_script_name = tr "Masaf/Remove line Breaks" 48 | remove_position_tags = tr "Masaf/Remove Position tags" 49 | select_playing_line = tr "Masaf/Select playing line" 50 | generate_srt_like_text = tr "Masaf/Generate SRT like text" 51 | 52 | ------------ Special Tags ------------ 53 | fix_line_position = tr "Masaf/Special Tags/Fix line Position" 54 | set_line_as_no_background = tr "Masaf/Special Tags/Set line as No Background" 55 | set_line_as_dont_correct_rtl = tr "Masaf/Special Tags/Set line as Don't Correct RTL" 56 | set_line_as_dont_remove = tr "Masaf/Special Tags/Set line as Don't Remove" 57 | 58 | ------------ Miscs ------------ 59 | unify_background_lines_script_name = tr "Masaf/Misc/Unify Background lines" 60 | add_code_to_selected_lines_script_name = tr "Masaf/Misc/Add Code to Selected lines" 61 | import_text_to_selected_lines = tr "Masaf/Misc/Import text to selected Lines" 62 | display_sum_of_times = tr "Masaf/Misc/Display sum of times" 63 | go_to_line = tr "Masaf/Misc/Go to line" 64 | 65 | script_description = tr "Some Aegisub automation scripts specially designed for Right-To-Left language subtitles" 66 | script_author = "Majid Shamkhani" 67 | script_version = "1.26.0" 68 | 69 | -- <<<<<<<<<<<<<<<<<<<<<<<<< Main Methods >>>>>>>>>>>>>>>>>>>>>>>>> 70 | 71 | BgPatternRegex = 72 | [[\{\\p1.*?\}m (\d+(\.\d+)?) (\d+(\.\d+)?) l (\d+(\.\d+)?) (\d+(\.\d+)?) l (\d+(\.\d+)?) (\d+(\.\d+)?) l (\d+(\.\d+)?) (\d+(\.\d+)?) l (\d+(\.\d+)?) (\d+(\.\d+)?)]] 73 | PosPattern = "{\\pos%(.-%)}" 74 | BgPosPattern = "\\pos%(.-%)" 75 | SplitChars = {"||", "\\N", "%.", ",", "،", ";", "%?", "؟", "!", ":", "؛", "۔"} 76 | WeakChars = "~!@#\\$%\\^&\\*\\-\\+=;\\|×÷٪\\?؟\\\\" 77 | PunctuationMarks = [[%.,،%?؟:؛!;۔]] 78 | PunctuationMarksRegex = "[\\.,،\\?؟:؛!;۔]+" 79 | StartingBracketChars = [[%({%[<«“]] 80 | EndingsBracketChars = [[%)}%]>»”]] 81 | CodePattern = "({.-})" 82 | LastPunctuationMark = "([\\.,\\?!؟:،۔؛]\\s*$)" 83 | 84 | LreChar = utf8.char(0x202A) 85 | RleChar = utf8.char(0x202B) 86 | PdfChar = utf8.char(0x202C) 87 | 88 | FixedPosTag = "\\fixedpos" 89 | NoBgTag = "\\nobg" -- No Background 90 | DcrtlTag = "\\dcrtl" -- Dont Correct RTL 91 | Drl = "\\drl" -- Dont Remove Line 92 | 93 | -- ------------------------- AddBackground --------------------- 94 | 95 | function AddBackground(subs) 96 | if not videoLoaded() then 97 | return 98 | end 99 | 100 | local meta, styles = karaskel.collect_head(subs) 101 | -- start processing lines 102 | local i, n = 0, #subs 103 | n = subs.n 104 | local periorEndTime = "" 105 | local groupBackgroundIndex = -1 106 | local groupCount = 0 107 | 108 | local bgShape, doExit = getBackgroundLine(subs, styles) 109 | 110 | -- Missing background shape 111 | -- Adding new shape line and exit 112 | if doExit then 113 | return 114 | end 115 | 116 | -- Comment background line 117 | bgShape.comment = true 118 | subs[bgShape.i] = bgShape 119 | 120 | local positionTag = getPositionTag(bgShape.text) 121 | 122 | local secondForContinuousBackground, dialogOk = 123 | getNumberFromUser("\r\n Enter maximum second to make background continious: \r\n", 1) 124 | 125 | if not dialogOk then 126 | return 127 | end 128 | 129 | local lastLineStyle = nil 130 | while i < n do 131 | i = i + 1 132 | 133 | aegisub.progress.task("Processing line " .. i .. "/" .. n) 134 | aegisub.progress.set(i / n * 100) 135 | 136 | local l = subs[i] 137 | if l.class == "dialogue" and l.effect == "" and not l.comment and shouldAddBackground(l) then 138 | -- remove already added background line 139 | if bgShape ~= nil and i ~= bgShape.i and isBackgroundLine(l) then 140 | if canRemoveBackground(l) then 141 | subs.delete(i) 142 | i = i - 1 143 | n = n - 1 144 | end 145 | goto continue 146 | end 147 | 148 | -- Set text style align to 5 once. 149 | if lastLineStyle == nil or lastLineStyle ~= l.name then 150 | lastLineStyle = changeStyleAlignToFive(subs, styles, l) 151 | end 152 | 153 | if not string.find(l.text, FixedPosTag) then 154 | l.text = addPositionTag(l.text, positionTag) 155 | end 156 | subs[i] = l 157 | 158 | local startTimeEqualsPeriorEndTime = isStartTimeEqualsPeriorEndTime(l, periorEndTime, secondForContinuousBackground) 159 | 160 | if not startTimeEqualsPeriorEndTime then 161 | l.i = i 162 | l.comment = false 163 | local bgLine = generateBackground(l, bgShape) 164 | if groupBackgroundIndex ~= -1 and groupCount > 0 then 165 | setLastGroupBackgroundEndTime(subs, groupBackgroundIndex, periorEndTime) 166 | groupCount = 0 167 | end 168 | subs.insert(i, bgLine) 169 | groupBackgroundIndex = i 170 | i = i + 1 171 | n = n + 1 172 | else 173 | groupCount = groupCount + 1 174 | end 175 | 176 | periorEndTime = l.end_time 177 | 178 | ::continue:: 179 | end 180 | end 181 | 182 | if groupCount > 0 then 183 | setLastGroupBackgroundEndTime(subs, groupBackgroundIndex, periorEndTime) 184 | end 185 | 186 | aegisub.set_undo_point(add_background) 187 | end 188 | 189 | ------------------------------ Split Line ----------------------------- 190 | 191 | function Split(subs, selected) 192 | if #selected > 1 then 193 | return 194 | end 195 | local index = selected[1] 196 | local textParts = {} 197 | local line = subs[index] 198 | local text = line.text 199 | 200 | local line2 = table.copy(line) 201 | 202 | -- Finding manual splittnig symbol -> || 203 | s, e = utf8.find(text, SplitChars[1]) 204 | if s then 205 | line.text = utf8.sub(text, 1, s - 1) 206 | line2.text = utf8.sub(text, e + 1, utf8.len(text)) 207 | line.text = rtlCorrectNonCodeText(trim(line.text)) 208 | line2.text = rtlCorrectNonCodeText(trim(line2.text)) 209 | changeLineTimeAfterSplit(text, line, line2) 210 | subs[index] = line 211 | subs.insert(index + 1, line2) 212 | goto continue 213 | end 214 | 215 | textParts = getSubtitleTextParts(text) 216 | 217 | s, e, idx = getFirstChar(text, textParts) 218 | if idx > 0 then 219 | -- Remove split char from end of text 220 | if idx <= 2 then 221 | line.text = utf8.sub(text, 1, s - 1) 222 | line2.text = utf8.sub(text, e + 1, utf8.len(text)) 223 | else 224 | line.text = utf8.sub(text, 1, e) 225 | line2.text = utf8.sub(text, e + 1, utf8.len(text)) 226 | end 227 | changeLineTimeAfterSplit(text, line, line2) 228 | line.text = rtlCorrectNonCodeText(trim(line.text)) 229 | line2.text = rtlCorrectNonCodeText(trim(line2.text)) 230 | subs[index] = line 231 | subs.insert(index + 1, line2) 232 | end 233 | 234 | ::continue:: 235 | 236 | aegisub.set_undo_point(split_script_name) 237 | return selected 238 | end 239 | 240 | -- -------------------------SplitAtIndex --------------------- 241 | 242 | function SplitAtIndex(subs, selected) 243 | if #selected > 1 then 244 | return 245 | end 246 | local line = subs[selected[1]] 247 | 248 | text = line.text 249 | 250 | line2 = table.copy(line) 251 | 252 | local idx, dialogOk = 253 | getNumberFromUser("\r\n Enter index of character that you want to split line on that character: \r\n", 2) 254 | 255 | if not dialogOk then 256 | return 257 | end 258 | 259 | local s, e, idx = getCharAtIndex(text, idx) 260 | if s then 261 | -- Remove split char from end of text 262 | if idx <= 2 then 263 | line.text = utf8.sub(text, 1, s - 1) 264 | line2.text = utf8.sub(text, e + 1, utf8.len(text)) 265 | else 266 | line.text = utf8.sub(text, 1, e) 267 | line2.text = utf8.sub(text, e + 1, utf8.len(text)) 268 | end 269 | changeLineTimeAfterSplit(text, line, line2) 270 | line.text = rtlCorrectNonCodeText(trim(line.text)) 271 | line2.text = rtlCorrectNonCodeText(trim(line2.text)) 272 | subs[selected[1]] = line 273 | subs.insert(selected[1] + 1, line2) 274 | end 275 | 276 | aegisub.set_undo_point(split_at_index_script_name) 277 | return selected 278 | end 279 | 280 | function BreakSemiLongLines(subs) 281 | if not videoLoaded() then 282 | return 283 | end 284 | local i, n = 0, #subs 285 | local meta, styles = karaskel.collect_head(subs) 286 | local videoWidth = getVideoWidth() 287 | n = subs.n 288 | while i < n do 289 | i = i + 1 290 | local l = subs[i] 291 | if l.class == "dialogue" and l.effect == "" and not l.comment and notBreakedText(l.text) and not isBackgroundLine(l) then 292 | local textWidth = getTextWidth(l, styles) 293 | local breakToleranse = (videoWidth / 5) * 3 294 | if textWidth >= breakToleranse then 295 | l.text = autoBreakLine(l.text) 296 | subs[i] = l 297 | end 298 | end 299 | end 300 | aegisub.set_undo_point(break_semi_long_lines) 301 | end 302 | 303 | function BreakSelectedLine(subs, selected) 304 | if #selected > 1 then 305 | return 306 | end 307 | 308 | local l = subs[selected[1]] 309 | local meta, styles = karaskel.collect_head(subs) 310 | 311 | if l.class == "dialogue" and l.effect == "" and not l.comment and notBreakedText(l.text) and not isBackgroundLine(l) then 312 | local textWidth = getTextWidth(l, styles) 313 | l.text = autoBreakLine(l.text) 314 | subs[selected[1]] = l 315 | end 316 | aegisub.set_undo_point(break_selected_line) 317 | end 318 | 319 | --------------------------- RtlCorrection --------------------- 320 | 321 | function RtlCorrection(subs) 322 | -- start processing lines 323 | local i, n = 0 324 | n = subs.n 325 | 326 | while i < n do 327 | i = i + 1 328 | local l = subs[i] 329 | if l.class == "dialogue" and l.effect == "" and not l.comment and canCorrectRtl(l.text) then 330 | if not isBackgroundLine(l) then 331 | local parts = getSubtitleTextParts(l.text) 332 | local text = "" 333 | for k = 1, #parts do 334 | local t = parts[k] 335 | t = rtlCorrectNonCodeText(t) 336 | text = text .. t 337 | end 338 | l.text = text 339 | subs[i] = l 340 | end 341 | end 342 | end 343 | 344 | aegisub.set_undo_point(rtl_correction_script_name) 345 | end 346 | 347 | ------------------------- Rtl Corrector Selected Line ----------------------- 348 | 349 | function RtlCorrectorSelectedLine(subs, selected) 350 | for i = 1, #selected, 1 do 351 | local line = subs[selected[i]] 352 | 353 | -- start processing lines 354 | 355 | if (not isBackgroundLine(line)) then 356 | line.text = rtlCorrectIfAllowed(line.text) 357 | subs[selected[i]] = line 358 | end 359 | end 360 | 361 | aegisub.set_undo_point(rtl_correction_selected_line) 362 | end 363 | 364 | ------------------------- Rtl Corrector without Normalize ----------------------- 365 | 366 | function RtlCorrectorWithoutNormalize(subs, selected) 367 | for i = 1, #selected, 1 do 368 | local line = subs[selected[i]] 369 | 370 | -- start processing lines 371 | 372 | if (not isBackgroundLine(line)) then 373 | line.text = rtlCorrectTextWithCode(line.text, true) 374 | subs[selected[i]] = line 375 | end 376 | end 377 | 378 | aegisub.set_undo_point(rtl_correction_without_normalize) 379 | end 380 | 381 | ---------------------- Add RLE ----------------------- 382 | 383 | function AddRle(subs, selected) 384 | for i = 1, #selected, 1 do 385 | local line = subs[selected[i]] 386 | 387 | -- start processing lines 388 | 389 | if (not isBackgroundLine(line)) then 390 | local text, code = removeRtlChars(line.text), "" 391 | local textParts = getSubtitleTextParts(text) 392 | code, text = getCodeAndPlainTextPart(text, textParts) 393 | text = addRleToBeginigOfLine(text) 394 | 395 | line.text = code .. text 396 | subs[selected[i]] = line 397 | end 398 | end 399 | 400 | aegisub.set_undo_point(add_rle) 401 | end 402 | 403 | ------------------------------ Undo Rtl Correction ---------------------------- 404 | 405 | function UndoRtlCorrection(subs, selected) 406 | for i = 1, #selected, 1 do 407 | local line = subs[selected[i]] 408 | line.text = removeRtlChars(line.text) 409 | subs[selected[i]] = line 410 | end 411 | aegisub.set_undo_point(undo_rtl_correction) 412 | end 413 | 414 | ------------------------------ Show Rtl Editor --------------------------------- 415 | 416 | function ShowRtlEditor(subs, selected) 417 | if #selected > 1 then 418 | return 419 | end 420 | local line = subs[selected[1]] 421 | local textParts = getSubtitleTextParts(line.text) 422 | local codeText, plainText = getCodeAndPlainTextPart(line.text, textParts) 423 | local sourceText = utf8.gsub(plainText, "\\N", "\n") 424 | local result, newText = openEditor(sourceText) 425 | 426 | if not result then 427 | return 428 | end 429 | -- Replace line break with \N 430 | newText = utf8.gsub(newText, "\n", "\\N") 431 | if canCorrectRtl(codeText) then 432 | newText = rtlCorrectNonCodeText(newText) 433 | end 434 | line.text = codeText .. newText 435 | subs[selected[1]] = line 436 | 437 | aegisub.set_undo_point(show_rtl_editor_script_name) 438 | end 439 | 440 | --------------------------- Unify Background lines ------------------------------ 441 | 442 | function UnifyBackgroundLines(subs, selected) 443 | local firstLine, firstLineIdx = getFirstSubtitleLine(subs) 444 | if not isBackgroundLine(firstLine) then 445 | return 446 | end 447 | -- start processing lines 448 | local i, n = 0 449 | n = subs.n 450 | local lastBackgroundIdx = firstLineIdx 451 | 452 | while i < n do 453 | i = i + 1 454 | local l = subs[i] 455 | 456 | -- Prevent moving first line (Background shape) 457 | if l.class == "dialogue" and l.effect == "" and i ~= firstLineIdx then 458 | if isBackgroundLine(l) then 459 | lastBackgroundIdx = lastBackgroundIdx + 1 460 | subs.insert(lastBackgroundIdx, l) 461 | i = i + 1 462 | subs.delete(i) 463 | end 464 | end 465 | end 466 | 467 | aegisub.set_undo_point(unify_background_lines_script_name) 468 | end 469 | 470 | --------------------------- Add Code To Selected Lines ------------------------------ 471 | function AddCodeToSelectedLines(subs, selected) 472 | local code = getTextFromUser() 473 | if code == nil then 474 | return 475 | end 476 | for i = 1, #selected, 1 do 477 | local line = subs[selected[i]] 478 | line.text = code .. line.text 479 | subs[selected[i]] = line 480 | end 481 | aegisub.set_undo_point(add_code_to_selected_lines_script_name) 482 | end 483 | 484 | --------------------------- Remove line Breaks ------------------------------ 485 | function RemoveLineBreaks(subs, selected) 486 | for i = 1, #selected, 1 do 487 | local line = subs[selected[i]] 488 | line.text = utf8.gsub(line.text, "\\N", " ") 489 | line.text = removeDoubleSpace(line.text) 490 | subs[selected[i]] = line 491 | end 492 | aegisub.set_undo_point(remove_line_break_script_name) 493 | end 494 | 495 | ---------------------- Import Text to selected lines ------------------------- 496 | function ImportTextToSelectedLines(subs, selected) 497 | if #selected == 0 then 498 | return 499 | end 500 | local result, text = openEditor("") 501 | 502 | if not result then 503 | return 504 | end 505 | local texts = re.split(text, "\n") 506 | for i = 1, #selected, 1 do 507 | if i > table.getn(texts) then 508 | return 509 | end 510 | local line = subs[selected[i]] 511 | line.text = texts[i] 512 | subs[selected[i]] = line 513 | end 514 | 515 | aegisub.set_undo_point(import_text_to_selected_lines) 516 | end 517 | 518 | ---------------------- Select playing line ------------------------- 519 | function SelectPlayingLine(subs, selected) 520 | local vframe = aegisub.project_properties().video_position 521 | local fr2ms = aegisub.ms_from_frame 522 | 523 | local j = #selected 524 | if j < 1 or j == #subs then 525 | j = 1 526 | end 527 | for i = j, #subs do 528 | local line = subs[i] 529 | if line.class == "dialogue" and line.start_time >= fr2ms(vframe) then 530 | selected = {i - 1} 531 | return selected 532 | end 533 | end 534 | if j > 1 then 535 | for i = 1, j do 536 | local line = subs[i] 537 | if line.class == "dialogue" and line.start_time >= fr2ms(vframe) then 538 | selected = {i - 1} 539 | return selected 540 | end 541 | end 542 | end 543 | end 544 | 545 | ---------------------- Make next line continuous ------------------------- 546 | function MakeNextLineContinuous(subs, selected) 547 | if #selected == 0 or #selected > 1 then 548 | return 549 | end 550 | local index = selected[1] 551 | if index == subs.n then 552 | return 553 | end 554 | local line = subs[index] 555 | local nextLine = subs[index + 1] 556 | nextLine.start_time = line.end_time 557 | if nextLine.end_time == 0 then 558 | nextLine.end_time = line.end_time + (utf8.len(nextLine.text) * 100) 559 | end 560 | subs[index + 1] = nextLine 561 | selected = {index + 1} 562 | aegisub.set_undo_point(make_next_line_continuous) 563 | return selected 564 | end 565 | 566 | function MakeSameTime(subs, selected) 567 | if #selected < 2 then 568 | return 569 | end 570 | local firstLine = subs[selected[1]] 571 | for i = 2, #selected, 1 do 572 | local l = subs[selected[i]] 573 | l.start_time = firstLine.start_time 574 | l.end_time = firstLine.end_time 575 | subs[selected[i]] = l 576 | end 577 | aegisub.set_undo_point(make_same_time) 578 | end 579 | 580 | function MakeSameStartTime(subs, selected) 581 | if #selected < 2 then 582 | return 583 | end 584 | local firstLine = subs[selected[1]] 585 | for i = 2, #selected, 1 do 586 | local l = subs[selected[i]] 587 | l.start_time = firstLine.start_time 588 | subs[selected[i]] = l 589 | end 590 | aegisub.set_undo_point(make_same_start_time) 591 | end 592 | 593 | function MakeSameEndTime(subs, selected) 594 | if #selected < 2 then 595 | return 596 | end 597 | local firstLine = subs[selected[1]] 598 | for i = 2, #selected, 1 do 599 | local l = subs[selected[i]] 600 | l.end_time = firstLine.end_time 601 | subs[selected[i]] = l 602 | end 603 | aegisub.set_undo_point(make_same_end_time) 604 | end 605 | 606 | ---------------------- Start/End line shifter ------------------------- 607 | function ShiftStartLineForward(subs, selected) 608 | if #selected == 0 or #selected > 1 then 609 | return 610 | end 611 | local index = selected[1] 612 | local line = subs[index] 613 | line.start_time = line.start_time + 100 614 | subs[index] = line 615 | aegisub.set_undo_point(shift_start_line_forward) 616 | end 617 | 618 | function ShiftStartLineBackward(subs, selected) 619 | if #selected == 0 or #selected > 1 then 620 | return 621 | end 622 | local index = selected[1] 623 | local line = subs[index] 624 | line.start_time = line.start_time - 100 625 | subs[index] = line 626 | aegisub.set_undo_point(shift_start_line_backward) 627 | end 628 | 629 | function ShiftEndLineForward(subs, selected) 630 | if #selected == 0 or #selected > 1 then 631 | return 632 | end 633 | local index = selected[1] 634 | local line = subs[index] 635 | line.end_time = line.end_time + 100 636 | subs[index] = line 637 | aegisub.set_undo_point(shift_end_line_forward) 638 | end 639 | 640 | function ShiftEndLineBackward(subs, selected) 641 | if #selected == 0 or #selected > 1 then 642 | return 643 | end 644 | local index = selected[1] 645 | local line = subs[index] 646 | line.end_time = line.end_time - 100 647 | subs[index] = line 648 | aegisub.set_undo_point(shift_end_line_backward) 649 | end 650 | 651 | ---------------------- Move part of lines ------------------------- 652 | function MoveLastTextPart(subs, selected) 653 | if #selected == 0 or #selected > 1 then 654 | return 655 | end 656 | local index = selected[1] 657 | if index == subs.n then 658 | return 659 | end 660 | local line = subs[index] 661 | local nextLine = subs[index + 1] 662 | local oldLine = table.copy(line) 663 | 664 | local text, code = removeRtlChars(line.text), "" 665 | local textParts = getSubtitleTextParts(text) 666 | code, text = getCodeAndPlainTextPart(text, textParts) 667 | 668 | local parts = getTextSplitCharsParts(text) 669 | if #parts == 0 then 670 | return 671 | end 672 | local textParts = getTextPartsBySplitCharIndexes(parts, text) 673 | 674 | text = "" 675 | for i = 1, #textParts - 1, 1 do 676 | text = text .. textParts[i] .. " " 677 | end 678 | 679 | local nextText, nextCode = removeRtlChars(trim(nextLine.text)), "" 680 | local nextTextParts = getSubtitleTextParts(nextText) 681 | nextCode, nextText = getCodeAndPlainTextPart(nextText, nextTextParts) 682 | 683 | nextText = textParts[#textParts] .. " " .. nextText 684 | line.text = code .. rtlCorrectIfAllowed(text) 685 | nextLine.text = nextCode .. rtlCorrectIfAllowed(nextText) 686 | changeLineTimeAfterMove(oldLine, line, nextLine) 687 | subs[index] = line 688 | subs[index + 1] = nextLine 689 | 690 | aegisub.set_undo_point(move_last_text_part) 691 | end 692 | 693 | function MoveFirstPartOfNext(subs, selected) 694 | if #selected == 0 or #selected > 1 then 695 | return 696 | end 697 | local index = selected[1] 698 | if index == subs.n then 699 | return 700 | end 701 | local line = subs[index] 702 | local nextLine = subs[index + 1] 703 | 704 | local nextText, nextCode = "" 705 | nextText = removeRtlChars(nextLine.text) 706 | local nextTextParts = getSubtitleTextParts(nextText) 707 | nextCode, nextText = getCodeAndPlainTextPart(nextText, nextTextParts) 708 | 709 | local oldLine = table.copy(line) 710 | local parts = getTextSplitCharsParts(nextText) 711 | if #parts == 0 then 712 | return 713 | end 714 | local textParts = getTextPartsBySplitCharIndexes(parts, nextText) 715 | 716 | nextText = "" 717 | for i = 2, #textParts, 1 do 718 | nextText = nextText .. textParts[i] .. " " 719 | end 720 | 721 | nextText = nextCode .. nextText 722 | nextLine.text = rtlCorrectIfAllowed(nextText) 723 | 724 | local text = removeRtlChars(line.text) 725 | text = text .. " " .. trim(textParts[1]) 726 | line.text = rtlCorrectIfAllowed(text) 727 | 728 | changeLineTimeAfterMove(oldLine, line, nextLine) 729 | subs[index] = line 730 | subs[index + 1] = nextLine 731 | 732 | aegisub.set_undo_point(move_first_part_of_next) 733 | end 734 | 735 | ---------------------- Move words ------------------------- 736 | function MoveLastWord(subs, selected) 737 | if #selected == 0 or #selected > 1 then 738 | return 739 | end 740 | local index = selected[1] 741 | if index == subs.n then 742 | return 743 | end 744 | local line = subs[index] 745 | local nextLine = subs[index + 1] 746 | local text = trim(removeRtlChars(line.text)) 747 | local oldLine = table.copy(line) 748 | local lastWord = getLastWord(text) 749 | if lastWord == nil then 750 | return 751 | end 752 | 753 | local textLen = utf8.len(text) 754 | line.text = utf8.sub(text, 1, textLen - utf8.len(lastWord) - 1) 755 | 756 | local nextText, nextCode = removeRtlChars(nextLine.text), "" 757 | local textParts = getSubtitleTextParts(nextText) 758 | nextCode, nextText = getCodeAndPlainTextPart(nextText, textParts) 759 | 760 | nextText = nextCode .. lastWord .. " " .. nextText 761 | line.text = rtlCorrectIfAllowed(trim(line.text)) 762 | nextLine.text = rtlCorrectIfAllowed(trim(nextText)) 763 | changeLineTimeAfterMove(oldLine, line, nextLine) 764 | subs[index] = line 765 | subs[index + 1] = nextLine 766 | 767 | aegisub.set_undo_point(move_last_word) 768 | end 769 | 770 | function MoveFirstWordOfNext(subs, selected) 771 | if #selected == 0 or #selected > 1 then 772 | return 773 | end 774 | local index = selected[1] 775 | if index == subs.n then 776 | return 777 | end 778 | local line = subs[index] 779 | local nextLine = subs[index + 1] 780 | 781 | local text, code = removeRtlChars(trim(nextLine.text)), "" 782 | local textParts = getSubtitleTextParts(text) 783 | code, text = getCodeAndPlainTextPart(text, textParts) 784 | 785 | local oldLine = table.copy(line) 786 | local firstWord = getFirstWord(text) 787 | if firstWord == nil then 788 | return 789 | end 790 | 791 | line.text = line.text .. " " .. firstWord 792 | line.text = rtlCorrectIfAllowed(line.text) 793 | 794 | text = code .. utf8.sub(text, utf8.len(firstWord) + 2, utf8.len(text)) 795 | nextLine.text = rtlCorrectIfAllowed(text) 796 | 797 | changeLineTimeAfterMove(oldLine, line, nextLine) 798 | subs[index] = line 799 | subs[index + 1] = nextLine 800 | 801 | aegisub.set_undo_point(move_first_word_of_next) 802 | end 803 | 804 | ---------------------- Remove Position Tags ------------------------- 805 | function RemovePositionTags(subs) 806 | for i = 1, #subs do 807 | local l = subs[i] 808 | if l.class == "dialogue" and l.effect == "" and (not l.comment) and (not isBackgroundLine(l)) then 809 | l.text = removePosTag(l.text) 810 | subs[i] = l 811 | end 812 | end 813 | aegisub.set_undo_point(remove_position_tags) 814 | end 815 | 816 | ---------------------- Display sum of times ------------------------- 817 | function DisplaySumOfTimes(subs) 818 | local sum = 0 819 | for i = 1, #subs do 820 | local l = subs[i] 821 | if l.class == "dialogue" and l.effect == "" and (not l.comment) and (not isBackgroundLine(l)) then 822 | sum = sum + (l.end_time - l.start_time) 823 | end 824 | end 825 | 826 | local minutes = math.ceil(sum / 1000 / 60) 827 | local msg = "Total minutes = " .. tostring(minutes) 828 | msg = msg .. "\nTotal time = " .. secondsToClock(sum / 1000) 829 | showMessage(msg) 830 | end 831 | 832 | ---------------------- Generate SRT Like Text ------------------------- 833 | function GenerateSrtLikeText(subs) 834 | local sum = 0 835 | local srtText = "" 836 | local lineNumber = 0 837 | for i = 1, #subs do 838 | local l = subs[i] 839 | if l.class == "dialogue" and l.effect == "" then 840 | lineNumber = lineNumber + 1 841 | if (not l.comment) and (not isBackgroundLine(l)) then 842 | srtText = srtText .. lineNumber .. "\n" 843 | srtText = srtText .. secondsToClock(l.start_time / 1000) .. " --> " .. secondsToClock(l.end_time / 1000) .. "\n" 844 | srtText = srtText .. replaceLineBreak(cleanTags(l.text)) .. "\n" 845 | srtText = srtText .. "\n" 846 | end 847 | end 848 | end 849 | openEditor(srtText) 850 | end 851 | 852 | ---------------------- Remove Background Lines ------------------------- 853 | function RemoveBackgroundLines(subs) 854 | local i, n = 0, #subs 855 | n = subs.n 856 | while i < n do 857 | i = i + 1 858 | local l = subs[i] 859 | if l.class == "dialogue" and l.effect == "" and not l.comment and isBackgroundLine(l) then 860 | subs.delete(i) 861 | i = i - 1 862 | n = n - 1 863 | end 864 | end 865 | aegisub.set_undo_point(remove_background_lines) 866 | end 867 | 868 | ---------------------- Number Converters ------------------------- 869 | function ConvertNumbersToEnglish(subs) 870 | local i, n = 0 871 | n = subs.n 872 | 873 | while i < n do 874 | i = i + 1 875 | local l = subs[i] 876 | if l.class == "dialogue" and l.effect == "" and not l.comment then 877 | if not isBackgroundLine(l) then 878 | local parts = getSubtitleTextParts(l.text) 879 | local text = "" 880 | for k = 1, #parts do 881 | local t = parts[k] 882 | t = applyNumbersToEnglish(t) 883 | text = text .. t 884 | end 885 | l.text = text 886 | subs[i] = l 887 | end 888 | end 889 | end 890 | 891 | aegisub.set_undo_point(convert_numbers_to_english) 892 | end 893 | 894 | function SelectedNumbersToEnglish(subs, selected) 895 | for i = 1, #selected, 1 do 896 | local l = subs[selected[i]] 897 | 898 | if l.class == "dialogue" and l.effect == "" and not l.comment then 899 | if not isBackgroundLine(l) then 900 | local parts = getSubtitleTextParts(l.text) 901 | local text = "" 902 | for k = 1, #parts do 903 | local t = parts[k] 904 | t = applyNumbersToEnglish(t) 905 | text = text .. t 906 | end 907 | l.text = text 908 | subs[selected[i]] = l 909 | end 910 | end 911 | end 912 | 913 | aegisub.set_undo_point(selected_numbers_to_english) 914 | end 915 | 916 | function ConvertNumbersToArabic(subs) 917 | local i, n = 0 918 | n = subs.n 919 | 920 | while i < n do 921 | i = i + 1 922 | local l = subs[i] 923 | if l.class == "dialogue" and l.effect == "" and not l.comment then 924 | if not isBackgroundLine(l) then 925 | local parts = getSubtitleTextParts(l.text) 926 | local text = "" 927 | for k = 1, #parts do 928 | local t = parts[k] 929 | t = applyNumbersToArabic(t) 930 | text = text .. t 931 | end 932 | l.text = text 933 | subs[i] = l 934 | end 935 | end 936 | end 937 | 938 | aegisub.set_undo_point(convert_numbers_to_arabic) 939 | end 940 | 941 | function SelectedNumbersToArabic(subs, selected) 942 | for i = 1, #selected, 1 do 943 | local l = subs[selected[i]] 944 | 945 | if l.class == "dialogue" and l.effect == "" and not l.comment then 946 | if not isBackgroundLine(l) then 947 | local parts = getSubtitleTextParts(l.text) 948 | local text = "" 949 | for k = 1, #parts do 950 | local t = parts[k] 951 | t = applyNumbersToArabic(t) 952 | text = text .. t 953 | end 954 | l.text = text 955 | subs[selected[i]] = l 956 | end 957 | end 958 | end 959 | 960 | aegisub.set_undo_point(selected_numbers_to_arabic) 961 | end 962 | 963 | function ConvertNumbersToPersian(subs) 964 | local i, n = 0 965 | n = subs.n 966 | 967 | while i < n do 968 | i = i + 1 969 | local l = subs[i] 970 | if l.class == "dialogue" and l.effect == "" and not l.comment then 971 | if not isBackgroundLine(l) then 972 | local parts = getSubtitleTextParts(l.text) 973 | local text = "" 974 | for k = 1, #parts do 975 | local t = parts[k] 976 | t = applyNumbersToPersian(t) 977 | text = text .. t 978 | end 979 | l.text = text 980 | subs[i] = l 981 | end 982 | end 983 | end 984 | 985 | aegisub.set_undo_point(convert_numbers_to_arabic) 986 | end 987 | 988 | function SelectedNumbersToPersian(subs, selected) 989 | for i = 1, #selected, 1 do 990 | local l = subs[selected[i]] 991 | 992 | if l.class == "dialogue" and l.effect == "" and not l.comment then 993 | if not isBackgroundLine(l) then 994 | local parts = getSubtitleTextParts(l.text) 995 | local text = "" 996 | for k = 1, #parts do 997 | local t = parts[k] 998 | t = applyNumbersToPersian(t) 999 | text = text .. t 1000 | end 1001 | l.text = text 1002 | subs[selected[i]] = l 1003 | end 1004 | end 1005 | end 1006 | 1007 | aegisub.set_undo_point(selected_numbers_to_persian) 1008 | end 1009 | 1010 | function FixLinePosition(subs, selected) 1011 | addTag(subs, selected, FixedPosTag) 1012 | aegisub.set_undo_point(fix_line_position) 1013 | end 1014 | 1015 | function SetLineAsNoBackground(subs, selected) 1016 | addTag(subs, selected, NoBgTag) 1017 | aegisub.set_undo_point(set_line_as_no_background) 1018 | end 1019 | 1020 | function SetLineAsDontCorrectRtl(subs, selected) 1021 | addTag(subs, selected, DcrtlTag) 1022 | aegisub.set_undo_point(set_line_as_dont_correct_rtl) 1023 | end 1024 | 1025 | function SetLineAsDontRemove(subs, selected) 1026 | addTag(subs, selected, Drl) 1027 | aegisub.set_undo_point(set_line_as_dont_remove) 1028 | end 1029 | 1030 | function ShiftLineBreak(subs, selected) 1031 | if #selected > 1 then 1032 | return 1033 | end 1034 | 1035 | local line = subs[selected[1]] 1036 | 1037 | local text, code = removeRtlChars(line.text), "" 1038 | local textParts = getSubtitleTextParts(text) 1039 | code, text = getCodeAndPlainTextPart(text, textParts) 1040 | 1041 | text = utf8.gsub(text, "\\N", " \\N ") 1042 | text = removeDoubleSpace(trim(text)) 1043 | local parts = getWordList(text) 1044 | local i = getFirstLineBreakIndex(parts) 1045 | if i == 0 or i == #parts then 1046 | return text 1047 | end 1048 | 1049 | parts[i] = parts[i + 1] 1050 | parts[i + 1] = "\\N" 1051 | 1052 | text = table.concat(parts, " ") 1053 | if canCorrectRtl(code) then 1054 | text = rtlCorrectNonCodeText(text) 1055 | end 1056 | line.text = code .. text 1057 | subs[selected[1]] = line 1058 | aegisub.set_undo_point(shift_line_break) 1059 | end 1060 | 1061 | function ShiftLineBreakBack(subs, selected) 1062 | if #selected > 1 then 1063 | return 1064 | end 1065 | 1066 | local line = subs[selected[1]] 1067 | 1068 | local text, code = removeRtlChars(line.text), "" 1069 | local textParts = getSubtitleTextParts(text) 1070 | code, text = getCodeAndPlainTextPart(text, textParts) 1071 | 1072 | text = utf8.gsub(text, "\\N", " \\N ") 1073 | text = removeDoubleSpace(trim(text)) 1074 | local parts = getWordList(text) 1075 | local i = getFirstLineBreakIndex(parts) 1076 | if i == 0 or i == 1 then 1077 | return text 1078 | end 1079 | 1080 | parts[i] = parts[i - 1] 1081 | parts[i - 1] = "\\N" 1082 | 1083 | text = table.concat(parts, " ") 1084 | if canCorrectRtl(code) then 1085 | text = rtlCorrectNonCodeText(text) 1086 | end 1087 | line.text = code .. text 1088 | subs[selected[1]] = line 1089 | aegisub.set_undo_point(shift_line_break_back) 1090 | end 1091 | 1092 | ---------------------------- Go to line ------------------------- 1093 | 1094 | function GoToLine(subs, selected) 1095 | local lineNumber, dialogOk = 1096 | getNumberFromUser("\r\n Enter Line number: \r\n", "") 1097 | 1098 | if not dialogOk then 1099 | return 1100 | end 1101 | local currentLine = 0 1102 | for i = 1, #subs do 1103 | local line = subs[i] 1104 | if line.class == "dialogue" then 1105 | currentLine = currentLine + 1 1106 | if currentLine == tonumber(lineNumber) then 1107 | selected = {i} 1108 | return selected 1109 | end 1110 | end 1111 | end 1112 | end 1113 | 1114 | ------------------------- End of Main Methods ------------------- 1115 | 1116 | -- <<<<<<<<<<<<<<<<<<<<< Related Methods >>>>>>>>>>>>>>>>>>>>>>>> 1117 | 1118 | ---------------------- AddBackground Methods ------------------ 1119 | 1120 | function generateBackground(line, bgShape) 1121 | local bgLine = table.copy(line) 1122 | bgLine.text = bgShape.text 1123 | bgLine.style = bgShape.style 1124 | return bgLine 1125 | end 1126 | 1127 | function calcLineCount(line, styles) 1128 | local text = line.text 1129 | local videoWidth = getVideoWidth() 1130 | local lineCount = 0 1131 | if text:match([[\N]]) ~= nil then 1132 | local l = table.copy(line) 1133 | local lineParts = re.split(text, "\\\\N+") 1134 | for i, t in ipairs(lineParts) do 1135 | l.text = t 1136 | lineCount = lineCount + getNoneBreakedLineCount(l, videoWidth, styles) 1137 | end 1138 | else 1139 | lineCount = getNoneBreakedLineCount(line, videoWidth, styles) 1140 | end 1141 | return lineCount 1142 | end 1143 | 1144 | function getTextWidth(line, styles) 1145 | local cleanedText = cleanTags(line.text) 1146 | local w = aegisub.text_extents(styles[line.style], cleanedText) 1147 | return w 1148 | end 1149 | 1150 | function getTextHeight(styles, line) 1151 | local cleanedText = cleanTags(line.text) 1152 | local w, h = aegisub.text_extents(styles[line.style], cleanedText) 1153 | return h 1154 | end 1155 | 1156 | function getNoneBreakedLineCount(line, videoWidth, styles) 1157 | local stringWidth = getTextWidth(line, styles) 1158 | local margin = line.margin_l + line.margin_r 1159 | local drawableWidth = videoWidth - margin 1160 | return math.ceil(stringWidth / drawableWidth) 1161 | end 1162 | 1163 | function getVideoWidth() 1164 | local xres, yres = aegisub.video_size() 1165 | return xres 1166 | end 1167 | 1168 | function getVideoSize() 1169 | local xres, yres = aegisub.video_size() 1170 | return xres, yres 1171 | end 1172 | 1173 | function getBackgroundLine(subs, styles) 1174 | --aegisub.debug.out(subs[1].text) 1175 | local firstLine, i = getFirstSubtitleLine(subs) 1176 | 1177 | if firstLine == nil or not isBackgroundLine(firstLine) then 1178 | createBackgroundStyle(subs, styles) 1179 | createBackgroundLine(subs, firstLine, i) 1180 | showMessage( 1181 | tr [[The background shape is missing and now added as first line of subtitle. 1182 | Please do flowing steps: 1183 | 1- Change background size and position if needed. 1184 | 2- Use appropriate style for background. 1185 | 3- Run command again. 1186 | 1187 | Note: 1188 | The script will add background to all lines except lines containing {\nobg} command 1189 | or lines with style name ended with _NoBg word (e.g OnScreenText_NoBg)]] 1190 | ) 1191 | return nil, true 1192 | end 1193 | 1194 | return firstLine, false, i 1195 | end 1196 | 1197 | function getFirstSubtitleLine(subs) 1198 | for i, l in ipairs(subs) do 1199 | if l.class == "dialogue" then 1200 | l.i = i 1201 | return l, i 1202 | end 1203 | end 1204 | return nil, -1 1205 | end 1206 | 1207 | function isStartTimeEqualsPeriorEndTime(line, periorEndTime, secondForContinuousBackground) 1208 | if periorEndTime == "" then 1209 | return false 1210 | end 1211 | local diff = line.start_time - periorEndTime 1212 | return diff < secondForContinuousBackground * 1000 1213 | end 1214 | 1215 | function setLastGroupBackgroundEndTime(subs, groupBackgroundIndex, periorEndTime) 1216 | if groupBackgroundIndex == -1 then 1217 | return 1218 | end 1219 | local line = subs[groupBackgroundIndex] 1220 | line.end_time = periorEndTime 1221 | subs[groupBackgroundIndex] = line 1222 | end 1223 | 1224 | function createBackgroundStyle(subs, styles) 1225 | local style = styles["TextBackground"] 1226 | if style then 1227 | -- Set existing background style align to 5 1228 | style.align = 5 1229 | updateStyle(subs, style.name, style) 1230 | return 1231 | end 1232 | style = { 1233 | class = "style", 1234 | section = "V4+ Styles", 1235 | name = "TextBackground", 1236 | fontname = "Arial", 1237 | fontsize = "20", 1238 | color1 = "&H46000000&", 1239 | color2 = "&H000000FF&", 1240 | color3 = "&H00000000&", 1241 | color4 = "&H00000000&", 1242 | bold = false, 1243 | italic = false, 1244 | underline = false, 1245 | strikeout = false, 1246 | scale_x = 100, 1247 | scale_y = 100, 1248 | spacing = 0, 1249 | angle = 0, 1250 | borderstyle = 1, 1251 | outline = 0, 1252 | shadow = 0, 1253 | align = 5, 1254 | margin_l = 10, 1255 | margin_r = 10, 1256 | margin_t = 10, 1257 | margin_b = 10, 1258 | encoding = 1 1259 | } 1260 | subs.insert(styles.n, style) 1261 | end 1262 | 1263 | function createBackgroundLine(subs, line, idx) 1264 | local bgLine = table.copy(line) 1265 | local videoW, videoH = getVideoSize() 1266 | local margin = videoW / 64 1267 | local shapeHeight = margin * 7 1268 | bgLine.style = "TextBackground" 1269 | bgLine.text = 1270 | string.format( 1271 | "{\\p1\\pos(%d,%d)}m 0 0 l %d 0 l %d %d l 0 %d l 0 0", 1272 | videoW / 2, 1273 | videoH - shapeHeight + margin, 1274 | videoW - 1, 1275 | videoW - 1, 1276 | shapeHeight, 1277 | shapeHeight 1278 | ) 1279 | subs.insert(idx, bgLine) 1280 | end 1281 | 1282 | function isBackgroundLine(line) 1283 | return re.match(line.text, BgPatternRegex) ~= nil 1284 | end 1285 | 1286 | function videoLoaded() 1287 | local w = getVideoWidth() 1288 | if w == nil then 1289 | showMessage([[No video loaded. 1290 | Please "Open Video..." or "Use Dummy Video..." and try again.]]) 1291 | return false 1292 | end 1293 | return true 1294 | end 1295 | 1296 | function shouldAddBackground(line) 1297 | return (not string.find(line.style:lower(), "_nobg")) and (not string.find(line.text, NoBgTag)) or 1298 | (string.find(line.style:lower(), "_nobg") and (string.find(line.text, "\\addbg"))) 1299 | end 1300 | 1301 | function getPositionTag(text) 1302 | local pos = string.match(text, BgPosPattern) 1303 | if pos ~= nil then 1304 | return "{" .. pos .. "}" 1305 | end 1306 | return "" 1307 | end 1308 | 1309 | function addPositionTag(text, positionTag) 1310 | text = removePosTag(text) 1311 | text = positionTag .. text 1312 | return text 1313 | end 1314 | 1315 | function changeStyleAlignToFive(subs, styles, line) 1316 | local style = styles[line.style] 1317 | style.align = 5 1318 | style.outline = 0 1319 | style.shadow = 1 1320 | style.borderstyle = 1 1321 | updateStyle(subs, style.name, style) 1322 | return style.name 1323 | end 1324 | 1325 | function updateStyle(subs, styleName, style) 1326 | for i = 1, #subs do 1327 | local l = subs[i] 1328 | if l.class == "style" and l.name == styleName then 1329 | subs[i] = style 1330 | return 1331 | end 1332 | end 1333 | end 1334 | 1335 | function canRemoveBackground(line) 1336 | return not string.find(line.text, Drl) 1337 | end 1338 | 1339 | --------------------- SplitLine Methods ---------------------------- 1340 | 1341 | function getFirstChar(text, textParts) 1342 | local sStart = 0 1343 | local sEnd = 0 1344 | local idx = 0 1345 | 1346 | local codeText, plainText = getCodeAndPlainTextPart(text, textParts) 1347 | local codeLength = string.len(codeText) 1348 | sStart = string.len(plainText) 1349 | 1350 | for i = 1, #SplitChars do 1351 | local s, e = utf8.find(plainText, SplitChars[i]) 1352 | if s ~= nil and s < sStart then 1353 | sStart = s 1354 | sEnd = e 1355 | idx = i 1356 | end 1357 | end 1358 | return sStart + codeLength, sEnd + codeLength, idx 1359 | end 1360 | 1361 | function changeLineTimeAfterSplit(text, line1, line2) 1362 | local start = line1.start_time 1363 | local endt = line1.end_time 1364 | local dur = endt - start 1365 | --aegisub.log(dur) 1366 | local l = dur / utf8.len(text) 1367 | line1.end_time = start + utf8.len(line1.text) * l 1368 | line2.start_time = line1.end_time 1369 | return line1, line2 1370 | end 1371 | 1372 | function changeLineTimeAfterMove(oldLine, line1, line2) 1373 | local start, endt, dur, l = 0 1374 | 1375 | if line2.start_time == line2.end_time then 1376 | start = oldLine.start_time 1377 | endt = oldLine.end_time 1378 | dur = endt - start 1379 | l = dur / utf8.len(oldLine.text) 1380 | line1.end_time = start + utf8.len(line1.text) * l 1381 | else 1382 | start = line1.start_time 1383 | endt = line2.end_time 1384 | dur = endt - start 1385 | l = dur / (utf8.len(line1.text) + utf8.len(line2.text)) 1386 | line1.end_time = start + utf8.len(line1.text) * l 1387 | line2.start_time = line1.end_time 1388 | end 1389 | return line1, line2 1390 | end 1391 | 1392 | function getNumberFromUser(msg, defaultValue) 1393 | local config = { 1394 | {class = "label", label = msg, x = 0, y = 0}, 1395 | {class = "intedit", name = "inputNumber", value = defaultValue, x = 0, y = 1} 1396 | } 1397 | local btn, result = aegisub.dialog.display(config, {"OK", "Cancel"}, {ok = "OK", cancel = "Cancel"}) 1398 | if btn then 1399 | local r = tonumber(result.inputNumber) 1400 | return r, true 1401 | end 1402 | return 0, false 1403 | end 1404 | 1405 | function getCharAtIndex(text, index) 1406 | local parts = getTextSplitCharsParts(text) 1407 | 1408 | if #parts > 0 then 1409 | if index > #parts then 1410 | return nil 1411 | end 1412 | -- -1 means last index of array 1413 | -- if idx == 0 then 1414 | -- index = #parts 1415 | -- end 1416 | 1417 | -- returns start, end, SplitCharIndex 1418 | return parts[index][1], parts[index][2], parts[index][3] 1419 | end 1420 | return nil 1421 | end 1422 | 1423 | function getTextFromUser() 1424 | local config = { 1425 | {class = "label", label = "\r\n Enter your code here: \r\n", x = 0, y = 0}, 1426 | {class = "textbox", name = "txtCode", value = "{\\ }", x = 0, y = 1, width = 10} 1427 | } 1428 | local btn, result = aegisub.dialog.display(config, {"OK", "Cancel"}, {ok = "OK", cancel = "Cancel"}) 1429 | if btn then 1430 | return result.txtCode 1431 | end 1432 | return nil 1433 | end 1434 | 1435 | function autoBreakLine(lineText) 1436 | local text, code = removeRtlChars(lineText), "" 1437 | text = removeDoubleSpace(text) 1438 | local textParts = getSubtitleTextParts(text) 1439 | code, text = getCodeAndPlainTextPart(text, textParts) 1440 | 1441 | local wordList = getWordList(trim(text)) 1442 | local breakIndex = getCloserPunctuationMarkIndex(wordList) 1443 | -- if there is no punctuationMarks near the break tolerance 1444 | if breakIndex == 0 then 1445 | -- break from middle of text 1446 | breakIndex = math.floor(#wordList / 2) 1447 | end 1448 | 1449 | wordList[breakIndex] = wordList[breakIndex] .. " \\N " 1450 | text = table.concat(wordList, " ") 1451 | if canCorrectRtl(code) then 1452 | text = rtlCorrectNonCodeText(text) 1453 | end 1454 | return code .. text 1455 | end 1456 | 1457 | function getCloserPunctuationMarkIndex(wordList) 1458 | local tolerance = calcBreakTolerance(#wordList) 1459 | local breakIndex = 0 1460 | local closerIndex = tolerance 1461 | 1462 | for i, t in ipairs(wordList) do 1463 | if re.match(t, PunctuationMarksRegex) ~= nil then 1464 | local idx = math.abs(math.floor(#wordList / 2) - i) 1465 | if idx <= tolerance and idx < closerIndex then 1466 | idx = closerIndex 1467 | breakIndex = i 1468 | end 1469 | end 1470 | end 1471 | return breakIndex 1472 | end 1473 | 1474 | function calcBreakTolerance(wordCount) 1475 | local percent = 60 1476 | local x = math.floor((wordCount * percent) / 100) 1477 | local tolerance = math.ceil(x / 2) 1478 | return math.floor(wordCount / 2) - tolerance 1479 | end 1480 | 1481 | function notBreakedText(text) 1482 | return re.match(text, "\\\\N+") == nil 1483 | end 1484 | ----------------------- Rtl Correction Methods --------------------- 1485 | 1486 | function removeRtlChars(s) 1487 | local lroChar = utf8.char(0x202D) 1488 | local rloChar = utf8.char(0x202E) 1489 | local replaced = utf8.gsub(s, RleChar, "") 1490 | replaced = utf8.gsub(replaced, PdfChar, "") 1491 | replaced = utf8.gsub(replaced, LreChar, "") 1492 | replaced = utf8.gsub(replaced, lroChar, "") 1493 | replaced = utf8.gsub(replaced, rloChar, "") 1494 | return replaced 1495 | end 1496 | 1497 | function addRleToEachNoneAlphabeticChars(s) 1498 | -- All weak characters enclosed between numbers 1499 | local weakCharsWithinNumbers = "(\\d)([" .. WeakChars .. "])(\\d)" 1500 | 1501 | local replaced = s 1502 | while re.match(replaced, weakCharsWithinNumbers) ~= nil do 1503 | replaced = re.sub(replaced, weakCharsWithinNumbers, "\\1" .. RleChar .. "\\2" .. PdfChar .. "\\3") 1504 | end 1505 | 1506 | -- Make last punctuation mark RTL 1507 | replaced = re.sub(replaced, LastPunctuationMark, PdfChar .. RleChar .. "\\1") 1508 | replaced = utf8.gsub(replaced, "\\N", "\\N" .. RleChar) 1509 | return RleChar .. replaced 1510 | end 1511 | 1512 | function addRleToBeginigOfLine(s) 1513 | if isRtl(s) then 1514 | s = utf8.gsub(s, "\\N", "\\N" .. RleChar) 1515 | return RleChar .. s 1516 | end 1517 | return s 1518 | end 1519 | 1520 | function removeSpacesBeforePunctuationMarks(s) 1521 | local pattern = "(%s+)([{" .. PunctuationMarks .. "}])" 1522 | local replaced = s 1523 | while utf8.match(replaced, pattern) do 1524 | replaced = utf8.gsub(replaced, pattern, "%2") 1525 | end 1526 | return replaced 1527 | end 1528 | 1529 | function addRequiredSpaceAfterPunctuationMarks(s) 1530 | local pattern = "([{" .. PunctuationMarks .. "}])([^%s{" .. PunctuationMarks .. "}])" 1531 | local replaced = s 1532 | while utf8.match(replaced, pattern) do 1533 | replaced = utf8.gsub(replaced, pattern, "%1 %2") 1534 | end 1535 | return replaced 1536 | end 1537 | 1538 | function removeSpaceAfterStartingBrackets(s) 1539 | local pattern = "([{" .. StartingBracketChars .. "}])([%s]+)" 1540 | local replaced = s 1541 | while utf8.match(replaced, pattern) do 1542 | replaced = utf8.gsub(replaced, pattern, "%1") 1543 | end 1544 | return replaced 1545 | end 1546 | 1547 | function removeSpaceBeforeEndingBrackets(s) 1548 | local pattern = "([%s]+)([{" .. EndingsBracketChars .. "}])" 1549 | local replaced = s 1550 | while utf8.match(replaced, pattern) do 1551 | replaced = utf8.gsub(replaced, pattern, "%2") 1552 | end 1553 | return replaced 1554 | end 1555 | 1556 | function addRequiredSpaceAfterEndingBrackets(s) 1557 | local pattern = 1558 | "([{" .. 1559 | EndingsBracketChars .. "}])([^%s{" .. EndingsBracketChars .. PunctuationMarks .. StartingBracketChars .. '"}])' 1560 | local replaced = s 1561 | if utf8.match(replaced, pattern) then 1562 | replaced = utf8.gsub(replaced, pattern, "%1 %2") 1563 | end 1564 | return replaced 1565 | end 1566 | 1567 | function addRequiredSpaceBeforeStartingBrackets(s) 1568 | local pattern = "([^%s{" .. StartingBracketChars .. "}])([{" .. StartingBracketChars .. "}])" 1569 | local replaced = s 1570 | while utf8.match(replaced, pattern) do 1571 | replaced = utf8.gsub(replaced, pattern, "%1 %2") 1572 | end 1573 | return replaced 1574 | end 1575 | 1576 | function isRtl(s) 1577 | local RtlChars = { 1578 | "ء", 1579 | "آ", 1580 | "أ", 1581 | "ا", 1582 | "ب", 1583 | "پ", 1584 | "ت", 1585 | "ة", 1586 | "ث", 1587 | "ج", 1588 | "چ", 1589 | "ح", 1590 | "خ", 1591 | "د", 1592 | "ذ", 1593 | "ر", 1594 | "ز", 1595 | "ژ", 1596 | "س", 1597 | "ش", 1598 | "ص", 1599 | "ض", 1600 | "ط", 1601 | "ظ", 1602 | "ع", 1603 | "غ", 1604 | "ف", 1605 | "ق", 1606 | "ک", 1607 | "ك", 1608 | "گ", 1609 | "ل", 1610 | "م", 1611 | "ن", 1612 | "و", 1613 | "ه", 1614 | "ی", 1615 | "ي", 1616 | "۔" 1617 | } 1618 | 1619 | local step = utf8.len(s) 1620 | for i = 1, step do 1621 | local ch = utf8.sub(s, i, i) 1622 | --aegisub.log(i.." "..ch) 1623 | for j = 1, #RtlChars do 1624 | if RtlChars[j] == ch then 1625 | return true 1626 | end 1627 | end 1628 | end 1629 | return false 1630 | end 1631 | 1632 | function getSubtitleTextParts(s) 1633 | local text = s 1634 | local parts = {} 1635 | local p1 = "^(%s*{.-})" 1636 | local p2 = "^(.-)({.-})" 1637 | local p3 = "({.-})" 1638 | 1639 | while string.match(text, p3) do 1640 | while string.match(text, p1) do 1641 | local a = string.match(text, p1) 1642 | table.insert(parts, a) 1643 | text = string.gsub(text, p1, "") 1644 | end 1645 | 1646 | while string.match(text, p2) do 1647 | local a, b = string.match(text, p2) 1648 | table.insert(parts, a) 1649 | table.insert(parts, b) 1650 | text = string.gsub(text, p2, "") 1651 | end 1652 | end 1653 | 1654 | if utf8.len(text) > 0 then 1655 | table.insert(parts, text) 1656 | end 1657 | 1658 | return parts 1659 | end 1660 | 1661 | function rtlCorrectNonCodeText(s, noNormalize) 1662 | if utf8.match(s, CodePattern) == nil then 1663 | if not noNormalize then 1664 | s = removeRtlChars(s) 1665 | s = removeDoubleSpace(s) 1666 | s = removeSpacesBeforePunctuationMarks(s) 1667 | s = addRequiredSpaceAfterPunctuationMarks(s) 1668 | s = addRequiredSpaceBeforeStartingBrackets(s) 1669 | s = removeSpaceAfterStartingBrackets(s) 1670 | s = removeSpaceBeforeEndingBrackets(s) 1671 | s = addRequiredSpaceAfterEndingBrackets(s) 1672 | end 1673 | if isRtl(s) then 1674 | s = addRleToEachNoneAlphabeticChars(s) 1675 | end 1676 | end 1677 | return s 1678 | end 1679 | 1680 | function canCorrectRtl(text) 1681 | -- dcrtl = dont correct rtl 1682 | local canCorrect = not string.find(text, DcrtlTag) 1683 | return canCorrect 1684 | end 1685 | 1686 | function rtlCorrectTextWithCode(s, noNormalize) 1687 | local parts = getSubtitleTextParts(s) 1688 | local text = "" 1689 | for k = 1, #parts do 1690 | local t = parts[k] 1691 | t = rtlCorrectNonCodeText(t, noNormalize) 1692 | text = text .. t 1693 | end 1694 | return text 1695 | end 1696 | 1697 | function rtlCorrectIfAllowed(s) 1698 | if not canCorrectRtl(s) then 1699 | return s 1700 | end 1701 | return rtlCorrectTextWithCode(s) 1702 | end 1703 | ------------------------------- Rtl Editor Methods ---------------------- 1704 | 1705 | function openEditor(str) 1706 | local config = { 1707 | {class = "label", label = "\r\n Press Ctrl+Shift to switch to Right to left mode \r\n", x = 0, y = 0}, 1708 | {class = "textbox", name = "editor", value = str, x = 0, y = 1, width = 12, height = 8} 1709 | } 1710 | local btn, result = aegisub.dialog.display(config, {"OK", "Cancel"}, {ok = "OK", cancel = "Cancel"}) 1711 | return btn, result.editor 1712 | end 1713 | 1714 | ------------------------- Move methods ------------------------- 1715 | 1716 | function getTextSplitCharsParts(text) 1717 | local parts = {} 1718 | text = trim(text) 1719 | 1720 | for i = 1, #SplitChars do 1721 | local txt = text 1722 | local ln = 0 1723 | while txt ~= "" do 1724 | local s, e = utf8.find(txt, SplitChars[i]) 1725 | if s then 1726 | table.insert(parts, {}) 1727 | table.insert(parts[#parts], ln + s) 1728 | table.insert(parts[#parts], ln + e) 1729 | table.insert(parts[#parts], i) 1730 | txt = utf8.sub(txt, e + 1, utf8.len(txt)) 1731 | ln = ln + e 1732 | else 1733 | goto continue 1734 | end 1735 | end 1736 | ::continue:: 1737 | end 1738 | 1739 | -- if text not contains any SplitChars 1740 | if #parts == 0 and text ~= "" then 1741 | table.insert(parts, {}) 1742 | table.insert(parts[#parts], 1) 1743 | table.insert(parts[#parts], utf8.len(text)) 1744 | table.insert(parts[#parts], 1) 1745 | end 1746 | 1747 | table.sort(parts, compare) 1748 | return parts 1749 | end 1750 | 1751 | function getTextPartsBySplitCharIndexes(parts, text) 1752 | if #parts == 0 then 1753 | return nil 1754 | end 1755 | text = trim(text) 1756 | local start = 1 1757 | local textParts = {} 1758 | for i = 1, #parts do 1759 | local part = utf8.sub(text, start, parts[i][2]) 1760 | table.insert(textParts, part) 1761 | start = parts[i][2] + 1 1762 | end 1763 | -- text after last SplitChar 1764 | if start <= utf8.len(text) then 1765 | table.insert(textParts, utf8.sub(text, start)) 1766 | end 1767 | return textParts 1768 | end 1769 | 1770 | function getLastWord(text) 1771 | local words = {} 1772 | for w in text:gmatch("%S+") do 1773 | table.insert(words, w) 1774 | end 1775 | if (#words == 0) then 1776 | return nil 1777 | end 1778 | return words[#words] 1779 | end 1780 | 1781 | function getFirstWord(text) 1782 | local words = {} 1783 | for w in text:gmatch("%S+") do 1784 | table.insert(words, w) 1785 | end 1786 | if (#words == 0) then 1787 | return nil 1788 | end 1789 | return words[1] 1790 | end 1791 | 1792 | function getFirstLineBreakIndex(parts) 1793 | for i, t in ipairs(parts) do 1794 | if utf8.find(t, "\\N") then 1795 | return i 1796 | end 1797 | end 1798 | return 0 1799 | end 1800 | ------------------ Number Converter Methods ------------------- 1801 | function applyNumbersToEnglish(text) 1802 | -- Persian numbers to English 1803 | if utf8.match(text, CodePattern) == nil then 1804 | text = utf8.gsub(text, "۱", "1") 1805 | text = utf8.gsub(text, "۲", "2") 1806 | text = utf8.gsub(text, "۳", "3") 1807 | text = utf8.gsub(text, "۴", "4") 1808 | text = utf8.gsub(text, "۵", "5") 1809 | text = utf8.gsub(text, "۶", "6") 1810 | text = utf8.gsub(text, "۷", "7") 1811 | text = utf8.gsub(text, "۸", "8") 1812 | text = utf8.gsub(text, "۹", "9") 1813 | text = utf8.gsub(text, "۰", "0") 1814 | 1815 | -- Arabic numbers to English 1816 | text = utf8.gsub(text, "١", "1") 1817 | text = utf8.gsub(text, "٢", "2") 1818 | text = utf8.gsub(text, "٣", "3") 1819 | text = utf8.gsub(text, "٤", "4") 1820 | text = utf8.gsub(text, "٥", "5") 1821 | text = utf8.gsub(text, "٦", "6") 1822 | text = utf8.gsub(text, "٧", "7") 1823 | text = utf8.gsub(text, "٨", "8") 1824 | text = utf8.gsub(text, "٩", "9") 1825 | text = utf8.gsub(text, "٠", "0") 1826 | end 1827 | return text 1828 | end 1829 | 1830 | function applyNumbersToArabic(text) 1831 | if utf8.match(text, CodePattern) == nil then 1832 | -- Persian numbers to Arabic 1833 | text = utf8.gsub(text, "۱", "١") 1834 | text = utf8.gsub(text, "۲", "٢") 1835 | text = utf8.gsub(text, "۳", "٣") 1836 | text = utf8.gsub(text, "۴", "٤") 1837 | text = utf8.gsub(text, "۵", "٥") 1838 | text = utf8.gsub(text, "۶", "٦") 1839 | text = utf8.gsub(text, "۷", "٧") 1840 | text = utf8.gsub(text, "۸", "٨") 1841 | text = utf8.gsub(text, "۹", "٩") 1842 | text = utf8.gsub(text, "۰", "٠") 1843 | 1844 | -- English numbers to Arabic 1845 | text = utf8.gsub(text, "1", "١") 1846 | text = utf8.gsub(text, "2", "٢") 1847 | text = utf8.gsub(text, "3", "٣") 1848 | text = utf8.gsub(text, "4", "٤") 1849 | text = utf8.gsub(text, "5", "٥") 1850 | text = utf8.gsub(text, "6", "٦") 1851 | text = utf8.gsub(text, "7", "٧") 1852 | text = utf8.gsub(text, "8", "٨") 1853 | text = utf8.gsub(text, "9", "٩") 1854 | text = utf8.gsub(text, "0", "٠") 1855 | end 1856 | return text 1857 | end 1858 | 1859 | function applyNumbersToPersian(text) 1860 | if utf8.match(text, CodePattern) == nil then 1861 | -- Arabic numbers to Persian 1862 | text = utf8.gsub(text, "١", "۱") 1863 | text = utf8.gsub(text, "٢", "۲") 1864 | text = utf8.gsub(text, "٣", "۳") 1865 | text = utf8.gsub(text, "٤", "۴") 1866 | text = utf8.gsub(text, "٥", "۵") 1867 | text = utf8.gsub(text, "٦", "۶") 1868 | text = utf8.gsub(text, "٧", "۷") 1869 | text = utf8.gsub(text, "٨", "۸") 1870 | text = utf8.gsub(text, "٩", "۹") 1871 | text = utf8.gsub(text, "٠", "۰") 1872 | 1873 | -- English numbers to Persian 1874 | text = utf8.gsub(text, "1", "۱") 1875 | text = utf8.gsub(text, "2", "۲") 1876 | text = utf8.gsub(text, "3", "۳") 1877 | text = utf8.gsub(text, "4", "۴") 1878 | text = utf8.gsub(text, "5", "۵") 1879 | text = utf8.gsub(text, "6", "۶") 1880 | text = utf8.gsub(text, "7", "۷") 1881 | text = utf8.gsub(text, "8", "۸") 1882 | text = utf8.gsub(text, "9", "۹") 1883 | text = utf8.gsub(text, "0", "۰") 1884 | end 1885 | return text 1886 | end 1887 | 1888 | ------------------ Shared Methods ------------------- 1889 | function cleanTags(text) 1890 | return string.gsub(text, [[{\.-}]], "") 1891 | end 1892 | 1893 | function showMessage(msg) 1894 | local config = { 1895 | {class = "label", label = "\r\n" .. msg .. "\r\n", x = 0, y = 0} 1896 | } 1897 | local btn, result = aegisub.dialog.display(config, {"OK"}, {ok = "OK"}) 1898 | end 1899 | 1900 | function compare(a, b) 1901 | return a[1] < b[1] 1902 | end 1903 | 1904 | function trim(s) 1905 | local r = s:gsub("^%s*(.-)%s*$", "%1") 1906 | return r 1907 | end 1908 | 1909 | function removePosTag(text) 1910 | return string.gsub(text, PosPattern, "") 1911 | end 1912 | 1913 | function removeDoubleSpace(s) 1914 | while string.match(s, "%s%s") ~= nil do 1915 | s = string.gsub(s, "%s%s", " ") 1916 | end 1917 | return s 1918 | end 1919 | 1920 | function secondsToClock(seconds) 1921 | local seconds = tonumber(seconds) 1922 | 1923 | if seconds <= 0 then 1924 | return "00:00:00" 1925 | else 1926 | local hours = string.format("%02.f", math.floor(seconds / 3600)) 1927 | local mins = string.format("%02.f", math.floor(seconds / 60 - (hours * 60))) 1928 | local secs = string.format("%02.f", math.floor(seconds - hours * 3600 - mins * 60)) 1929 | return hours .. ":" .. mins .. ":" .. secs 1930 | end 1931 | end 1932 | 1933 | function replaceLineBreak(s) 1934 | return utf8.gsub(s, "\\N", "\n") 1935 | end 1936 | 1937 | function addTag(subs, selected, tag) 1938 | for i = 1, #selected, 1 do 1939 | local line = subs[selected[i]] 1940 | line.text = "{" .. tag .. "}" .. line.text 1941 | subs[selected[i]] = line 1942 | end 1943 | end 1944 | 1945 | function getCodeAndPlainTextPart(text, textParts) 1946 | local plainText = "" 1947 | local codeText = "" 1948 | local codeIndex = 0 1949 | 1950 | if #textParts > 0 then 1951 | for i = 1, #textParts do 1952 | if utf8.match(textParts[i], CodePattern) == nil then 1953 | break 1954 | end 1955 | codeIndex = i 1956 | codeText = codeText .. textParts[i] 1957 | end 1958 | 1959 | if codeIndex > 0 then 1960 | for i = codeIndex + 1, #textParts do 1961 | plainText = plainText .. textParts[i] 1962 | end 1963 | else 1964 | plainText = text 1965 | end 1966 | end 1967 | return codeText, plainText 1968 | end 1969 | 1970 | function getWordList(text) 1971 | return re.split(text, " ") 1972 | end 1973 | 1974 | ------------------------------ End of methods ------------------------------ 1975 | 1976 | aegisub.register_macro(add_background, tr "Adds background shape for subtitles", AddBackground) 1977 | aegisub.register_macro(remove_background_lines, tr "Remove all Background lines", RemoveBackgroundLines) 1978 | 1979 | ------------ Corrections ------------- 1980 | aegisub.register_macro(rtl_correction_script_name, tr "Correct Rtl display problems for all lines", RtlCorrection) 1981 | aegisub.register_macro( 1982 | rtl_correction_selected_line, 1983 | tr "Corrert Rtl display problems for selected lines", 1984 | RtlCorrectorSelectedLine 1985 | ) 1986 | aegisub.register_macro( 1987 | rtl_correction_without_normalize, 1988 | tr "Corrert Rtl display problems for selected lines without normalize text", 1989 | RtlCorrectorWithoutNormalize 1990 | ) 1991 | aegisub.register_macro(add_rle, tr "Add RLE Control character to beginig of selected line", AddRle) 1992 | aegisub.register_macro(undo_rtl_correction, tr "Undo Rtl correction", UndoRtlCorrection) 1993 | aegisub.register_macro(convert_numbers_to_english, tr "Convert all numbers to English numbers", ConvertNumbersToEnglish) 1994 | aegisub.register_macro(convert_numbers_to_arabic, tr "Convert all numbers to Arabic numbers", ConvertNumbersToArabic) 1995 | aegisub.register_macro(convert_numbers_to_persian, tr "Convert all numbers to Persian numbers", ConvertNumbersToPersian) 1996 | aegisub.register_macro( 1997 | selected_numbers_to_english, 1998 | tr "Convert selected line numbers to English numbers", 1999 | SelectedNumbersToEnglish 2000 | ) 2001 | aegisub.register_macro( 2002 | selected_numbers_to_arabic, 2003 | tr "Convert selected line numbers to Arabic numbers", 2004 | SelectedNumbersToArabic 2005 | ) 2006 | aegisub.register_macro( 2007 | selected_numbers_to_persian, 2008 | tr "Convert selected line numbers to Persian numbers", 2009 | SelectedNumbersToPersian 2010 | ) 2011 | 2012 | ------------ Timing ------------- 2013 | aegisub.register_macro(shift_start_line_forward, tr "Shift line start time forward", ShiftStartLineForward) 2014 | aegisub.register_macro(shift_start_line_backward, tr "Shift line start time backward", ShiftStartLineBackward) 2015 | aegisub.register_macro(shift_end_line_forward, tr "Shift line end time forward", ShiftEndLineForward) 2016 | aegisub.register_macro(shift_end_line_backward, tr "Shift line end time backward", ShiftEndLineBackward) 2017 | aegisub.register_macro( 2018 | make_next_line_continuous, 2019 | tr "Make next line continuous with current line", 2020 | MakeNextLineContinuous 2021 | ) 2022 | aegisub.register_macro(make_same_time, tr "Make same time", MakeSameTime) 2023 | aegisub.register_macro(make_same_start_time, tr "Make same start time", MakeSameStartTime) 2024 | aegisub.register_macro(make_same_end_time, tr "Make same end time", MakeSameEndTime) 2025 | 2026 | ------------ Text Movements ------------- 2027 | aegisub.register_macro(move_last_text_part, tr "Move last text part to next line", MoveLastTextPart) 2028 | aegisub.register_macro(move_first_part_of_next, tr "Move first part of next line to current line", MoveFirstPartOfNext) 2029 | aegisub.register_macro(move_last_word, tr "Move last word to next line", MoveLastWord) 2030 | aegisub.register_macro(move_first_word_of_next, tr "Move first word of next line to current line", MoveFirstWordOfNext) 2031 | aegisub.register_macro(shift_line_break, tr "Shift Linebreak forward", ShiftLineBreak) 2032 | aegisub.register_macro(shift_line_break_back, tr "Shift Linebreak back", ShiftLineBreakBack) 2033 | 2034 | aegisub.register_macro(split_script_name, tr "Split selected line", Split) 2035 | aegisub.register_macro(split_at_index_script_name, tr "Split selected line at index", SplitAtIndex) 2036 | aegisub.register_macro(break_semi_long_lines, tr "Break Semi-Long lines", BreakSemiLongLines) 2037 | aegisub.register_macro(break_selected_line, tr "Break Selected line", BreakSelectedLine) 2038 | aegisub.register_macro(show_rtl_editor_script_name, tr "Show Rtl editor", ShowRtlEditor) 2039 | aegisub.register_macro(remove_line_break_script_name, tr "Remove line Breaks", RemoveLineBreaks) 2040 | aegisub.register_macro(remove_position_tags, tr "Remove all position tags", RemovePositionTags) 2041 | aegisub.register_macro(select_playing_line, tr "Select current playing line", SelectPlayingLine) 2042 | aegisub.register_macro(generate_srt_like_text, tr "Generate SRT like text", GenerateSrtLikeText) 2043 | 2044 | ------------ Special Tags ------------ 2045 | aegisub.register_macro(fix_line_position, tr "Prevent automatic change position by script", FixLinePosition) 2046 | aegisub.register_macro( 2047 | set_line_as_no_background, 2048 | tr "Prevent automatic background generation by script", 2049 | SetLineAsNoBackground 2050 | ) 2051 | aegisub.register_macro( 2052 | set_line_as_dont_correct_rtl, 2053 | tr "Prevent automatic Rtl correction by script", 2054 | SetLineAsDontCorrectRtl 2055 | ) 2056 | aegisub.register_macro(set_line_as_dont_remove, tr "Prevent automatic line deletion by script", SetLineAsDontRemove) 2057 | 2058 | ------------ Miscs ------------ 2059 | aegisub.register_macro(unify_background_lines_script_name, tr "Unify background lines", UnifyBackgroundLines) 2060 | aegisub.register_macro(add_code_to_selected_lines_script_name, tr "Add code to selected lines", AddCodeToSelectedLines) 2061 | aegisub.register_macro(import_text_to_selected_lines, tr "Import text to selected lines", ImportTextToSelectedLines) 2062 | aegisub.register_macro(display_sum_of_times, tr "Display sum of times", DisplaySumOfTimes) 2063 | aegisub.register_macro(go_to_line, tr "Go to line", GoToLine) 2064 | --------------------------------------------------------------------------------