├── .gitignore ├── depctrl ├── src │ ├── main.rs │ ├── sha1.rs │ ├── version.rs │ ├── lib.rs │ └── tests.rs ├── Cargo.toml └── Cargo.lock ├── src ├── 0x.JoinPrevious.lua ├── 0x.ConcatSimultaneous.moon ├── 0x.JoinPairs.moon ├── 0x.NotFuniDidntLaugh.moon ├── 0x.ClipToShape.moon ├── 0x.DeriveTrack.moon ├── 0x.StableRetime.moon ├── color.lua └── 0x.KaraTemplater.moon └── doc ├── 0x.DeriveTrack.md ├── 0x.color.md └── 0x.KaraTemplater.md /.gitignore: -------------------------------------------------------------------------------- 1 | depctrl/target 2 | -------------------------------------------------------------------------------- /depctrl/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /src/0x.JoinPrevious.lua: -------------------------------------------------------------------------------- 1 | local tr = aegisub.gettext 2 | 3 | script_name = tr"Join previous" 4 | script_description = tr"Set previous line's end to selected line's start" 5 | script_author = "The0x539" 6 | script_version = "1" 7 | 8 | local function join_previous(subs, sel, i) 9 | local line = subs[i - 1] 10 | line.end_time = subs[i].start_time 11 | subs[i - 1] = line 12 | aegisub.set_undo_point(tr"join previous") 13 | end 14 | 15 | aegisub.register_macro(script_name, script_description, join_previous) 16 | 17 | -------------------------------------------------------------------------------- /depctrl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "depctrl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "*", features = ["derive"] } 8 | serde_json = "*" 9 | serde_with = { version = "*", features = ["macros"], default-features = false } 10 | indexmap = { version = "*", features = ["serde"] } 11 | chrono = { version = "*", features = ["serde"], default-features = false } 12 | hex = "*" 13 | thiserror = "*" 14 | time = "0.3.37" 15 | 16 | [dev-dependencies] 17 | ureq = { version = "*", features = ["tls"], default-features = false } 18 | miette = { version = "*", features = ["fancy", "syntect-highlighter"] } 19 | pretty_assertions = "*" 20 | json-spanned-value = { version = "0.2.2", features = ["preserve_order"] } 21 | -------------------------------------------------------------------------------- /src/0x.ConcatSimultaneous.moon: -------------------------------------------------------------------------------- 1 | export script_name = 'ConcatSimultaneous' 2 | export script_description = 'Perform "Join (concatenate)" on any adjacent lines with matching start/end timestamps' 3 | export script_author = 'The0x539' 4 | export script_version = '0.1.0' 5 | 6 | main = (subs, sel, _) -> 7 | for i = #sel, 2, -1 8 | idx1, idx2 = sel[i-1], sel[i] 9 | line1, line2 = subs[idx1], subs[idx2] 10 | if line1.start_time == line2.start_time and line1.end_time == line2.end_time 11 | line1.text = line1.text\gsub(' +$', '') .. ' ' .. line2.text\gsub('^ +', '') 12 | subs[idx1] = line1 13 | subs.delete idx2 14 | aegisub.set_undo_point 'concatenate simultaneous lines' 15 | 16 | aegisub.register_macro 'Concat simultaneous lines', script_description, main 17 | -------------------------------------------------------------------------------- /src/0x.JoinPairs.moon: -------------------------------------------------------------------------------- 1 | export script_name = 'Join pairs' 2 | export script_description = 'Join pairs of lines with "Join (keep first)" behavior' 3 | export script_author = 'The0x539' 4 | export script_version = '1' 5 | 6 | -- For the sake of this validation function's performance, this script assumes sel is a sorted list. 7 | can_join_pairs = (subs, sel, _) -> (sel % 2 == 0) and (sel[#sel] - sel[1] == #sel - 1) 8 | 9 | join_pairs = (subs, sel, _) -> 10 | to_del = {} 11 | for i = 1, #sel - 1, 2 12 | si, sj = sel[i], sel[i + 1] 13 | 14 | line = subs[si] 15 | line.end_time = subs[sj].end_time 16 | subs[si] = line 17 | 18 | table.insert to_del, sj 19 | 20 | subs.delete to_del 21 | aegisub.set_undo_point 'join pairs' 22 | return [i for i in *sel[1, #sel / 2]] 23 | 24 | aegisub.register_macro script_name, script_description, join_pairs 25 | -------------------------------------------------------------------------------- /depctrl/src/sha1.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, str::FromStr}; 2 | 3 | use hex::FromHex; 4 | use serde_with::{DeserializeFromStr, SerializeDisplay}; 5 | 6 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 7 | pub struct Sha1(pub [u8; 20], #[cfg(test)] bool); 8 | 9 | impl Display for Sha1 { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | #[cfg(test)] 12 | let is_upper = self.1; 13 | #[cfg(not(test))] 14 | let is_upper = true; 15 | 16 | let s = if is_upper { 17 | hex::encode_upper(&self.0) 18 | } else { 19 | hex::encode(&self.0) 20 | }; 21 | f.write_str(&s) 22 | } 23 | } 24 | 25 | impl FromStr for Sha1 { 26 | type Err = hex::FromHexError; 27 | fn from_str(s: &str) -> Result { 28 | Ok(Self( 29 | FromHex::from_hex(s)?, 30 | #[cfg(test)] 31 | s.contains(|c: char| c.is_ascii_uppercase()), 32 | )) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /depctrl/src/version.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, str::FromStr}; 2 | 3 | use serde_with::{DeserializeFromStr, SerializeDisplay}; 4 | use thiserror::Error; 5 | 6 | #[rustfmt::skip] 7 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 8 | #[derive(DeserializeFromStr, SerializeDisplay)] 9 | pub struct Version { 10 | pub major: u8, 11 | pub minor: u8, 12 | pub patch: u8, 13 | } 14 | 15 | impl Display for Version { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | write!(f, "{}.{}.{}", self.major, self.minor, self.patch) 18 | } 19 | } 20 | 21 | impl Version { 22 | fn parse(s: &str) -> Option { 23 | let (major, rest) = s.split_once('.')?; 24 | let (minor, patch) = rest.split_once('.')?; 25 | Some(Self { 26 | major: major.parse().ok()?, 27 | minor: minor.parse().ok()?, 28 | patch: patch.parse().ok()?, 29 | }) 30 | } 31 | } 32 | 33 | #[derive(Debug, Error)] 34 | #[error("Could not parse `{0}` as version")] 35 | pub struct VersionParseError(String); 36 | 37 | impl FromStr for Version { 38 | type Err = VersionParseError; 39 | fn from_str(s: &str) -> Result { 40 | Self::parse(s).ok_or_else(|| VersionParseError(s.to_owned())) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /doc/0x.DeriveTrack.md: -------------------------------------------------------------------------------- 1 | We've all been there. You have something whose motion you can obviously follow, and yet no matter what you try, Mocha refuses to cooperate. Its "manual tracking" functionality seems to work, but then when you export, it's like you didn't do anything at all. You'd track it by hand, but it has some scaling or rotation or *whatever* that makes it hard to follow. You can easily reposition a copy of the shape to follow the footage, and if you had tracking data, a-mo would be more than happy to do the calculations for you, but... **argh.** 2 | 3 | Introducing *DeriveTrack*, a new entry in the Aegisub-Motion submenu. Derive acts as a sort of inverse to the usual Apply macro: where Apply takes position, scale, and rotation data from AfterEffects tracking data and uses it to generate tracked typesetting, Derive instead looks at the position, scale, and rotation of tracked typesetting in order to produce AfterEffects tracking data, presumably to then be applied to other not-yet-tracked TS. 4 | 5 | 6 | Alternatively: 7 | 8 | ![](https://i.imgur.com/mkhuZGu.png) 9 | 10 | General usage is as follows: 11 | 12 | * Spend upwards of 30 minutes struggling to get Mocha to do its job (optional, recommended) 13 | * Give up (implied) 14 | * Draw a shape that traces a contour (or just some edges or corners; anything you can track with your eyes, really) from the base content 15 | * Track the shape manually on each frame (multi-frame events are fine) 16 | * Select the tracked range 17 | * Run DeriveTrack 18 | * Wonder why it uses the log window instead of a proper dialog 19 | * Copy all the output text to clipboard 20 | * Apply the tracking data exactly as if you'd gotten it from Mocha 21 | * Bask in the satisfaction of a sign well tracked (optional, may require positive outlook on life) 22 | 23 | Happy tracking! 24 | -------------------------------------------------------------------------------- /src/0x.NotFuniDidntLaugh.moon: -------------------------------------------------------------------------------- 1 | export script_name = 'Not funi, didn\'t laugh' 2 | export script_description = 'Helper script for dealing with idiosyncratic prosub timing' 3 | export script_author = 'The0x539' 4 | export script_version = '1' 5 | 6 | util = require 'aegisub.util' 7 | 8 | validate = (subs, sel, _) -> 9 | for li in *sel 10 | unless subs[li].text\find '\\N' 11 | return false 12 | true 13 | 14 | validate_single = (subs, sel, i) -> 15 | #sel == 1 and sel[1] == i and validate subs, sel, i 16 | 17 | strip = (text) -> text\gsub('^%- ?', '')\gsub('^ +', '')\gsub(' +$', '') 18 | 19 | split = (line_a) -> 20 | line_b = util.copy line_a 21 | 22 | text = line_a.text 23 | end_of_a, start_of_b = text\find '\\N' 24 | end_of_a -= 1 25 | start_of_b += 1 26 | text_a = text\sub 1, end_of_a 27 | text_b = text\sub start_of_b 28 | 29 | line_a.text = strip text_a 30 | line_b.text = strip text_b 31 | if line_a.effect == 'split' 32 | line_a.effect = '' 33 | line_b.effect = '' 34 | 35 | line_a, line_b 36 | 37 | ms_from_frame = (frame) -> 38 | ms = aegisub.ms_from_frame frame 39 | cs = ms / 10 40 | cs = math.floor(cs + 0.5) 41 | cs * 10 42 | 43 | split_single = (subs, sel, i) -> 44 | frame = aegisub.project_properties!.video_position 45 | time = ms_from_frame frame 46 | line_a, line_b = split subs[i] 47 | line_a.end_time = time 48 | line_b.start_time = time 49 | subs[i] = line_b 50 | subs.insert i, line_a 51 | aegisub.set_undo_point 'split combined line at video' 52 | {i + 1}, i + 1 53 | 54 | split_multiple = (subs, sel, active) -> 55 | table.sort sel 56 | new_selection = {} 57 | new_active = active 58 | 59 | for i = #sel, 1, -1 60 | li = sel[i] 61 | line_a, line_b = split subs[li] 62 | 63 | subs[li] = line_b 64 | subs.insert li, line_a 65 | 66 | table.insert new_selection, li + i - 1 67 | table.insert new_selection, li + i 68 | if li <= active 69 | new_active += 1 70 | 71 | aegisub.set_undo_point 'split combined lines' 72 | new_selection, new_active 73 | 74 | aegisub.register_macro "#{script_name}/Split line before current frame", script_description, split_single, validate_single 75 | aegisub.register_macro "#{script_name}/Split selected lines", script_description, split_multiple, validate 76 | -------------------------------------------------------------------------------- /src/0x.ClipToShape.moon: -------------------------------------------------------------------------------- 1 | tr = aegisub.gettext 2 | 3 | export script_name = tr'Simple Clip to Shape' 4 | export script_description = tr'Convert a clip to a shape, without all the nonsense' 5 | export script_author = 'The0x539' 6 | export script_version = '0.2.0' 7 | export script_namespace = '0x.ClipToShape' 8 | 9 | DependencyControl = require 'l0.DependencyControl' 10 | rec = DependencyControl { 11 | feed: 'https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/DependencyControl.json' 12 | { 13 | { 14 | 'a-mo.LineCollection' 15 | version: '1.0.1' 16 | url: 'https://github.com/TypesettingTools/Aegisub-Motion' 17 | feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json' 18 | } 19 | { 20 | 'l0.ASSFoundation' 21 | version: '0.2.2' 22 | url: 'https://github.com/TypesettingTools/ASSFoundation' 23 | feed: 'https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json' 24 | } 25 | } 26 | } 27 | LineCollection, ASSFoundation = rec\requireModules! 28 | 29 | processLine = (line, preservePos) -> 30 | data = ASSFoundation\parse line 31 | 32 | data\removeSections 2, nil 33 | 34 | shape = (data\removeTags {'clip_vect', 'iclip_vect'})[1] 35 | if shape == nil 36 | rect = (data\removeTags {'clip_rect', 'iclip_rect'})[1] 37 | if rect == nil 38 | return 39 | shape = rect\getVect! 40 | 41 | pos = data\getPosition! 42 | if preservePos 43 | -- this method call was figured out using far too much guesswork 44 | shape\commonOp 'sub', nil, nil, pos.x, pos.y 45 | else 46 | pos.x, pos.y = 0, 0 47 | data\insertTags pos 48 | 49 | data\insertSections ASSFoundation.Section.Drawing {shape} 50 | 51 | align = data\insertDefaultTags 'align' 52 | align.value = 7 53 | 54 | data\cleanTags! 55 | data\commit! 56 | 57 | processAll = (preservePos) -> (subs, sel, _i) -> 58 | lines = LineCollection subs, sel, () -> true 59 | lines\runCallback (_subs, line, _i) -> processLine line, preservePos 60 | lines\replaceLines! 61 | aegisub.set_undo_point 'convert clip to shape' 62 | 63 | canProcess = (subs, sel, _i) -> 64 | for i in *sel 65 | if subs[i].text\find '\\i?clip' 66 | return true 67 | return false 68 | 69 | aegisub.register_macro 'Clip to shape', script_description, processAll(false), canProcess 70 | aegisub.register_macro 'Clip to shape (keep pos)', script_description, processAll(true), canProcess 71 | -------------------------------------------------------------------------------- /doc/0x.color.md: -------------------------------------------------------------------------------- 1 | # Colorlib 2 | 3 | Ever wanted to create a neat little gradient in KFX? Been frustrated by colorspaces, since RGB gradients rarely really turn out the way you want? Look no further! 4 | 5 | Colorlib gives you a multitude of useful[^1] functions for manipulating colors in ASS (and, hey, why not outside it too). 6 | 7 | In this, "ASS color string" refers to a value you could pass to, for instance, a `\c` tag, i.e.: `&H012345&`. 8 | 9 | ## Interpolation 10 | 11 | Linear interpolation between two colors in a given colorspace. Arguments for all are in the same format: 12 | 13 | `interp_xxx(t, color_1, color_2)` 14 | 15 | - `t`: "Distance" to go, from 0 to 1, inclusive. A value of 0 corresponds to `color_1`, a value of 1 to `color_2`. A value of 0.5 is halfway. 16 | - `color_1`: "Starting" color 17 | - `color_2`: "Ending" color 18 | 19 | Returns an ASS color string. 20 | 21 | Currently, these four colorspaces are supported: 22 | 23 | - `interp_lch` - LCh (CIE L\*C\*h) 24 | - `interp_lab` - CIELAB (L\*a\*b\*) 25 | - `interp_rgb` - simple RGB 26 | - `interp_xyz` - XYZ (CIE 1931 XYZ) 27 | 28 | In addition, `interp_alpha` is a function with an equivalent signature for ASS alpha values. 29 | 30 | ## Color formatting 31 | 32 | Format "raw" values into ASS color strings. 33 | 34 | `fmt_rgb(r, g, b)` 35 | - All values between 0 and 255. 36 | 37 | `fmt_xyz(x, y, z)` 38 | - All values between 0 and 1. 39 | 40 | `fmt_lab(l, a, b)` 41 | - `l` between 0 and 100. 42 | - `a` and `b` between -128 and 127. 43 | 44 | `fmt_lch(l, c, h)` 45 | - `l` between 0 and 100. 46 | - `c` and `h` between 0 and 360. 47 | 48 | `fmt_alpha(alpha)` 49 | - Hopefully self-explanatory, mostly to round out the set. Takes values between 0 and 255. 50 | 51 | ## Color adjustment 52 | 53 | Adjust a given color. Currently only supports lighten/darken operations. 54 | As of the time of writing, the lightening logic doesn't *quite* work correctly, so some colors end up not quite reaching the desired extremes. If relevant, you can just use values outside the ±1 range. 55 | 56 | `lighten(color, amount)` 57 | - Lighten the given color (ASS color string) by the specified amount. 58 | - `amount` ranges from -1 to 1. 59 | - 0 leaves the color unchanged 60 | - 1 makes it as light (close to white) as possible 61 | - -1 makes it as dark (close to black) as possible 62 | 63 | `lighten_complex(x, y, z, amount, colorspace)` 64 | - Like `lighten`, but instead takes raw color values instead of a color string. 65 | - `colorspace` can be one of `rgb`, `xyz`, `lch`, or `lab` (default). 66 | 67 | [^1]: usefulness of aforementioned functions is not guaranteed -------------------------------------------------------------------------------- /depctrl/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use chrono::naive::NaiveDate as Date; 4 | use indexmap::IndexMap; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_with::skip_serializing_none; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | 11 | pub mod sha1; 12 | pub mod version; 13 | 14 | use sha1::Sha1; 15 | use version::Version; 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 19 | pub struct Feed<'a> { 20 | pub dependency_control_feed_format_version: Version, 21 | pub name: &'a str, 22 | pub description: &'a str, 23 | pub base_url: &'a str, 24 | pub file_base_url: &'a str, 25 | pub url: &'a str, 26 | pub maintainer: &'a str, 27 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 28 | pub known_feeds: IndexMap<&'a str, &'a str>, 29 | pub macros: IndexMap<&'a str, Package<'a>>, 30 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 31 | pub modules: IndexMap<&'a str, Package<'a>>, 32 | } 33 | 34 | #[skip_serializing_none] 35 | #[derive(Debug, Clone, Serialize, Deserialize)] 36 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 37 | pub struct Package<'a> { 38 | pub file_base_url: Option<&'a str>, 39 | pub url: &'a str, 40 | pub author: &'a str, 41 | pub name: &'a str, 42 | pub description: Cow<'a, str>, 43 | pub channels: IndexMap<&'a str, ReleaseChannel<'a>>, 44 | pub changelog: Option>>>, 45 | } 46 | 47 | #[skip_serializing_none] 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 50 | pub struct ReleaseChannel<'a> { 51 | pub version: Version, 52 | pub released: Date, 53 | pub default: bool, 54 | pub platforms: Option>, 55 | pub files: Vec>, 56 | pub required_modules: Option>>, 57 | pub file_base_url: Option<&'a str>, 58 | } 59 | 60 | #[skip_serializing_none] 61 | #[derive(Debug, Clone, Serialize, Deserialize)] 62 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 63 | pub struct PackageFile<'a> { 64 | pub name: &'a str, 65 | pub url: Option<&'a str>, 66 | pub platform: Option<&'a str>, 67 | pub delete: Option, 68 | pub sha1: Option, 69 | } 70 | 71 | #[skip_serializing_none] 72 | #[derive(Debug, Clone, Serialize, Deserialize)] 73 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 74 | pub struct Dependency<'a> { 75 | pub module_name: &'a str, 76 | pub name: Option<&'a str>, 77 | pub url: Option<&'a str>, 78 | pub version: Option, 79 | pub feed: Option<&'a str>, 80 | pub optional: Option, 81 | } 82 | -------------------------------------------------------------------------------- /src/0x.DeriveTrack.moon: -------------------------------------------------------------------------------- 1 | require 'karaskel' 2 | 3 | check_cancel = () -> 4 | if aegisub.progress.is_cancelled! 5 | aegisub.cancel! 6 | 7 | main = (subs, sel, active) -> 8 | meta, styles = karaskel.collect_head subs, false 9 | 10 | positions = {} 11 | scales = {} 12 | rotations = {} 13 | 14 | for i in *sel 15 | line = subs[i] 16 | style = styles[line.style] 17 | 18 | insert = (t, v) -> 19 | 20 | local pos, fscx, fscy, frz 21 | for block in line.text\gmatch '{.-}' 22 | if pos_tag = block\match '\\pos%([0-9.-]+,[0-9.-]+%)' 23 | x, y = pos_tag\match '([0-9.-]+),([0-9.-]+)' 24 | pos = {x: x, y: y} 25 | 26 | fscx = block\match('\\fscx([0-9.-]+)') 27 | fscy = block\match('\\fscy([0-9.-]+)') 28 | frz = block\match('\\frz([0-9.-]+)') 29 | 30 | if pos == nil 31 | aegisub.log 'missing pos tag\n' 32 | fscx or= style.scale_x 33 | fscy or= style.scale_y 34 | frz or= style.angle 35 | 36 | start_frame = aegisub.frame_from_ms line.start_time 37 | end_frame = aegisub.frame_from_ms line.end_time 38 | for _ = start_frame, end_frame - 1 39 | table.insert positions, pos 40 | table.insert scales, {x: fscx, y: fscy} 41 | table.insert rotations, frz 42 | 43 | data = {} 44 | println = (fmt, ...) -> table.insert data, string.format(fmt or '', ...) 45 | println 'Adobe After Effects 6.0 Keyframe Data' 46 | println '' 47 | println '\tUnits per Second\t23.976' 48 | println '\tSource Width\t%d', meta.res_x 49 | println '\tSource Height\t%d', meta.res_y 50 | println '\tSource Pixel Aspect Ratio\t1' 51 | println '\tComp Pixel Aspect Ratio\t1' 52 | println '' 53 | 54 | println 'Position' 55 | println '\tFrame\tX pixels\tY pixels\tZ pixels' 56 | for i, pos in ipairs positions 57 | println '\t%d\t%f\t%f\t0', i - 1, pos.x, pos.y 58 | 59 | println '' 60 | println 'Scale' 61 | println '\tFrame\tX percent\tY percent\tZ percent' 62 | for i, scale in ipairs scales 63 | println '\t%d\t%f\t%f\t0', i - 1, scale.x, scale.y 64 | 65 | println '' 66 | println 'Rotation' 67 | println '\tFrame\tDegrees' 68 | for i, rotation in ipairs rotations 69 | println '\t%d\t%f', i - 1, -rotation 70 | 71 | result = table.concat data, '\n' 72 | 73 | aegisub.log result 74 | 75 | validate = (subs, sel, _active) -> 76 | -- tag blocks' contents can (and probably will) change, but their presence shouldn't 77 | strip = (line) -> line.text\gsub '{.-}', '{}' 78 | 79 | -- no meaningful derivation if there isn't a selection of multiple lines 80 | return false if #sel < 2 81 | 82 | for i = 2, #sel 83 | i0, i1 = sel[i - 1], sel[i] 84 | line0, line1 = subs[i0], subs[i1] 85 | if strip(line0) != strip(line1) or 86 | line0.end_time != line1.start_time or 87 | line0.layer != line1.layer or 88 | line0.comment != line1.comment 89 | return false 90 | 91 | return true 92 | 93 | aegisub.register_macro 'Aegisub-Motion/Derive', 'Attempt to derive motion tracking data from tracked lines', main, validate 94 | -------------------------------------------------------------------------------- /depctrl/src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use miette::{Diagnostic, IntoDiagnostic, NamedSource, Result, SourceOffset, SourceSpan, WrapErr}; 4 | use pretty_assertions::assert_eq; 5 | use thiserror::Error; 6 | 7 | macro_rules! gen_tests { 8 | ($($author:ident @ $path:literal)*) => { 9 | $( 10 | #[test] 11 | fn $author() -> Result<()> { 12 | let url = concat!( 13 | "https://raw.githubusercontent.com/", 14 | $path, 15 | "/DependencyControl.json", 16 | ); 17 | test_roundtrip(url) 18 | } 19 | )* 20 | }; 21 | } 22 | 23 | gen_tests! { 24 | petzku @ "petzku/Aegisub-Scripts/master" 25 | arch1t3cht @ "TypesettingTools/arch1t3cht-Aegisub-Scripts/main" 26 | phoscity @ "PhosCity/Aegisub-Scripts/main" 27 | zeref @ "TypesettingTools/zeref-Aegisub-Scripts/main" 28 | myaa @ "TypesettingTools/Myaamori-Aegisub-Scripts/master" 29 | unanimated @ "TypesettingTools/unanimated-Aegisub-Scripts/master" 30 | lyger @ "TypesettingTools/lyger-Aegisub-Scripts/master" 31 | line0 @ "TypesettingTools/line0-Aegisub-Scripts/master" 32 | coffeeflux @ "TypesettingTools/CoffeeFlux-Aegisub-Scripts/master" 33 | } 34 | 35 | fn test_roundtrip(url: &str) -> Result<()> { 36 | setup(); 37 | 38 | let json = ureq::get(url) 39 | .send(std::io::empty()) 40 | .into_diagnostic() 41 | .wrap_err("HTTP request failed")? 42 | .into_string() 43 | .into_diagnostic() 44 | .wrap_err("Failed to read HTTP response")?; 45 | 46 | let json = json.trim().trim_start_matches('\u{FEFF}'); 47 | 48 | let de_err = |e| DeserializeError::new(e, &json, url); 49 | 50 | let val: serde_json::Value = serde_json::from_str(&json).map_err(de_err)?; 51 | let feed: Feed = serde_json::from_str(&json).map_err(de_err)?; 52 | 53 | let roundtrip = serde_json::to_value(feed).into_diagnostic()?; 54 | 55 | assert_eq!(val, roundtrip, "Round trip serialization did not match!"); 56 | Ok(()) 57 | } 58 | 59 | fn setup() { 60 | let _ = miette::set_hook(Box::new(|_| { 61 | Box::new( 62 | miette::MietteHandlerOpts::new() 63 | .context_lines(4) 64 | .rgb_colors(miette::RgbColors::Preferred) 65 | .build(), 66 | ) 67 | })); 68 | } 69 | 70 | #[derive(Debug, Error, Diagnostic)] 71 | #[error("Failed to deserialize JSON")] 72 | struct DeserializeError { 73 | #[source_code] 74 | json: NamedSource, 75 | #[label("here")] 76 | at: SourceSpan, 77 | source: serde_json::Error, 78 | } 79 | 80 | impl DeserializeError { 81 | fn new(source: serde_json::Error, json: &str, name: &str) -> Self { 82 | let source_offset = SourceOffset::from_location(json, source.line(), source.column()); 83 | 84 | let mut at = source_offset.into(); 85 | if source.is_data() { 86 | if let Some(span) = span_from_offset(json, source_offset.offset()) { 87 | at = span; 88 | } 89 | } 90 | 91 | let json = NamedSource::new(name, json.to_owned()).with_language("JSON"); 92 | Self { json, at, source } 93 | } 94 | } 95 | 96 | fn span_from_offset(json: &str, offset: usize) -> Option { 97 | let root: json_spanned_value::spanned::Value = json_spanned_value::from_str(json).ok()?; 98 | 99 | let mut stack = vec![&root]; 100 | let mut ranges = vec![]; 101 | 102 | // depth-first search for all values whose span includes `offset` 103 | while let Some(val) = stack.pop() { 104 | if !val.range().contains(&offset) { 105 | continue; 106 | } 107 | 108 | ranges.push(val.range()); 109 | 110 | if let Some(arr) = val.as_array() { 111 | stack.extend(arr); 112 | } else if let Some(obj) = val.as_object() { 113 | for key in obj.keys() { 114 | if key.range().contains(&offset) { 115 | return Some(key.range().into()); 116 | } 117 | } 118 | stack.extend(obj.values()); 119 | } 120 | } 121 | 122 | // return the last found value, i.e., the deepest and smallest span 123 | ranges.last().map(|r| r.clone().into()) 124 | } 125 | -------------------------------------------------------------------------------- /src/0x.StableRetime.moon: -------------------------------------------------------------------------------- 1 | tr = aegisub.gettext 2 | 3 | export script_name = tr'Stable Retime' 4 | export script_description = tr'Set start/end timestamps while preserving kara/transform timing' 5 | export script_author = 'The0x539' 6 | export script_version = '0.2.1' 7 | export script_namespace = '0x.StableRetime' 8 | 9 | DependencyControl = require 'l0.DependencyControl' 10 | rec = DependencyControl { 11 | feed: 'https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/DependencyControl.json' 12 | { 13 | { 14 | 'a-mo.LineCollection' 15 | version: '1.0.1' 16 | url: 'https://github.com/TypesettingTools/Aegisub-Motion' 17 | feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json' 18 | } 19 | { 20 | 'l0.ASSFoundation' 21 | version: '0.2.2' 22 | url: 'https://github.com/TypesettingTools/ASSFoundation' 23 | feed: 'https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json' 24 | } 25 | } 26 | } 27 | LineCollection, ASSFoundation = rec\requireModules! 28 | 29 | check_cancel = () -> 30 | if aegisub.progress.is_cancelled! 31 | aegisub.cancel! 32 | 33 | processLine = (line, start, videoPosition) -> 34 | startDelta = if start then videoPosition - line.start_time else 0 35 | endDelta = if start then 0 else videoPosition - line.end_time 36 | 37 | data = ASSFoundation\parse line 38 | 39 | ktags = data\getTags {'k_fill', 'k_sweep', 'k_bord'} 40 | unless #ktags == 0 41 | ktags[1].value -= startDelta 42 | if ktags[1].value < 0 43 | error 'Attempted to shift line start past end of first syl' 44 | 45 | ktags[#ktags].value += endDelta 46 | if ktags[#ktags].value < 0 47 | error 'Attempted to shift line end past start of last syl' 48 | 49 | for tag in *data\getTags {'transform', 'move'} 50 | -- if these are both zero, the original tag probably omitted timestamps 51 | -- this has the behavior of making the transform span the line's entire duration 52 | -- this script should ensure that it spans the *original* duration 53 | -- no matter which delta is nonzero, this will require making the tag's timestamps explicit 54 | if tag.startTime.value == 0 and tag.endTime.value == 0 55 | tag.endTime += line.duration 56 | 57 | -- "timestamp" values, as used in all the non-\k tags that this script modifies, are relative to a line's start time 58 | -- as such, to preserve tag timing, one must apply the negated start delta to all such tags 59 | -- setting explicit timestamps is important, as explained above, but beyond that, the end delta should be irrelevant 60 | tag.startTime -= startDelta 61 | tag.endTime -= startDelta 62 | 63 | -- this script can only meaningfully support 7-arg complex fade, not 2-arg simple fade 64 | -- these are documented as "\fade" and "\fad" respectively, but renderers decide using argument count 65 | -- assf calls them "fade" and "fade_simple", and expects \fade to be complex and \fad to be simple 66 | -- it may not be ideal, but unlike renderers, automation macros are allowed to fail in cases like this 67 | -- Go fix your tags before they break some other macro that's less aware of this situation. 68 | for tag in *data\getTags {'fade'} 69 | tag.inStartTime -= startDelta 70 | tag.outStartTime -= startDelta 71 | 72 | data\commit! 73 | line.start_time += startDelta 74 | line.end_time += endDelta 75 | 76 | msFromFrame = (frame) -> 77 | ms = aegisub.ms_from_frame frame 78 | cs = ms / 10 79 | cs = math.floor(cs + 0.5) 80 | return cs * 10 81 | 82 | doCb = (lines, cb) -> 83 | lines\runCallback (lines, line, i) -> 84 | check_cancel! 85 | cb line 86 | aegisub.progress.set 100 * (i / #lines) 87 | 88 | processAll = (subs, sel, start) -> 89 | videoFrame = aegisub.project_properties!.video_position 90 | if not start 91 | videoFrame += 1 92 | videoPosition = msFromFrame videoFrame 93 | 94 | lines = LineCollection subs, sel, () -> true 95 | doCb lines, (line) -> processLine line, start, videoPosition 96 | lines\replaceLines! 97 | 98 | splitAll = (subs, sel, before) -> 99 | -- "chunk": some contiguous selected lines. A selection consists of one or more chunks. 100 | 101 | sel_a = {} 102 | sel_b = {} 103 | 104 | aegisub.progress.task 'Duplicating lines' 105 | 106 | -- this and similar variables are indices into `sel` 107 | chunkStart = 1 108 | -- how many lines we've inserted to the left of where we're currently working 109 | offset = 0 110 | while chunkStart <= #sel 111 | check_cancel! 112 | -- determine where this chunk ends 113 | chunkEnd = chunkStart 114 | while sel[chunkEnd + 1] == sel[chunkEnd] + 1 115 | chunkEnd += 1 116 | chunkLen = (chunkEnd - chunkStart) + 1 117 | 118 | insertionIdx = sel[chunkEnd] + 1 + offset 119 | -- duplicate the lines, iterating backwards to preserve order 120 | for i = chunkEnd, chunkStart, -1 121 | lineIdx = sel[i] + offset 122 | -- this insertion happens backwards for each chunk, but that shouldn't matter 123 | table.insert sel_a, lineIdx 124 | table.insert sel_b, lineIdx + chunkLen 125 | 126 | subs.insert insertionIdx, subs[lineIdx] 127 | 128 | aegisub.progress.set 100 * (#sel_a / #sel) 129 | 130 | -- update offset and move on to next chunk 131 | offset += chunkLen 132 | chunkStart += chunkLen 133 | 134 | videoFrame = aegisub.project_properties!.video_position 135 | if not before 136 | videoFrame += 1 137 | videoPosition = msFromFrame videoFrame 138 | 139 | aegisub.progress.task 'Retiming "before" lines' 140 | leftLines = LineCollection subs, sel_a, () -> true 141 | doCb leftLines, (line) -> processLine line, false, videoPosition 142 | leftLines\replaceLines! 143 | 144 | aegisub.progress.task 'Retiming "after" lines' 145 | rightLines = LineCollection subs, sel_b, () -> true 146 | doCb rightLines, (line) -> processLine line, true, videoPosition 147 | rightLines\replaceLines! 148 | 149 | return sel_b 150 | 151 | setStart = (subs, sel, _i) -> 152 | processAll subs, sel, true 153 | aegisub.set_undo_point 'Snap start to video, preserving tag timing' 154 | 155 | setEnd = (subs, sel, _i) -> 156 | processAll subs, sel, false 157 | aegisub.set_undo_point 'Snap end to video, preserving tag timing' 158 | 159 | splitBefore = (subs, sel, _i) -> 160 | new_sel = splitAll subs, sel, true 161 | aegisub.set_undo_point 'Split lines before current frame, preserving tag timing' 162 | return new_sel 163 | 164 | splitAfter = (subs, sel, _i) -> 165 | new_sel = splitAll subs, sel, false 166 | aegisub.set_undo_point 'Split lines after current frame, preserving tag timing' 167 | return new_sel 168 | 169 | aegisub.register_macro 'Stable Retime/Set Start', script_description, setStart 170 | aegisub.register_macro 'Stable Retime/Set End', script_description, setEnd 171 | aegisub.register_macro 'Stable Retime/Split Before', script_description, splitBefore 172 | aegisub.register_macro 'Stable Retime/Split After', script_description, splitAfter 173 | -------------------------------------------------------------------------------- /src/color.lua: -------------------------------------------------------------------------------- 1 | local function XYZfromRGB(r, g, b) 2 | r, g, b = r/0xFF, g/0xFF, b/0xFF 3 | 4 | local function f(n) 5 | if n > 0.04045 then 6 | return math.pow(((n + 0.055) / 1.055), 2.4) 7 | else 8 | return n / 12.92 9 | end 10 | end 11 | r, g, b = f(r), f(g), f(b) 12 | 13 | local x = r*0.4124564 + g*0.3575761 + b*0.1804375 14 | local y = r*0.2126729 + g*0.7151522 + b*0.0721750 15 | local z = r*0.0193339 + g*0.1191920 + b*0.9503041 16 | 17 | return x, y, z 18 | end 19 | 20 | local function RGBfromXYZ(x, y, z) 21 | local r = x*3.2404542 + y*-1.5371385 + z*-0.4985314 22 | local g = x*-0.9692660 + y*1.8760108 + z*0.0415560 23 | local b = x*0.0556434 + y*-0.2040259 + z*1.0572252 24 | 25 | local function f(n) 26 | if n > 0.0031308 then 27 | return 1.055 * math.pow(n, 1/2.4) - 0.055 28 | else 29 | return 12.92*n 30 | end 31 | end 32 | r, g, b = f(r), f(g), f(b) 33 | 34 | local function clamp(n) 35 | return math.min(math.max(n*255, 0), 255) 36 | end 37 | return clamp(r), clamp(g), clamp(b) 38 | end 39 | 40 | local function LABfromXYZ(x, y, z) 41 | local Xn, Yn, Zn = 0.95047, 1.0, 1.08883 42 | 43 | x, y, z = x/Xn, y/Yn, z/Zn 44 | 45 | local function f(n) 46 | if n > 0.008856 then 47 | return math.pow(n, 1/3) 48 | else 49 | return (903.3 * n + 16) / 116 50 | end 51 | end 52 | x, y, z = f(x), f(y), f(z) 53 | 54 | local l = (116 * y) - 16 55 | local a = 500 * (x - y) 56 | local b = 200 * (y - z) 57 | 58 | return l, a, b 59 | end 60 | 61 | local function XYZfromLAB(l, a, b) 62 | local ref_X, ref_Y, ref_Z = 0.95047, 1.0, 1.08883 63 | 64 | local y = (l + 16) / 116 65 | local x = a/500 + y 66 | local z = y - b/200 67 | 68 | local function fxz(n) 69 | if n^3 > 0.008856 then 70 | return n^3 71 | else 72 | return (116 * n - 16) / 903.3 73 | end 74 | end 75 | 76 | x, z = fxz(x), fxz(z) 77 | if l > (903.3 * 0.008856) then 78 | y = y^3 79 | else 80 | y = l / 903.3 81 | end 82 | 83 | return x*ref_X, y*ref_Y, z*ref_Z 84 | end 85 | 86 | -- TODO: rewrite above functions to use this instead of embedding the constants into the arithmetic 87 | local function mat_vec_mul(matrix, vector) 88 | local result = {} 89 | for i = 1, #matrix do 90 | local v = 0 91 | for j = 1, #vector do 92 | v = v + matrix[i][j] * vector[j] 93 | end 94 | result[i] = v 95 | end 96 | return result 97 | end 98 | 99 | local function vec_pow_inplace(vector, pow) 100 | for i = 1, #vector do 101 | vector[i] = math.pow(vector[i], pow) 102 | end 103 | end 104 | 105 | local function OKLABfromXYZ(x, y, z) 106 | local lms_from_xyz = { 107 | {0.8189330101, 0.3618667424, -0.1288597137}, 108 | {0.0329845436, 0.9293118715, 0.0361456387}, 109 | {0.0482003018, 0.2643662691, 0.6338517070} 110 | } 111 | 112 | local lab_from_lms = { 113 | {0.2104542553, 0.7936177850, -0.0040720468}, 114 | {1.9779984951, -2.4285922050, 0.4505937099}, 115 | {0.0259040371, 0.7827717662, -0.8086757660} 116 | } 117 | 118 | local lms = mat_vec_mul(lms_from_xyz, {x, y, z}) 119 | vec_pow_inplace(lms, 1/3) 120 | local lab = mat_vec_mul(lab_from_lms, lms) 121 | return lab[1], lab[2], lab[3] 122 | end 123 | 124 | local function XYZfromOKLAB(l, a, b) 125 | local xyz_from_lms = { 126 | {1.22701385, -0.55779998, 0.28125615}, 127 | {-0.04058018, 1.11225687, -0.07167668}, 128 | {-0.07638128, -0.42148198, 1.58616322} 129 | } 130 | 131 | local lms_from_lab = { 132 | {1, 0.39633779, 0.21580376}, 133 | {1.00000001, -0.10556134, -0.06385417}, 134 | {1.00000005, -0.08948418, -1.29148554} 135 | } 136 | 137 | local lms = mat_vec_mul(lms_from_lab, {l, a, b}) 138 | vec_pow_inplace(lms, 3) 139 | local xyz = mat_vec_mul(xyz_from_lms, lms) 140 | return xyz[1], xyz[2], xyz[3] 141 | end 142 | 143 | local function LCHfromLAB(l, a, b) 144 | -- l is unchanged 145 | local c = math.sqrt(a*a + b*b) 146 | local h = math.atan2(b, a) * 180/math.pi 147 | h = h % 360 148 | 149 | return l, c, h 150 | end 151 | 152 | local function LABfromLCH(l, c, h) 153 | -- l is unchanged 154 | local hr = h * math.pi/180 155 | local a = math.cos(hr) * c 156 | local b = math.sin(hr) * c 157 | 158 | return l, a, b 159 | end 160 | 161 | local function interpolateLCh(t, l1, c1, h1, l2, c2, h2) 162 | local l = (1-t)*l1 + t*l2 163 | local c = (1-t)*c1 + t*c2 164 | 165 | if h2 - h1 >= 180 then 166 | h2 = h2 - 360 167 | elseif h1 - h2 >= 180 then 168 | h1 = h1 - 360 169 | end 170 | local h = (1-t)*h1 + t*h2 171 | h = h % 360 172 | return l, c, h 173 | end 174 | 175 | local function interpolate(t, a1, b1, c1, a2, b2, c2) 176 | local a = (1-t)*a1 + t*a2 177 | local b = (1-t)*b1 + t*b2 178 | local c = (1-t)*c1 + t*c2 179 | return a, b, c 180 | end 181 | 182 | local function lighten(x, y, z, amount, space) 183 | -- amount: value between -1 and 1 184 | -- (though *technically* absolutely larger values) work too 185 | space = space or "lab" 186 | local l,a,b 187 | if space == "lch" or space == "lab" then 188 | l = x 189 | elseif space == "rgb" then 190 | l,a,b = LABfromXYZ(XYZfromRGB(x,y,z)) 191 | elseif space == "xyz" then 192 | l,a,b = LABfromXYZ(x,y,z) 193 | else 194 | -- invalid colorspace requested, give up gracefully 195 | return x, y, z 196 | end 197 | 198 | if amount > 0 then 199 | local diff = 100 - l 200 | l = l + diff * amount 201 | else 202 | local diff = l 203 | l = l + diff * amount 204 | end 205 | 206 | if space == "lch" or space == "lab" then 207 | return l,y,z 208 | elseif space == "rgb" then 209 | return RGBfromXYZ(XYZfromLAB(l,a,b)) 210 | elseif space == "xyz" then 211 | return XYZfromLAB(l,a,b) 212 | end 213 | end 214 | 215 | local function round(n) 216 | return math.floor(n + 0.5) 217 | end 218 | 219 | -- parse_ass_color helper function 220 | -- sometimes you really wonder why someone used a signed integer 221 | local function u32_from_f64(n) 222 | n = math.max(n, 0) 223 | n = math.min(n, 0xFFFFFFFF) 224 | n = round(n) 225 | return n 226 | end 227 | 228 | local function u8_from_f64(n) 229 | n = math.max(n, 0) 230 | n = math.min(n, 0xFF) 231 | n = round(n) 232 | return n 233 | end 234 | 235 | -- behavior is reasonably close to that of libass 236 | -- quite possibly overengineered 237 | local function parse_ass_color(c) 238 | -- skip *specific* leading garbage 239 | c = c:gsub('^[&H]*', '') 240 | 241 | -- this part specifically does not match the libass implementation. 242 | -- tonumber rejects (some) trailing garbage, but ass_strtod ignores it 243 | c = c:match('^[0-9A-Fa-f]*') 244 | 245 | local rgb = u32_from_f64(tonumber(c, 16) or 0) 246 | 247 | local bit = require('bit') 248 | local r = bit.rshift(bit.band(rgb, 0x000000FF), 0) 249 | local g = bit.rshift(bit.band(rgb, 0x0000FF00), 8) 250 | local b = bit.rshift(bit.band(rgb, 0x00FF0000), 16) 251 | return r, g, b 252 | end 253 | 254 | local function parse_ass_alpha(a) 255 | if type(a) == 'number' then 256 | return a 257 | end 258 | 259 | a = a:gsub('^[&H]*', '') 260 | a = a:match('^[0-9A-Fa-f]*') 261 | 262 | return u8_from_f64(tonumber(a, 16) or 0) 263 | end 264 | 265 | local function fmt_ass_color(r, g, b) 266 | r, g, b = round(r), round(g), round(b) 267 | return ("&H%02X%02X%02X&"):format(b, g, r) 268 | end 269 | 270 | local function fmt_ass_alpha(a) 271 | a = round(a) 272 | return ("&H%02X&"):format(a) 273 | end 274 | 275 | local function interp_oklch(t, color_1, color_2) 276 | local l1, c1, h1 = LCHfromLAB(OKLABfromXYZ(XYZfromRGB(parse_ass_color(color_1)))) 277 | local l2, c2, h2 = LCHfromLAB(OKLABfromXYZ(XYZfromRGB(parse_ass_color(color_2)))) 278 | local l, c, h = interpolateLCh(t, l1, c1, h1, l2, c2, h2) 279 | local r, g, b = RGBfromXYZ(XYZfromOKLAB(LABfromLCH(l, c, h))) 280 | return fmt_ass_color(r, g, b) 281 | end 282 | 283 | local function interp_oklab(t, color_1, color_2) 284 | local l1, a1, b1 = OKLABfromXYZ(XYZfromRGB(parse_ass_color(color_1))) 285 | local l2, a2, b2 = OKLABfromXYZ(XYZfromRGB(parse_ass_color(color_2))) 286 | local l, a, b = interpolate(t, l1, a1, b1, l2, a2, b2) 287 | local r, g, b = RGBfromXYZ(XYZfromOKLAB(l, a, b)) 288 | return fmt_ass_color(r, g, b) 289 | end 290 | 291 | local function interp_lch(t, color_1, color_2) 292 | local l1, c1, h1 = LCHfromLAB(LABfromXYZ(XYZfromRGB(parse_ass_color(color_1)))) 293 | local l2, c2, h2 = LCHfromLAB(LABfromXYZ(XYZfromRGB(parse_ass_color(color_2)))) 294 | local l, c, h = interpolateLCh(t, l1, c1, h1, l2, c2, h2) 295 | local r, g, b = RGBfromXYZ(XYZfromLAB(LABfromLCH(l, c, h))) 296 | return fmt_ass_color(r, g, b) 297 | end 298 | 299 | local function interp_lab(t, color_1, color_2) 300 | local l1, a1, b1 = LABfromXYZ(XYZfromRGB(parse_ass_color(color_1))) 301 | local l2, a2, b2 = LABfromXYZ(XYZfromRGB(parse_ass_color(color_2))) 302 | local l, a, b = interpolate(t, l1, a1, b1, l2, a2, b2) 303 | local r, g, b = RGBfromXYZ(XYZfromLAB(l, a, b)) 304 | return fmt_ass_color(r, g, b) 305 | end 306 | 307 | local function interp_xyz(t, color_1, color_2) 308 | local x1, y1, z1 = XYZfromRGB(parse_ass_color(color_1)) 309 | local x2, y2, z2 = XYZfromRGB(parse_ass_color(color_2)) 310 | local x, y, z = interpolate(t, x1, y1, z1, x2, y2, z2) 311 | local r, g, b = RGBfromXYZ(x, y, z) 312 | return fmt_ass_color(r, g, b) 313 | end 314 | 315 | local function interp_rgb(t, color_1, color_2) 316 | local r1, g1, b1 = parse_ass_color(color_1) 317 | local r2, g2, b2 = parse_ass_color(color_2) 318 | local r, g, b = interpolate(t, r1, g1, b1, r2, g2, b2) 319 | return fmt_ass_color(r, g, b) 320 | end 321 | 322 | local function interp_alpha(t, alpha_1, alpha_2) 323 | local a1 = parse_ass_alpha(alpha_1) 324 | local a2 = parse_ass_alpha(alpha_2) 325 | 326 | local a = (1-t)*a1 + t*a2 327 | return fmt_ass_alpha(a) 328 | end 329 | 330 | local fmt_rgb = fmt_ass_color 331 | 332 | local function fmt_xyz(x, y, z) 333 | local r, g, b = RGBfromXYZ(x, y, z) 334 | return fmt_ass_color(r, g, b) 335 | end 336 | 337 | local function fmt_lab(l, a, b) 338 | local r, g, b = RGBfromXYZ(XYZfromLAB(l, a, b)) 339 | return fmt_ass_color(r, g, b) 340 | end 341 | 342 | local function fmt_lch(l, c, h) 343 | local r, g, b = RGBfromXYZ(XYZfromLAB(LABfromLCH(l, c, h))) 344 | return fmt_ass_color(r, g, b) 345 | end 346 | 347 | local function fmt_oklab(l, a, b) 348 | local r, g, b = RGBfromXYZ(XYZfromOKLAB(l, a, b)) 349 | return fmt_ass_color(r, g, b) 350 | end 351 | 352 | local function fmt_oklch(l, c, h) 353 | local r, g, b = RGBfromXYZ(XYZfromOKLAB(LABfromLCH(l, c, h))) 354 | return fmt_ass_color(r, g, b) 355 | end 356 | 357 | local function lighten_ass(c, amount) 358 | local r, g, b = parse_ass_color(c) 359 | r, g, b = lighten(r, g, b, amount, "rgb") 360 | return fmt_ass_color(r, g, b) 361 | end 362 | 363 | return { 364 | interp_lch = interp_lch, 365 | interp_lab = interp_lab, 366 | interp_oklab = interp_oklab, 367 | interp_oklch = interp_oklch, 368 | interp_xyz = interp_xyz, 369 | interp_rgb = interp_rgb, 370 | fmt_rgb = fmt_rgb, 371 | fmt_xyz = fmt_xyz, 372 | fmt_lab = fmt_lab, 373 | fmt_lch = fmt_lch, 374 | fmt_oklab = fmt_oklab, 375 | fmt_oklch = fmt_oklch, 376 | interp_alpha = interp_alpha, 377 | fmt_alpha = fmt_ass_alpha, 378 | lighten = lighten_ass, 379 | lighten_complex = lighten, 380 | } 381 | -------------------------------------------------------------------------------- /doc/0x.KaraTemplater.md: -------------------------------------------------------------------------------- 1 | Hi. I'm The0x539's templater. I don't really have a name per se, so The0x539 wrote my docs in the first person as a very strange workaround. It doesn't come up much, so hopefully it isn't too distracting. 2 | 3 | If you already have experience with a templater, many aspects of my operation may be quite familiar. However, there are a few key differences in basic usage that may cause confusion when switching over. 4 | Notably, though I share many features with other templaters, I do not support templates written for them, or vice versa, aside from simple incidental cases. 5 | This isn't actually anything new: the two other major templaters already aren't compatible with one another, but it seems worthwhile to make the incompatibility clear and explicit. When writing a template, one must choose which templater to target. This was already the case before I came along. 6 | 7 | # Templates 8 | Any reasonable template will consist of two things, which these docs will call *components* and *input*. 9 | In-depth descriptions of each will follow, but briefly, for those somewhat familiar with templating, a *component* is a subtitle line marked with `code`/`template`/`mixin` (more on that last one later) in its effect field, and an *input* line is one marked with `kara`. 10 | With some exceptions (primarily `code once`), the idea behind executing a template is to, for each input line, "run" each component against that line (or against each char/etc within it). 11 | 12 | ## Input 13 | Input is probably the simpler of the two parts of a template. Simply put, for a typical template, an input line consists of one line of lyrics for a song. 14 | The lyrics may include k-timing tags, for use with `syl`-class components, as well as the standard karaskel hyphen-prefixed inline-fx tags. Details on k-timing are beyond the scope of this document. 15 | 16 | Importantly, a line of input *must* have its effect field set to `kara` (or `karaoke`). 17 | **Unlike other templaters, I do not treat a line as input unless it is already explicitly marked as such.** 18 | This is not only a deliberate design decision, but also (arguably) necessary due to my more advanced conditional execution logic. 19 | 20 | Input lines' actor fields are very useful for conditional execution purposes. 21 | On a basic level, different actor values can be used to denote different parts of a song, applying a different set of components to each. 22 | Getting more advanced, a template could be written to have a common `template` component apply to all the input, but select different `mixin`s based on parts of the song, perhaps to differ in color but share all other attributes. 23 | In its most powerful form, an input line's actor field can be used as a collection of modifiers to mix and match different components on a per-line basis. 24 | More on this later. 25 | 26 | ## Components 27 | Components are the *fun* part of making a template. Components come in three varieties: `code`, `template`, and `mixin`. 28 | My `code` and `template` components are, at a basic level, very similar to those of other templaters, and `mixin` components are like a more powerful form of karaOK's `template lsyl` family of components. 29 | 30 | A component is defined by its effect field, which acts as a space-separated list of tokens. 31 | The first token must be `code`, `template`, or `mixin`, depending on which kind of component it is. 32 | The second token is the component's *class*, which is one of the following: 33 | 34 | |**Class**|**Description**| 35 | |-|-| 36 | |`once`|Runs once at the beginning of template evaluation. Only valid for `code` components. (`template once` TBD)| 37 | |`line`|Runs once for each line of input.| 38 | |`syl`|Runs once for each k-timed `syl` within each line of input. Lines with no k-timing information will be treated as one big syl.| 39 | |`word`|Like `syl`, but separated by whitespace instead of k-timing tags.| 40 | |`char`|Runs once for each character (currently uses `unicode.chars`) within each line of input.| 41 | 42 | Further tokens within a component's effect field, if present, are *modifiers*. More on those later. 43 | 44 | ### `code` Components 45 | A `code` component is simple: running it executes its text as a block of Lua code. 46 | Their most common use is `code once` components that import utility libraries, define helper functions, or set up global constants for use elsewhere in the template. 47 | However, the other classes do occasionally prove useful, executing the code on, *e.g.*, a per-line basis. 48 | 49 | ### `template` Components 50 | As their name might imply, `template` components are central to writing a template, as with my predecessors. 51 | It is their sole responsibility to generate new lines of *output*. If there are no `template` components, no lines will be generated. 52 | Generally speaking, running a `template` component entails evaluating any inline variables or inline expressions in its text, then generating a line of output consisting of the evaluated template text and the piece of text that the component's running against. 53 | For example, consider the following simple template and the output it produces based on the given input: 54 | 55 | ``` 56 | template line: {\placeholder} 57 | template syl: {\foo} 58 | kara: {\k86}Word{\k41}less{\k24}ly {\k87}watch{\k33}ing {\k63}he {\k66}waits{\k25} {\k44}by {\k22}the {\k57}win{\k69}dow 59 | kara: {\k36}And {\k38}won{\k59}ders{\k43} {\k25}at {\k16}the {\k40}emp{\k21}ty {\k49}place {\k26}in{\k85}side 60 | fx: {\placeholder}Wordlessly watching he waits by the window 61 | fx: {\foo}Word 62 | fx: {\foo}less 63 | fx: {\foo}ly 64 | fx: {\foo}watch 65 | fx: {\foo}ing 66 | fx: {\foo}he 67 | fx: {\foo}waits 68 | fx: {\foo} 69 | fx: {\foo}by 70 | fx: {\foo}the 71 | fx: {\foo}win 72 | fx: {\foo}dow 73 | fx: {\placeholder}And wonders at the empty place inside 74 | fx: {\foo}And 75 | fx: {\foo}won 76 | fx: {\foo}ders 77 | fx: {\foo} 78 | fx: {\foo}at 79 | fx: {\foo}the 80 | fx: {\foo}emp 81 | fx: {\foo}ty 82 | fx: {\foo}place 83 | fx: {\foo}in 84 | fx: {\foo}side 85 | ``` 86 | 87 | Already this is useful for simple song styles, since you can now apply a set of tags to a set of lines, perhaps even with multiple layers, while not having to manually keep everything consistent. 88 | Add some inline expressions to generate more complicated tags to each line and things start to get interesting. 89 | 90 | ### `mixin` Components 91 | Mixins, as a first-class component variety, are my defining feature. They modify the output produced by `template` components, generally adding tags. If `template lsyl` from karaOK is unfamiliar to you, consider the following basic demonstration: 92 | ``` 93 | template line: {\Tpl} 94 | mixin line: {\line} 95 | mixin syl: {\syl} 96 | mixin char: {\char} 97 | kara: {\k86}Word{\k41}less{\k24}ly {\k87}watch{\k33}ing {\k63}he {\k66}waits{\k25} {\k44}by {\k22}the {\k57}win{\k69}dow 98 | kara: {\k36}And {\k38}won{\k59}ders{\k43} {\k25}at {\k16}the {\k40}emp{\k21}ty {\k49}place {\k26}in{\k85}side 99 | fx: {\Tpl\line\syl\char}W{\char}o{\char}r{\char}d{\syl\char}l{\char}e{\char}s{\char}s{\syl\char}l{\char}y{\char} {\syl\char}w{\char}a{\char}t{\char}c{\char}h{\syl\char}i{\char}n{\char}g{\char} {\syl\char}h{\char}e{\char} {\syl\char}w{\char}a{\char}i{\char}t{\char}s{\syl\char} {\syl\char}b{\char}y{\char} {\syl\char}t{\char}h{\char}e{\char} {\syl\char}w{\char}i{\char}n{\syl\char}d{\char}o{\char}w 100 | fx: {\Tpl\line\syl\char}A{\char}n{\char}d{\char} {\syl\char}w{\char}o{\char}n{\syl\char}d{\char}e{\char}r{\char}s{\syl\char} {\syl\char}a{\char}t{\char} {\syl\char}t{\char}h{\char}e{\char} {\syl\char}e{\char}m{\char}p{\syl\char}t{\char}y{\char} {\syl\char}p{\char}l{\char}a{\char}c{\char}e{\char} {\syl\char}i{\char}n{\syl\char}s{\char}i{\char}d{\char}e 101 | ``` 102 | 103 | Any number of `mixin` components can "apply to" any number of `template` components using my powerful conditional execution abilities, offering much more expressive templating than the "name association" system my predecessors offer. 104 | 105 | Mixins can be very useful for relying more heavily on `template line` than is strictly necessary, resulting in cleaner output that's also easier to make sense of. 106 | For instance, combining `template line` with `mixin syl` allows for simple (read: doesn't involve movement or horizontal scaling) in-line highlight effects, and `mixin char` can be useful for by-character gradients. 107 | 108 | `mixin line` is an interesting construct. It goes once at the start of each generated line, like the text of the template it's modifying, but as a mixin, it works as a modifier. Even if the template's class is `syl`, `word`, or `char`, it's appropriate to use `mixin line` to modify the entire line *of output*. 109 | Combined with conditional execution, this behavior becomes quite useful: 110 | - If a song style is largely uniform, but uses different colors for different groups of lines (perhaps color-coded by character), then the common tags could be put in a `template` that applies to the entire song, and there would be a `mixin` for each color, with each one set to execute when appropriate (likely using the `actor` modifier). This could also have been achieved using a sort of lookup table, but it makes for a decent example, and this approach actually scales better when space-separated actors get involved. 111 | - If a style has some complicated stuff going on, but there is, for instance, a variety of templates meant to generate a consistent "glow" effect, then a mixin can be used to apply the same tags to all "glow" output without adding them to each template. 112 | 113 | ### Modifiers 114 | In addition to its variety and class, a component's effect field may contain any amount of *modifiers*, which alter its behavior. 115 | Most modifiers allow you to specify the conditions under which a component will execute based on the input or whatever else. 116 | 117 | If there are multiple different restrictions on a component, then all restrictions must be satisfied for the component to execute. 118 | For example, a mixin with the modifiers `actor foo actor bar layer 3 layer 5` will execute only for lines where ((actor contains `foo` OR actor contains `bar`) AND (layer is 3 or layer is 5)). 119 | 120 | #### `style` 121 | By default, a component is "interested" in only the style that it is itself set to. This means that it will only execute for input with a matching style. 122 | Adding `style foo` to a template will add `foo` to its set of interested styles, so it will also execute for input with the `foo` style. 123 | Styles with names containing spaces do not currently work with this modifier. 124 | 125 | #### `anystyle` 126 | Similar to the `all` modifier in other templaters, this makes the component interested in any style. 127 | 128 | #### `actor` 129 | Similar to the `style` modifier, each component has a set of actor values it is "interested" in. 130 | By default, this set is empty and a component won't look at the input's actor field. 131 | Adding one or more `actor` modifiers will restrict the component to only execute for input with matching actor values. 132 | 133 | **Advanced usage:** The actor field of a line of input is treated not as a single string, but as a space-separated list of actors, so input can be given multiple "actor values" that the component tries to match. 134 | If there is any intersection between the component's set of interested actors and the input's set of actor values, the component will execute. 135 | 136 | #### `noactor` 137 | Works basically the same way as `actor`, but adds to a list of *"disinterested"* actor values for which the component will *not* execute. 138 | Can sort of be said to take "precedence" over `actor`, but that doesn't accurately reflect how these modifiers are implemented. 139 | 140 | #### `t_actor` 141 | Only valid for `mixin`s. 142 | Very similar to `actor`, but checks the base template the mixin is running on, not the input. 143 | For instance, if one has a `template` that generates triangular particle effects, one could put `tri` in its actor field and then use `t_actor tri` to make mixins that only apply to the particles. 144 | 145 | #### `no_t_actor` 146 | 147 | 148 | #### `sylfx` and `inlinefx` 149 | Only valid for `syl` and `char` components. 150 | Another "interested values" system, this time for checking the value of the current syllable's `syl_fx`/`inline_fx` field. 151 | Useful for expressing conditional execution that changes on a per-syl basis. 152 | 153 | #### `layer` 154 | Only valid for `mixin`s. 155 | 156 | Again, similar to `style` or `actor`. 157 | By default, a mixin is "interested" in all layers. 158 | This modifier restricts a mixin to only execute for certain layer numbers. 159 | 160 | When checking the layer number, I look at *the output's current layer number*. 161 | Output lines start with a layer number copied from the `template` that generated them, but the number may be changed by functions such as `relayer`. 162 | If the layer is changed, I will "see" the updated layer number, not the original. 163 | 164 | #### `if` and `unless` 165 | Easily the most powerful conditional execution modifier. 166 | Given an `if foo` modifier, I will look in the Lua environment for a value named `foo`. 167 | When checking whether to execute, I will expect this value to be either a boolean or a function. 168 | If it is a function, I will call it (with no arguments) and look at the result. 169 | Either way, I then use this value as a predicate to decide whether the component should execute. 170 | 171 | For `if`, the component will only execute if the value is `true`. 172 | For `unless`, the component will only execute if the value is `false`. 173 | 174 | At the moment, only one instance of the `if`/`unless` modifier is allowed per component. 175 | 176 | `cond` is an alias for `if`. 177 | 178 | #### `noblank` 179 | Primarily useful for `syl` and `char` components. 180 | Similar to the `noblank` modifier in other templaters, but also works for mixins. 181 | This is useful for, say, preventing a `mixin char` component from pointlessly executing for spaces. 182 | 183 | Components with the `noblank` modifier will not execute for input whose text is empty or consists entirely of whitespace. 184 | Unlike other templaters, this will *not* filter out syls with zero duration. For that, see `nok0`. 185 | 186 | #### `nok0` 187 | Only valid for `syl` and `char` components. 188 | 189 | Components with this modifier will not execute for syls with zero duration, or chars belonging to such a syl. 190 | 191 | #### `keepspace` 192 | Only valid for `template`s. Primarily useful for `syl` and `word`. 193 | By default, just before inserting a line of output into the file, I will trim away any trailing whitespace. 194 | The `keepspace` modifier will disable this behavior for an individual template, should this be desirable for any reason. 195 | 196 | #### `nomerge` 197 | Only valid for `template`s. 198 | By default, just before removing trailing whitespace, I will merge consecutive tag blocks from output lines. 199 | This is done using the primitive technique of naively removing `}{` from the output, so if this breaks anything fancy, the `nomerge` modifier can disable this behavior. 200 | 201 | #### `notext` 202 | Only valid for `template`s. 203 | Equivalent to the `notext` modifier from other templaters. 204 | By default, the last step of generating a line (i.e. after running all `mixins`) is to append the corresponding text from the source line. The `notext` modifier disables this behavior. 205 | 206 | Useful when, say, generating particle effects with drawings. 207 | 208 | #### `loop` 209 | Other templaters support giving a component a `loop n` modifier, where `n` is some integer. 210 | The component will then execute *n* times, and the current/maximum loop values can be accessed using `j` and `maxj`, available as both Lua variables and inline variables. 211 | Loops follow the convention set by Lua, starting at 1 and including the maximum. 212 | 213 | My implementation of this feature is similar, but I require specifying a name in the modifier, e.g., `loop foo 3`. 214 | The loop count is then accessed using `$loop_foo` or, in Lua, `loopctx.state.foo`, and the maximum with `$maxloop_foo` or `loopctx.max.foo`. 215 | This allows for multiple independent named loops on a single component. 216 | Having multiple loops can be very powerful for anything that requires more than one "dimension" of repetition, like combining some frame-by-frame effect with a vertical gradient. 217 | 218 | If there are multiple loops, they will be nested in *reverse* order of appearance. 219 | As a demonstration of this, consider the following example: 220 | ``` 221 | template line loop foo 2 loop bar 3: {\foo$loop_foo\bar$loop_bar} 222 | kara: {\k86}Word{\k41}less{\k24}ly {\k87}watch{\k33}ing {\k63}he {\k66}waits{\k25} {\k44}by {\k22}the {\k57}win{\k69}dow 223 | kara: {\k36}And {\k38}won{\k59}ders{\k43} {\k25}at {\k16}the {\k40}emp{\k21}ty {\k49}place {\k26}in{\k85}side 224 | fx: {\foo1\bar1}Wordlessly watching he waits by the window 225 | fx: {\foo2\bar1}Wordlessly watching he waits by the window 226 | fx: {\foo1\bar2}Wordlessly watching he waits by the window 227 | fx: {\foo2\bar2}Wordlessly watching he waits by the window 228 | fx: {\foo1\bar3}Wordlessly watching he waits by the window 229 | fx: {\foo2\bar3}Wordlessly watching he waits by the window 230 | fx: {\foo1\bar1}And wonders at the empty place inside 231 | fx: {\foo2\bar1}And wonders at the empty place inside 232 | fx: {\foo1\bar2}And wonders at the empty place inside 233 | fx: {\foo2\bar2}And wonders at the empty place inside 234 | fx: {\foo1\bar3}And wonders at the empty place inside 235 | fx: {\foo2\bar3}And wonders at the empty place inside 236 | ``` 237 | See the `incr` method in the `loopctx` class for how this works. 238 | Basically, usage of `loop` is treated as one big loop, where the iteration step entails treating the loopctx as a little-endian list of digits: 239 | * Increment the first loop variable. 240 | * If this exceeds that loop's maximum value, step forward by resetting it to 1 and incrementing the *next* loop variable. 241 | * If the next loop variable also overflowed, step forward again in the same fashion. Repeat. 242 | * If the final loop variable has overflowed, the "big loop" is done. 243 | 244 | `repeat` is an alias for `loop`. 245 | 246 | Mixins now support loops! Loop variables for mixins are namespaced separately from their template's loop variables. 247 | As inline variables, they are accessed with `$mloop_bar` and `$maxmloop_bar`. 248 | In Lua, they are accessed from `mloopctx` instead of `loopctx`, and the `maxmloop` function takes the place of `maxloop`. 249 | 250 | #### `prefix` 251 | Only valid for `mixin`s. 252 | Mixin output is usually placed just before the text it's associated with. 253 | The `prefix` modifier instead places it at the beginning of the line, alongside the base `template` text and any `mixin line` components. 254 | Consider the following example, where one `mixin word` component has the `prefix` modifier and one does not: 255 | ``` 256 | template line: {\X} 257 | mixin word prefix: {\Y(!word.text!)} 258 | mixin word: {\Z} 259 | kara: {\k86}Word{\k41}less{\k24}ly {\k87}watch{\k33}ing {\k63}he {\k66}waits{\k25} {\k44}by {\k22}the {\k57}win{\k69}dow 260 | kara: {\k36}And {\k38}won{\k59}ders{\k43} {\k25}at {\k16}the {\k40}emp{\k21}ty {\k49}place {\k26}in{\k85}side 261 | fx: {\X\Y(Wordlessly )\Y(watching )\Y(he )\Y(waits )\Y(by )\Y(the )\Y(window)\Z}Wordlessly {\Z}watching {\Z}he {\Z}waits {\Z}by {\Z}the {\Z}window 262 | fx: {\X\Y(And )\Y(wonders )\Y(at )\Y(the )\Y(empty )\Y(place )\Y(inside)\Z}And {\Z}wonders {\Z}at {\Z}the {\Z}empty {\Z}place {\Z}inside 263 | ``` 264 | 265 | This may seem like a fairly strange modifier to offer, but I have it for a fairly specific purpose: using `\t(\clip)` in a `mixin syl prefix` component to produce behavior similar to `\kf`, but with all the flexibility of clips and transforms. 266 | 267 | # Template Execution 268 | TODO 269 | 270 | ## Execution Order/Semantics 271 | ``` 272 | for each line of input: 273 | (try to) run all `code line` components 274 | (try to) run all `template line` components 275 | 276 | for each word in the line: 277 | (try to) run all `code word` components 278 | (try to) run all `template word` components 279 | 280 | do the above for syls and chars 281 | ``` 282 | 283 | (mixins happen as part of "running a template") 284 | see the `apply_templates` function for more details 285 | 286 | TODO 287 | 288 | ## Inline Expressions 289 | In a template's text, Lua expressions enclosed in a pair of `!` will be evaluated, with their result placed in the output text, like `!word.text!` as used in an earlier example. 290 | Unlike other templaters, I will treat `nil` as an empty string here, which is convenient when writing functions like `retime` and `relayer` that are useful for their side effects. 291 | 292 | Depending on the component being executed, a few tables such as `line`, `syl`, `word`, and/or `char` will be in scope, and will be populated with information such as `.text_stripped`, `.duration`, or `.width`. 293 | More information on common fields can be found [here](https://aegisub.shinon71.moe/Automation/Lua/Modules/karaskel.lua). 294 | The stock templater lacks full support for `word` and `char` classes, but they are somewhat similar to `syl`. 295 | "Parent" items can be accessed where it makes sense, such as `char.syl` or `word.line`. 296 | 297 | I populate these objects with some fields that aren't available in other templaters. 298 | Notably, in a `syl`, the `.syl_fx` field is similar to the `.inline_fx` field, but does not "stick" to subsequent syls within a line. 299 | 300 | For example, consider the following line: `{\k86}Word{\k41}less{\k24}ly {\k87\-foo}watch{\k33}ing {\k63}he {\k66\-bar}waits{\k25} {\k44}by {\k22\-baz}the {\k57}win{\k69}dow`, and how the fields are populated: 301 | 302 | `syl.text_stripped` | Word | less | ly | watch | ing | he | waits | | by | the | win | dow 303 | --------------------|------|------|----|-------|--------|-------|-------|-------|-------|--------|--------|------- 304 | `syl.inline_fx` | "" | "" | "" | "foo" | "foo" | "foo" | "bar" | "bar" | "bar" | "baz" | "baz" | "baz" 305 | `syl.syl_fx` | "" | "" | "" | "foo" | "" | "" | "bar" | "" | "" | "baz" | "" | "" 306 | 307 | TODO 308 | 309 | ## Inline Variables 310 | In a template's text, a dollar sign followed by certain names will be expanded to certain values, like `$loop_foo` as used in an earlier example. 311 | I don't support all the same loop variables as other templaters, and the ones I do support will often have different names. 312 | More loop variables will probably be added in the future, since they're so convenient. 313 | For now, see the `eval_inline_var` function for what I currently offer. 314 | 315 | TODO 316 | 317 | ## Available Functions 318 | If present, the `ln.kara` and `0x.color` modules will be loaded automatically and can be accessed as `ln` and `colorlib`, respectively. 319 | 320 | Modules (code files that contain helper functions, as opposed to macros, which generally call `aegisub.register_macro` at some point) go in the `include` directory. In accordance with the standard Lua module system, those two modules must be located at `ln/kara.lua` and `0x/color.lua` respectively in order for those paths to resolve. This is completely normal; I'm not doing anything special here. Any auto4 code that loads any module follows these same rules. 321 | 322 | Several familiar functions from other templaters, such as `retime` and `maxloop`, are available with a few enhancements, and I also offer a few new functions. 323 | 324 | See the `util` and `template_env` declarations in my source code for my implementations of these functions, and to see what standard library functionality I expose by default. 325 | 326 | ### `print(...)` 327 | A handy debugging function. Works like Lua's standard `print` function, but backed by `aegisub.log`. 328 | I also offer `printf`, which is literally just an alias for `aegisub.log`. 329 | 330 | ### `retime(mode, start_offset=0, end_offset=0)` 331 | If you have any past templating experience, this function should be familiar. 332 | It's the most convenient way to change an output line's start/end times. 333 | 334 | I have a handful of modes that can be pretty useful and aren't necessarily available in other templaters. 335 | In particular, the `clamp` modes can be used to ensure other retimes don't extend the output's start/end times past the input's. 336 | 337 | | Mode | `line.start_time = start_offset +` | `line.end_time = end_offset +` | 338 | |---------------------|---------------------------------------|---------------------------------------| 339 | | `"syl"` | `orgline.start_time + syl.start_time` | `orgline.start_time + syl.end_time` | 340 | | `"presyl"` | `orgline.start_time + syl.start_time` | `orgline.start_time + syl.start_time` | 341 | | `"postsyl"` | `orgline.start_time + syl.end_time` | `orgline.start_time + syl.end_time` | 342 | | `"line"` | `orgline.start_time` | `orgline.end_time` | 343 | | `"preline"` | `orgline.start_time` | `orgline.start_time` | 344 | | `"postline"` | `orgline.end_time` | `orgline.end_time` | 345 | | `"start2syl"` | `orgline.start_time` | `orgline.start_time + syl.start_time` | 346 | | `"syl2end"` | `orgline.start_time + syl.end_time` | `orgline.end_time` | 347 | | `"presyl2postline"` | `orgline.start_time + syl.start_time` | `orgline.end_time` | 348 | | `"preline2postsyl"` | `orgline.start_time` | `orgline.start_time + syl.end_time` | 349 | | `"delta"` | `line.start_time` | `line.end_time` | 350 | | `"set"`, `"abs"` | `0` | `0` | 351 | 352 | As a special case, `clamp` keeps the start and end times unchanged, unless they would fall outside the input line's start/end times (plus offsets, if applicable). 353 | Similarly, `clampsyl` does the same, but with the original syl's start/end times. 354 | 355 | | Mode | `line.start_time = max(line.start_time,` | `line.end_time = min(line.end_time,` | 356 | |---------------------|------------------------------------------|--------------------------------------| 357 | | `"clamp"` | `orgline.start_time + start_offset)` | `orgline.end_time + end_offset)` | 358 | | `"clampsyl"` | `syl.start_time + start_offset)` | `syl.end_time + end_offset)` | 359 | 360 | ### `relayer(new_layer)` 361 | `line.layer = new_layer`. Enough said. 362 | 363 | ### `maxloop(var, val, index=nil)`, `maxmloop(var, val, index=nil)` 364 | *Basically* `loopctx.max[var] = val`. 365 | Usually useful if your loop count isn't hardcoded; perhaps it's based on the width of the syllable. 366 | Note that this function will get called repeatedly if it's in a component that gets called repeatedly, so it's generally advisable to use a `val` that remains consistent across repeated calls. 367 | `maxloop` is for `template` loops, while `maxmloop` is for `mixin` loops. 368 | 369 | If the loop has not already been defined, it will be added to my internal list of loops using `table.insert`. 370 | You can specify `index` to control where the variable goes in the list, and by extension the nesting order of your loops. 371 | 372 | ### `set(key, val)` 373 | `tenv[key] = val`. For if you want to assign to a (global) variable in an inline expression, and that's about it. 374 | 375 | ### `skip()` 376 | If this function is called while generating a line, that specific line of output will be discarded. 377 | (The rest of the template will still be evaluated, resulting in a complete generated line - the only difference is that the line won't be inserted into the document.) 378 | 379 | One possible use case for this is to call it in a mixin that has condition modifiers of some sort to express "If these conditions are met, *do not* generate output", since condition modifiers themselves can only express "execute only if these conditions are met". 380 | 381 | This is similar to calling `maxloop(0)` in karaOK, except not tied to a somewhat orthogonal feature, so one could in theory skip certain iterations of a loop without prematurely ending the entire loop. 382 | 383 | ### `unskip()` 384 | Cancels the effect of a previous `skip()` call, meaning the line will once again be included in the document. 385 | 386 | Whichever of the two functions was called most recently takes precedence. 387 | 388 | ### `mskip()` 389 | Skips the current iteration of the current mixin. 390 | Works very similarly to `skip()`, but on the level of an individual mixin's evaluation. 391 | 392 | ### `unmskip()` 393 | Take a wild guess. 394 | 395 | ### `util.tag_or_default(tag, default)` 396 | logarithm's `ln.line.tag` is a nice function, but because of how other templaters treat inline expressions that evaluate to `nil`, it returns an empty string if it finds no tags. 397 | The empty string is truthy in Lua, so this means that `ln.line.tag("foo") or "\\foo(bar)"` doesn't work the way one might hope. 398 | This function arose from a frustration with having to write code to do that so many times, so I allow you to just do `util.tag_or_default("foo", "\\foo(bar)")`. Hooray! 399 | 400 | ### `util.fad(t_in, t_out)` 401 | A specialization of `util.tag_or_default`, this function will handle formatting the `\fad` tag for you. 402 | 403 | ### `util.xf(obj=char, objs=orgline.chars, field='center')` 404 | Most commonly called with no arguments. 405 | Returns the position of the current character, returning 0.0 for the first char in the line and 1.0 for the final char. 406 | 407 | Supports the passing of alternative objects and position fields, *e.g.*, `util.xf(syl, orgline.syls, 'right')` 408 | 409 | In the beginning, there was `char.ci / #orgline.chars`, used by many a gradient-by-character script. 410 | However, index-based calculations are flawed for variable-width fonts, and may produce lopsided gradients depending on the glyphs within the line. 411 | Then there was `char.x / orgline.width`. 412 | This was better, but whether looking at the left, center, or right, this approach does not reach both extremes. 413 | Finally, there came `char.center / (final_char.center - first_char.center)`, and it was good. 414 | It became an annoying piece of boilerplate, though, so now I offer this function to just do it. 415 | 416 | This function was formerly known as `util.cx`. 417 | This name now exists as an alias for backwards compatibility. 418 | 419 | ### `util.lerp(t, v0, v1)` 420 | Straightforward linear interpolation of two numbers: `(v1 * t) + (v0 * (1 - t))`. 421 | 422 | ### `util.gbc(c1, c2, interp, t=util.xf())` 423 | A somewhat flexible function for gradient-by-character effects. 424 | Typical usage would go in a `mixin char` component and look something like `{\3c!util.gbc('&H0000FF&', '&HFF0000&')!}`. 425 | If no `interp` function is provided, I will try to guess how to interpolate the two values. 426 | At the moment, this means: 427 | * If both values are numbers, use `util.lerp`. 428 | * If both values are strings and the first value matches the usual format for an alpha tag (i.e., the pattern `&H[0-9a-fA-F][0-9a-fA-F]&`), use `colorlib.interp_alpha`. 429 | * Otherwise, if they're both strings, assume they're colors and use `colorlib.interp_lch`. 430 | * Otherwise, give up. 431 | 432 | If this is unsatisfactory, well, that's why I expose this function's third argument: specify your own interpolation function with a signature analagous to `util.lerp`'s. 433 | 434 | ### `util.multi_gbc(cs, interp, t=util.xf())` 435 | Like `util.gbc`, but accepts a *list* of colors, for multi-stop gradients. Nifty. 436 | 437 | ### `util.make_grad(v1, v2, dv=1, vertical=true, loopname='grad', extend=true, index=nil)` 438 | The first step to creating a clip-based gradient. 439 | 440 | `v1` and `v2` are the min/max values for the span of the gradient. 441 | `dv` is the "step size": each segment of the gradient will generally be this size. 442 | `vertical` determines whether the gradient goes from top to bottom or from left to right. 443 | Unless `extend` is false, the first and last gradient segments will have their clips extended to the edge of the screen. 444 | 445 | This function calls [`maxloop`](#maxloopvar-val-indexnil-maxmloopvar-val-indexnil) in order to emit multiple lines; 446 | `loopname` controls the loop variable used for the gradient, while `index` controls loop nesting order (see `maxloop` docs). 447 | 448 | ### `util.get_grad(c1, c2, interp, loopname='grad', offset=0)` 449 | The second step to creating a clip-based gradient. 450 | This works in largely the same way as `util.gbc`, 451 | except that it determines the interpolation `t` value using the template loop status. 452 | 453 | `offset` will be added to the loop state value when calculating `t`. This is useful for accurately determining the "next" or "previous" gradient value. 454 | 455 | ### `util.get_multi_grad(cs, interp, loopname='grad')` 456 | Like `multi_gbc`, but for `get_grad`. 457 | 458 | ### `util.ftoa(n, digits=2)` 459 | Formats `n` as a string with at most `digits` decimal digits, *without* any unwanted trailing zeroes/decimal point. 460 | Why does no programming language ever seem to make this easy? 461 | 462 | ### `util.fbf(mode='line', start_offset=0, end_offset=0, frames=1, loopname='fbf', index=1)` 463 | Similar to `retime` in usage, but splits a line into one copy timed to each frame of video. 464 | If `frames` is set to a larger integer, then the line will be split into sections that each last that many frames. 465 | Much like `util.make_grad`, this function uses regular user-accessible template loops; the loop variable can be customized using the `loopname` argument. 466 | 467 | `index` is passed directly to [`maxloop`](#maxloopvar-val-indexnil-maxmloopvar-val-indexnil) and controls loop nesting order. 468 | The default value of `1` nests the fbf loop inside all pre-existing loops. 469 | 470 | ### `util.rand.sign()` 471 | Either -1 or 1, randomly. 472 | 473 | ### `util.rand.item(list)` 474 | A random element in the provided list. 475 | Assumes a standard 1-indexed Lua list with no gaps, like any other "list"-y function. 476 | 477 | If passed a string, returns a random UTF-8 code point from its text. 478 | 479 | ### `util.rand.bool(p=0.5)` 480 | A random Boolean value, where `p` is the probability that the value is `true`. 481 | 482 | ### `util.rand.choice(a, b, p)` 483 | `if util.rand.bool(p) then a else b` 484 | 485 | ### `util.math.round(n)` 486 | Rounds `n` to the nearest integer. 487 | -------------------------------------------------------------------------------- /depctrl/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 25 | 26 | [[package]] 27 | name = "backtrace" 28 | version = "0.3.69" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 31 | dependencies = [ 32 | "addr2line", 33 | "cc", 34 | "cfg-if", 35 | "libc", 36 | "miniz_oxide", 37 | "object", 38 | "rustc-demangle", 39 | ] 40 | 41 | [[package]] 42 | name = "backtrace-ext" 43 | version = "0.2.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" 46 | dependencies = [ 47 | "backtrace", 48 | ] 49 | 50 | [[package]] 51 | name = "base64" 52 | version = "0.21.7" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 55 | 56 | [[package]] 57 | name = "bincode" 58 | version = "1.3.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 61 | dependencies = [ 62 | "serde", 63 | ] 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "1.3.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 70 | 71 | [[package]] 72 | name = "bitflags" 73 | version = "2.4.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 76 | 77 | [[package]] 78 | name = "cc" 79 | version = "1.0.83" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 82 | dependencies = [ 83 | "libc", 84 | ] 85 | 86 | [[package]] 87 | name = "cfg-if" 88 | version = "1.0.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 91 | 92 | [[package]] 93 | name = "chrono" 94 | version = "0.4.33" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" 97 | dependencies = [ 98 | "num-traits", 99 | "serde", 100 | ] 101 | 102 | [[package]] 103 | name = "crc32fast" 104 | version = "1.3.2" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 107 | dependencies = [ 108 | "cfg-if", 109 | ] 110 | 111 | [[package]] 112 | name = "darling" 113 | version = "0.20.5" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" 116 | dependencies = [ 117 | "darling_core", 118 | "darling_macro", 119 | ] 120 | 121 | [[package]] 122 | name = "darling_core" 123 | version = "0.20.5" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" 126 | dependencies = [ 127 | "fnv", 128 | "ident_case", 129 | "proc-macro2", 130 | "quote", 131 | "strsim", 132 | "syn", 133 | ] 134 | 135 | [[package]] 136 | name = "darling_macro" 137 | version = "0.20.5" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" 140 | dependencies = [ 141 | "darling_core", 142 | "quote", 143 | "syn", 144 | ] 145 | 146 | [[package]] 147 | name = "depctrl" 148 | version = "0.1.0" 149 | dependencies = [ 150 | "chrono", 151 | "hex", 152 | "indexmap 2.2.2", 153 | "json-spanned-value", 154 | "miette", 155 | "pretty_assertions", 156 | "serde", 157 | "serde_json", 158 | "serde_with", 159 | "thiserror", 160 | "time", 161 | "ureq", 162 | ] 163 | 164 | [[package]] 165 | name = "deranged" 166 | version = "0.3.11" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 169 | dependencies = [ 170 | "powerfmt", 171 | ] 172 | 173 | [[package]] 174 | name = "diff" 175 | version = "0.1.13" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 178 | 179 | [[package]] 180 | name = "equivalent" 181 | version = "1.0.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 184 | 185 | [[package]] 186 | name = "errno" 187 | version = "0.3.8" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 190 | dependencies = [ 191 | "libc", 192 | "windows-sys 0.52.0", 193 | ] 194 | 195 | [[package]] 196 | name = "fastrand" 197 | version = "2.0.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 200 | 201 | [[package]] 202 | name = "flate2" 203 | version = "1.0.28" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 206 | dependencies = [ 207 | "crc32fast", 208 | "miniz_oxide", 209 | ] 210 | 211 | [[package]] 212 | name = "fnv" 213 | version = "1.0.7" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 216 | 217 | [[package]] 218 | name = "form_urlencoded" 219 | version = "1.2.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 222 | dependencies = [ 223 | "percent-encoding", 224 | ] 225 | 226 | [[package]] 227 | name = "getrandom" 228 | version = "0.2.12" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 231 | dependencies = [ 232 | "cfg-if", 233 | "libc", 234 | "wasi", 235 | ] 236 | 237 | [[package]] 238 | name = "gimli" 239 | version = "0.28.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 242 | 243 | [[package]] 244 | name = "hashbrown" 245 | version = "0.12.3" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 248 | 249 | [[package]] 250 | name = "hashbrown" 251 | version = "0.14.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 254 | 255 | [[package]] 256 | name = "hex" 257 | version = "0.4.3" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 260 | 261 | [[package]] 262 | name = "hoot" 263 | version = "0.1.3" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452" 266 | dependencies = [ 267 | "httparse", 268 | "log", 269 | ] 270 | 271 | [[package]] 272 | name = "hootbin" 273 | version = "0.1.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2" 276 | dependencies = [ 277 | "fastrand", 278 | "hoot", 279 | "serde", 280 | "serde_json", 281 | "thiserror", 282 | ] 283 | 284 | [[package]] 285 | name = "httparse" 286 | version = "1.8.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 289 | 290 | [[package]] 291 | name = "ident_case" 292 | version = "1.0.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 295 | 296 | [[package]] 297 | name = "idna" 298 | version = "0.5.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 301 | dependencies = [ 302 | "unicode-bidi", 303 | "unicode-normalization", 304 | ] 305 | 306 | [[package]] 307 | name = "indexmap" 308 | version = "1.9.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 311 | dependencies = [ 312 | "autocfg", 313 | "hashbrown 0.12.3", 314 | ] 315 | 316 | [[package]] 317 | name = "indexmap" 318 | version = "2.2.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" 321 | dependencies = [ 322 | "equivalent", 323 | "hashbrown 0.14.3", 324 | "serde", 325 | ] 326 | 327 | [[package]] 328 | name = "is_ci" 329 | version = "1.2.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" 332 | 333 | [[package]] 334 | name = "itoa" 335 | version = "1.0.10" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 338 | 339 | [[package]] 340 | name = "json-spanned-value" 341 | version = "0.2.2" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "bb343fa4e3b1b22b344937deedac88da995abf139c2232cbeaa436c38380a210" 344 | dependencies = [ 345 | "indexmap 1.9.3", 346 | "serde", 347 | "serde_json", 348 | ] 349 | 350 | [[package]] 351 | name = "libc" 352 | version = "0.2.153" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 355 | 356 | [[package]] 357 | name = "line-wrap" 358 | version = "0.1.1" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" 361 | dependencies = [ 362 | "safemem", 363 | ] 364 | 365 | [[package]] 366 | name = "linked-hash-map" 367 | version = "0.5.6" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 370 | 371 | [[package]] 372 | name = "linux-raw-sys" 373 | version = "0.4.13" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 376 | 377 | [[package]] 378 | name = "log" 379 | version = "0.4.20" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 382 | 383 | [[package]] 384 | name = "memchr" 385 | version = "2.7.1" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 388 | 389 | [[package]] 390 | name = "miette" 391 | version = "7.0.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "98a72adfa0c7ae88ba0abcbd00047a476616c66b831d628b8ac7f1e9de0cfd67" 394 | dependencies = [ 395 | "backtrace", 396 | "backtrace-ext", 397 | "miette-derive", 398 | "owo-colors", 399 | "supports-color", 400 | "supports-hyperlinks", 401 | "supports-unicode", 402 | "syntect", 403 | "terminal_size", 404 | "textwrap", 405 | "thiserror", 406 | "unicode-width", 407 | ] 408 | 409 | [[package]] 410 | name = "miette-derive" 411 | version = "7.0.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "279def6bf114a34b3cf887489eb440d4dfcf709ab3ce9955e4a6f957ce5cce77" 414 | dependencies = [ 415 | "proc-macro2", 416 | "quote", 417 | "syn", 418 | ] 419 | 420 | [[package]] 421 | name = "miniz_oxide" 422 | version = "0.7.2" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 425 | dependencies = [ 426 | "adler", 427 | ] 428 | 429 | [[package]] 430 | name = "num-conv" 431 | version = "0.1.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 434 | 435 | [[package]] 436 | name = "num-traits" 437 | version = "0.2.18" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 440 | dependencies = [ 441 | "autocfg", 442 | ] 443 | 444 | [[package]] 445 | name = "object" 446 | version = "0.32.2" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 449 | dependencies = [ 450 | "memchr", 451 | ] 452 | 453 | [[package]] 454 | name = "once_cell" 455 | version = "1.19.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 458 | 459 | [[package]] 460 | name = "onig" 461 | version = "6.4.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" 464 | dependencies = [ 465 | "bitflags 1.3.2", 466 | "libc", 467 | "once_cell", 468 | "onig_sys", 469 | ] 470 | 471 | [[package]] 472 | name = "onig_sys" 473 | version = "69.8.1" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" 476 | dependencies = [ 477 | "cc", 478 | "pkg-config", 479 | ] 480 | 481 | [[package]] 482 | name = "owo-colors" 483 | version = "4.0.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" 486 | 487 | [[package]] 488 | name = "percent-encoding" 489 | version = "2.3.1" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 492 | 493 | [[package]] 494 | name = "pkg-config" 495 | version = "0.3.29" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" 498 | 499 | [[package]] 500 | name = "plist" 501 | version = "1.6.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" 504 | dependencies = [ 505 | "base64", 506 | "indexmap 2.2.2", 507 | "line-wrap", 508 | "quick-xml", 509 | "serde", 510 | "time", 511 | ] 512 | 513 | [[package]] 514 | name = "powerfmt" 515 | version = "0.2.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 518 | 519 | [[package]] 520 | name = "pretty_assertions" 521 | version = "1.4.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" 524 | dependencies = [ 525 | "diff", 526 | "yansi", 527 | ] 528 | 529 | [[package]] 530 | name = "proc-macro2" 531 | version = "1.0.78" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 534 | dependencies = [ 535 | "unicode-ident", 536 | ] 537 | 538 | [[package]] 539 | name = "quick-xml" 540 | version = "0.31.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 543 | dependencies = [ 544 | "memchr", 545 | ] 546 | 547 | [[package]] 548 | name = "quote" 549 | version = "1.0.35" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 552 | dependencies = [ 553 | "proc-macro2", 554 | ] 555 | 556 | [[package]] 557 | name = "regex-syntax" 558 | version = "0.8.2" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 561 | 562 | [[package]] 563 | name = "ring" 564 | version = "0.17.7" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" 567 | dependencies = [ 568 | "cc", 569 | "getrandom", 570 | "libc", 571 | "spin", 572 | "untrusted", 573 | "windows-sys 0.48.0", 574 | ] 575 | 576 | [[package]] 577 | name = "rustc-demangle" 578 | version = "0.1.23" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 581 | 582 | [[package]] 583 | name = "rustix" 584 | version = "0.38.31" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 587 | dependencies = [ 588 | "bitflags 2.4.2", 589 | "errno", 590 | "libc", 591 | "linux-raw-sys", 592 | "windows-sys 0.52.0", 593 | ] 594 | 595 | [[package]] 596 | name = "rustls" 597 | version = "0.22.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" 600 | dependencies = [ 601 | "log", 602 | "ring", 603 | "rustls-pki-types", 604 | "rustls-webpki", 605 | "subtle", 606 | "zeroize", 607 | ] 608 | 609 | [[package]] 610 | name = "rustls-pki-types" 611 | version = "1.2.0" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" 614 | 615 | [[package]] 616 | name = "rustls-webpki" 617 | version = "0.102.2" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" 620 | dependencies = [ 621 | "ring", 622 | "rustls-pki-types", 623 | "untrusted", 624 | ] 625 | 626 | [[package]] 627 | name = "ryu" 628 | version = "1.0.16" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 631 | 632 | [[package]] 633 | name = "safemem" 634 | version = "0.3.3" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 637 | 638 | [[package]] 639 | name = "same-file" 640 | version = "1.0.6" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 643 | dependencies = [ 644 | "winapi-util", 645 | ] 646 | 647 | [[package]] 648 | name = "serde" 649 | version = "1.0.196" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 652 | dependencies = [ 653 | "serde_derive", 654 | ] 655 | 656 | [[package]] 657 | name = "serde_derive" 658 | version = "1.0.196" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 661 | dependencies = [ 662 | "proc-macro2", 663 | "quote", 664 | "syn", 665 | ] 666 | 667 | [[package]] 668 | name = "serde_json" 669 | version = "1.0.113" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" 672 | dependencies = [ 673 | "itoa", 674 | "ryu", 675 | "serde", 676 | ] 677 | 678 | [[package]] 679 | name = "serde_with" 680 | version = "3.6.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" 683 | dependencies = [ 684 | "serde", 685 | "serde_derive", 686 | "serde_with_macros", 687 | ] 688 | 689 | [[package]] 690 | name = "serde_with_macros" 691 | version = "3.6.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" 694 | dependencies = [ 695 | "darling", 696 | "proc-macro2", 697 | "quote", 698 | "syn", 699 | ] 700 | 701 | [[package]] 702 | name = "smawk" 703 | version = "0.3.2" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 706 | 707 | [[package]] 708 | name = "spin" 709 | version = "0.9.8" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 712 | 713 | [[package]] 714 | name = "strsim" 715 | version = "0.10.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 718 | 719 | [[package]] 720 | name = "subtle" 721 | version = "2.5.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 724 | 725 | [[package]] 726 | name = "supports-color" 727 | version = "3.0.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" 730 | dependencies = [ 731 | "is_ci", 732 | ] 733 | 734 | [[package]] 735 | name = "supports-hyperlinks" 736 | version = "3.0.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" 739 | 740 | [[package]] 741 | name = "supports-unicode" 742 | version = "3.0.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" 745 | 746 | [[package]] 747 | name = "syn" 748 | version = "2.0.48" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 751 | dependencies = [ 752 | "proc-macro2", 753 | "quote", 754 | "unicode-ident", 755 | ] 756 | 757 | [[package]] 758 | name = "syntect" 759 | version = "5.2.0" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" 762 | dependencies = [ 763 | "bincode", 764 | "bitflags 1.3.2", 765 | "flate2", 766 | "fnv", 767 | "once_cell", 768 | "onig", 769 | "plist", 770 | "regex-syntax", 771 | "serde", 772 | "serde_derive", 773 | "serde_json", 774 | "thiserror", 775 | "walkdir", 776 | "yaml-rust", 777 | ] 778 | 779 | [[package]] 780 | name = "terminal_size" 781 | version = "0.3.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 784 | dependencies = [ 785 | "rustix", 786 | "windows-sys 0.48.0", 787 | ] 788 | 789 | [[package]] 790 | name = "textwrap" 791 | version = "0.16.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" 794 | dependencies = [ 795 | "smawk", 796 | "unicode-linebreak", 797 | "unicode-width", 798 | ] 799 | 800 | [[package]] 801 | name = "thiserror" 802 | version = "1.0.56" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" 805 | dependencies = [ 806 | "thiserror-impl", 807 | ] 808 | 809 | [[package]] 810 | name = "thiserror-impl" 811 | version = "1.0.56" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" 814 | dependencies = [ 815 | "proc-macro2", 816 | "quote", 817 | "syn", 818 | ] 819 | 820 | [[package]] 821 | name = "time" 822 | version = "0.3.37" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 825 | dependencies = [ 826 | "deranged", 827 | "itoa", 828 | "num-conv", 829 | "powerfmt", 830 | "serde", 831 | "time-core", 832 | "time-macros", 833 | ] 834 | 835 | [[package]] 836 | name = "time-core" 837 | version = "0.1.2" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 840 | 841 | [[package]] 842 | name = "time-macros" 843 | version = "0.2.19" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 846 | dependencies = [ 847 | "num-conv", 848 | "time-core", 849 | ] 850 | 851 | [[package]] 852 | name = "tinyvec" 853 | version = "1.6.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 856 | dependencies = [ 857 | "tinyvec_macros", 858 | ] 859 | 860 | [[package]] 861 | name = "tinyvec_macros" 862 | version = "0.1.1" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 865 | 866 | [[package]] 867 | name = "unicode-bidi" 868 | version = "0.3.15" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 871 | 872 | [[package]] 873 | name = "unicode-ident" 874 | version = "1.0.12" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 877 | 878 | [[package]] 879 | name = "unicode-linebreak" 880 | version = "0.1.5" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 883 | 884 | [[package]] 885 | name = "unicode-normalization" 886 | version = "0.1.22" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 889 | dependencies = [ 890 | "tinyvec", 891 | ] 892 | 893 | [[package]] 894 | name = "unicode-width" 895 | version = "0.1.11" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 898 | 899 | [[package]] 900 | name = "untrusted" 901 | version = "0.9.0" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 904 | 905 | [[package]] 906 | name = "ureq" 907 | version = "2.9.5" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "0b52731d03d6bb2fd18289d4028aee361d6c28d44977846793b994b13cdcc64d" 910 | dependencies = [ 911 | "base64", 912 | "hootbin", 913 | "log", 914 | "once_cell", 915 | "rustls", 916 | "rustls-pki-types", 917 | "rustls-webpki", 918 | "url", 919 | "webpki-roots", 920 | ] 921 | 922 | [[package]] 923 | name = "url" 924 | version = "2.5.0" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 927 | dependencies = [ 928 | "form_urlencoded", 929 | "idna", 930 | "percent-encoding", 931 | ] 932 | 933 | [[package]] 934 | name = "walkdir" 935 | version = "2.4.0" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 938 | dependencies = [ 939 | "same-file", 940 | "winapi-util", 941 | ] 942 | 943 | [[package]] 944 | name = "wasi" 945 | version = "0.11.0+wasi-snapshot-preview1" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 948 | 949 | [[package]] 950 | name = "webpki-roots" 951 | version = "0.26.1" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" 954 | dependencies = [ 955 | "rustls-pki-types", 956 | ] 957 | 958 | [[package]] 959 | name = "winapi" 960 | version = "0.3.9" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 963 | dependencies = [ 964 | "winapi-i686-pc-windows-gnu", 965 | "winapi-x86_64-pc-windows-gnu", 966 | ] 967 | 968 | [[package]] 969 | name = "winapi-i686-pc-windows-gnu" 970 | version = "0.4.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 973 | 974 | [[package]] 975 | name = "winapi-util" 976 | version = "0.1.6" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 979 | dependencies = [ 980 | "winapi", 981 | ] 982 | 983 | [[package]] 984 | name = "winapi-x86_64-pc-windows-gnu" 985 | version = "0.4.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 988 | 989 | [[package]] 990 | name = "windows-sys" 991 | version = "0.48.0" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 994 | dependencies = [ 995 | "windows-targets 0.48.5", 996 | ] 997 | 998 | [[package]] 999 | name = "windows-sys" 1000 | version = "0.52.0" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1003 | dependencies = [ 1004 | "windows-targets 0.52.0", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "windows-targets" 1009 | version = "0.48.5" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1012 | dependencies = [ 1013 | "windows_aarch64_gnullvm 0.48.5", 1014 | "windows_aarch64_msvc 0.48.5", 1015 | "windows_i686_gnu 0.48.5", 1016 | "windows_i686_msvc 0.48.5", 1017 | "windows_x86_64_gnu 0.48.5", 1018 | "windows_x86_64_gnullvm 0.48.5", 1019 | "windows_x86_64_msvc 0.48.5", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "windows-targets" 1024 | version = "0.52.0" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1027 | dependencies = [ 1028 | "windows_aarch64_gnullvm 0.52.0", 1029 | "windows_aarch64_msvc 0.52.0", 1030 | "windows_i686_gnu 0.52.0", 1031 | "windows_i686_msvc 0.52.0", 1032 | "windows_x86_64_gnu 0.52.0", 1033 | "windows_x86_64_gnullvm 0.52.0", 1034 | "windows_x86_64_msvc 0.52.0", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "windows_aarch64_gnullvm" 1039 | version = "0.48.5" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1042 | 1043 | [[package]] 1044 | name = "windows_aarch64_gnullvm" 1045 | version = "0.52.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1048 | 1049 | [[package]] 1050 | name = "windows_aarch64_msvc" 1051 | version = "0.48.5" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1054 | 1055 | [[package]] 1056 | name = "windows_aarch64_msvc" 1057 | version = "0.52.0" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1060 | 1061 | [[package]] 1062 | name = "windows_i686_gnu" 1063 | version = "0.48.5" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1066 | 1067 | [[package]] 1068 | name = "windows_i686_gnu" 1069 | version = "0.52.0" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1072 | 1073 | [[package]] 1074 | name = "windows_i686_msvc" 1075 | version = "0.48.5" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1078 | 1079 | [[package]] 1080 | name = "windows_i686_msvc" 1081 | version = "0.52.0" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1084 | 1085 | [[package]] 1086 | name = "windows_x86_64_gnu" 1087 | version = "0.48.5" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1090 | 1091 | [[package]] 1092 | name = "windows_x86_64_gnu" 1093 | version = "0.52.0" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1096 | 1097 | [[package]] 1098 | name = "windows_x86_64_gnullvm" 1099 | version = "0.48.5" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1102 | 1103 | [[package]] 1104 | name = "windows_x86_64_gnullvm" 1105 | version = "0.52.0" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1108 | 1109 | [[package]] 1110 | name = "windows_x86_64_msvc" 1111 | version = "0.48.5" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1114 | 1115 | [[package]] 1116 | name = "windows_x86_64_msvc" 1117 | version = "0.52.0" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1120 | 1121 | [[package]] 1122 | name = "yaml-rust" 1123 | version = "0.4.5" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1126 | dependencies = [ 1127 | "linked-hash-map", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "yansi" 1132 | version = "0.5.1" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 1135 | 1136 | [[package]] 1137 | name = "zeroize" 1138 | version = "1.7.0" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 1141 | -------------------------------------------------------------------------------- /src/0x.KaraTemplater.moon: -------------------------------------------------------------------------------- 1 | export script_name = '0x539\'s Templater' 2 | export script_description = '' 3 | export script_author = 'The0x539' 4 | export script_version = '0.1.0' 5 | export script_namespace = '0x.KaraTemplater' 6 | 7 | require 'karaskel' 8 | 9 | try_import = (name) -> 10 | success, module = pcall require, name 11 | if not success 12 | -- Create a dummy table that exists, but gives an informative error when accessed. 13 | accessor = () -> 14 | -- I cannot for the life of me figure out why no matter what value I pass 15 | -- as the second argument to `error`, the log window still shows a full stack trace. 16 | -- This is absurd. 17 | aegisub.log 0, "This template depends on the `#{name}` module. Please install it." 18 | aegisub.cancel! 19 | 20 | module = setmetatable {}, {__index: accessor, __newindex: accessor} 21 | module, success 22 | 23 | local print_stacktrace 24 | 25 | karaOK, USE_KARAOK = try_import 'ln.kara' 26 | colorlib, USE_COLOR = try_import '0x.color' 27 | 28 | -- A magic table that is interested in every style. 29 | all_styles = {} 30 | setmetatable all_styles, 31 | __index: -> true 32 | __newindex: -> 33 | 34 | check_cancel = () -> 35 | if aegisub.progress.is_cancelled! 36 | aegisub.cancel! 37 | 38 | -- Try to determine an apppropriate interpolation function for the two values provided. 39 | guess_interp = (tenv, c1, c2) -> 40 | ctype = type c1 41 | if ctype != type c2 42 | error "attempt to interpolate mismatched types: #{ctype} / #{type c2}" 43 | 44 | switch ctype 45 | when 'number' then tenv.util.lerp 46 | when 'string' 47 | -- the assumptions made in this branch are fallible and subject to future improvement 48 | if c1\match '&H[0-9a-fA-F][0-9a-fA-F]&' 49 | tenv.colorlib.interp_alpha 50 | else 51 | tenv.colorlib.interp_lch 52 | else error "unknown gradient type: #{ctype}. please pass a custom interpolation function." 53 | 54 | util = (tenv) -> { 55 | tag_or_default: (tag, default) -> 56 | value = karaOK.line.tag tag 57 | if value == '' then default else value 58 | 59 | fad: (t_in, t_out) -> tenv.util.tag_or_default {'fad', 'fade'}, "\\fad(#{t_in},#{t_out})" 60 | 61 | -- "x fraction": first obj gets 0.0, last gets 1.0, and the rest get appropriate fractional values based on their center 62 | xf: (obj=tenv.char, objs=tenv.orgline.chars, field='center') -> 63 | x = obj[field] 64 | x0 = objs[1][field] 65 | x1 = objs[#objs][field] 66 | 67 | if x1 == x0 then return 0 68 | 69 | (x - x0) / (x1 - x0) 70 | 71 | lerp: (t, v0, v1) -> (v1 * t) + (v0 * (1 - t)) 72 | 73 | gbc: (c1, c2, interp, t=tenv.util.xf!) -> 74 | interp or= guess_interp tenv, c1, c2 75 | interp t, c1, c2 76 | 77 | multi_gbc: (cs, interp, t=tenv.util.xf!) -> 78 | if t == 0 then return cs[1] 79 | if t == 1 then return cs[#cs] 80 | 81 | -- Without these parens, moonc outputs incorrect Lua. 82 | -- This is deeply disturbing. 83 | t *= (#cs - 1) 84 | 85 | c1, c2 = cs[1 + math.floor t], cs[2 + math.floor t] 86 | tenv.util.gbc c1, c2, interp, t % 1 87 | 88 | make_grad: (v1, v2, dv=1, vertical=true, loopname='grad', extend=true, index=nil) -> 89 | tenv.maxloop loopname, math.ceil((v2 - v1) / dv), index 90 | 91 | loopctx, meta = tenv.loopctx, tenv.meta 92 | loopval = loopctx.state[loopname] 93 | 94 | w1 = v1 + dv * (loopval - 1) 95 | w2 = v1 + dv * loopval 96 | if extend 97 | if loopval == 1 then w1 = 0 98 | if loopval == loopctx.max[loopname] 99 | w2 = if vertical then meta.res_y else meta.res_x 100 | else 101 | w2 = math.min w2, v2 102 | 103 | -- TODO:(?) some super overkill thing where you can specify all intended usages of this gradient, 104 | -- then it checks if the "next step" will be the same. 105 | -- while so, "take an extra step" in the tenv loop and extend the clip accordingly 106 | -- basically an ahead-of-time "combine gradient lines" action 107 | 108 | ftoa = tenv.util.ftoa 109 | if vertical 110 | "\\clip(0,#{ftoa w1},#{meta.res_x},#{ftoa w2})" 111 | else 112 | "\\clip(#{ftoa w1},0,#{ftoa w2},#{meta.res_y})" 113 | 114 | get_grad: (c1, c2, interp, loopname='grad', offset=0) -> 115 | interp or= guess_interp tenv, c1, c2 116 | -- TODO: expose this calculation somewhere, because it's handy 117 | t = (tenv.loopctx.state[loopname] - 1 + offset) / (tenv.loopctx.max[loopname] - 1) 118 | interp t, c1, c2 119 | 120 | get_multi_grad: (cs, interp, loopname='grad') -> 121 | t = (tenv.loopctx.state[loopname] - 1) / (tenv.loopctx.max[loopname] - 1) 122 | if t == 0 then return cs[1] 123 | if t == 1 then return cs[#cs] 124 | 125 | t *= (#cs - 1) 126 | c1, c2 = cs[1 + math.floor t], cs[2 + math.floor t] 127 | 128 | interp or= guess_interp tenv, c1, c2 129 | interp t % 1, c1, c2 130 | 131 | ftoa: (n, digits=2) -> 132 | assert digits >= 0 and digits == math.floor digits 133 | if n == math.floor n 134 | tostring n 135 | elseif digits == 0 136 | tostring tenv.util.math.round n 137 | else 138 | "%.#{digits}f"\format(n)\gsub('(%.%d-)0+$', '%1')\gsub('%.$', '') 139 | 140 | fbf: (mode='line', start_offset=0, end_offset=0, frames=1, loopname='fbf', index=1) -> 141 | tenv.retime mode, start_offset, end_offset 142 | 143 | first_frame = aegisub.frame_from_ms tenv.line.start_time 144 | last_frame = aegisub.frame_from_ms tenv.line.end_time 145 | 146 | n_frames = last_frame - first_frame 147 | loop_count = math.ceil(n_frames / frames) 148 | tenv.maxloop loopname, loop_count, index 149 | 150 | i = tenv.loopctx.state[loopname] 151 | start_time = aegisub.ms_from_frame(first_frame + (i - 1) * frames) 152 | if i == 1 153 | start_time = tenv.line.start_time 154 | end_time = aegisub.ms_from_frame(math.min(first_frame + i * frames, last_frame)) 155 | if i == tenv.loopctx.max[loopname] 156 | end_time = tenv.line.end_time 157 | 158 | tenv.retime 'abs', start_time, end_time 159 | 160 | rand: { 161 | -- Either -1 or 1, randomly. 162 | sign: -> math.random(0, 1) * 2 - 1 163 | 164 | -- A random entry from a list, or a random code point from a string. 165 | item: (list) -> 166 | if type(list) == 'string' 167 | len = unicode.len list 168 | idx = math.random 1, len 169 | i = 1 170 | for c in unicode.chars list 171 | if i == idx 172 | return c 173 | i += 1 174 | 175 | error 'unreachable' 176 | else 177 | list[math.random 1, #list] 178 | 179 | -- A boolean with a truth probability of p. 180 | bool: (p=0.5) -> math.random! < p 181 | 182 | -- Either of two things, with p chance of picking the first. 183 | choice: (a, b, p) -> if tenv.util.rand.bool p then a else b 184 | } 185 | 186 | math: { 187 | round: (n) -> math.floor(n + 0.5) 188 | } 189 | } 190 | 191 | -- The shared global scope for all template code. 192 | class template_env 193 | :_G, :math, :table, :string, :unicode, :tostring, :tonumber, :aegisub, :error, :karaskel, :require 194 | :colorlib 195 | 196 | printf: aegisub.log 197 | 198 | print: (...) -> 199 | args = {...} 200 | aegisub.log table.concat(args, '\t') 201 | aegisub.log '\n' 202 | 203 | -- Given a self object, returns an actual retime function, which cannot require a self parameter 204 | _retime = => (mode, start_offset=0, end_offset=0) -> 205 | return if @line == nil 206 | syl = @syl 207 | if syl == nil and @char != nil 208 | syl = @char.syl 209 | start_base, end_base = switch mode 210 | when 'syl' then syl.start_time, syl.end_time 211 | when 'presyl' then syl.start_time, syl.start_time 212 | when 'postsyl' then syl.end_time, syl.end_time 213 | when 'line' then 0, @orgline.duration 214 | when 'preline' then 0, 0 215 | when 'postline' then @orgline.duration, @orgline.duration 216 | when 'start2syl' then 0, syl.start_time 217 | when 'syl2end' then syl.end_time, @orgline.duration 218 | when 'presyl2postline' then syl.start_time, @orgline.duration 219 | when 'preline2postsyl' then 0, syl.end_time 220 | when 'delta' 221 | @line.start_time += start_offset 222 | @line.end_time += end_offset 223 | @line.duration = @line.end_time - @line.start_time 224 | return 225 | when 'set', 'abs' 226 | @line.start_time = start_offset 227 | @line.end_time = end_offset 228 | @line.duration = end_offset - start_offset 229 | return 230 | when 'clamp' 231 | @line.start_time = math.max @line.start_time, @orgline.start_time + start_offset 232 | @line.end_time = math.min @line.end_time, @orgline.end_time + end_offset 233 | @line.duration = @line.end_time - @line.start_time 234 | return 235 | when 'clampsyl' 236 | @line.start_time = math.max @line.start_time, syl.start_time + start_offset 237 | @line.end_time = math.min @line.end_time, syl.end_time + end_offset 238 | @line.duration = @line.end_time - @line.start_time 239 | return 240 | else error "Unknown retime mode: #{mode}", 2 241 | 242 | orig_start = @orgline.start_time 243 | @line.start_time = orig_start + start_base + start_offset 244 | @line.end_time = orig_start + end_base + end_offset 245 | @line.duration = @line.end_time - @line.start_time 246 | 247 | _relayer = => (new_layer) -> @line.layer = new_layer 248 | 249 | _maxloop = (name) => (var, val, index) -> 250 | error "Missing maxloop value. Did you forget to specify a loop name?" if val == nil 251 | with @[name] 252 | if .max[var] == nil 253 | if index 254 | table.insert .vars, index, var 255 | else 256 | table.insert .vars, var 257 | .max[var] = val 258 | .state[var] or= 1 259 | -- BUG: there are unaccounted-for situations in which .done should be set to true 260 | unless .state[var] > .max[var] 261 | .done = false 262 | return -- returns the loopctx otherwise, which we don't want 263 | 264 | _set = => (key, val) -> @[key] = val 265 | 266 | new: (@subs, @meta, @styles) => 267 | @_ENV = @ 268 | @tenv = @ 269 | @subtitles = @subs 270 | 271 | @ln = karaOK 272 | if USE_KARAOK 273 | @ln.init @ 274 | -- some monkey-patching to address some execution environment differences from karaOK 275 | monkey_patch = (f) -> (...) -> 276 | patched_syl, patched_line = false, false 277 | if @syl == nil and @char != nil 278 | @syl = @char 279 | patched_syl = true 280 | elseif @syl == nil and @word != nil 281 | @syl = @word 282 | patched_syl = true 283 | if @line == nil and @orgline != nil 284 | @line = @orgline 285 | patched_line = true 286 | retvals = {f ...} 287 | @syl = nil if patched_syl 288 | @line = nil if patched_line 289 | table.unpack retvals 290 | 291 | @ln.tag.pos = monkey_patch @ln.tag.pos 292 | @ln.tag.move = monkey_patch @ln.tag.move 293 | 294 | @util = util @ 295 | -- Not the best place to put an alias, but I don't want to mess up how the body of the util constructor is just a table literal. 296 | @util.cx = @util.xf 297 | 298 | @retime = _retime @ 299 | @relayer = _relayer @ 300 | @maxloop = _maxloop @, 'loopctx' 301 | @maxmloop = _maxloop @, 'mloopctx' 302 | @set = _set @ 303 | 304 | @__private = 305 | compilation_cache: {} 306 | 307 | -- Iterate over all sub lines, collecting those that are code chunks, templates, or mixins. 308 | parse_templates = (subs, tenv) -> 309 | components = 310 | code: {once: {}, line: {}, word: {}, syl: {}, char: {}} 311 | template: {line: {}, word: {}, syl: {}, char: {}} 312 | mixin: {line: {}, word: {}, syl: {}, char: {}} 313 | 314 | interested_styles = {} 315 | 316 | aegisub.progress.set 0 317 | 318 | dialogue_index = 0 319 | 320 | for i, line in ipairs subs 321 | check_cancel! 322 | 323 | continue unless line.class == 'dialogue' 324 | dialogue_index += 1 325 | continue unless line.comment 326 | 327 | error = (msg) -> 328 | tenv.print "Error parsing component on line #{dialogue_index}:" 329 | tenv.print '\t' .. msg 330 | tenv.print! 331 | 332 | tenv.print 'Line text:' 333 | tenv.print '\t' .. line.raw 334 | tenv.print! 335 | 336 | print_stacktrace! 337 | aegisub.cancel! 338 | 339 | effect = line.effect\gsub('^ *', '')\gsub(' *$', '') 340 | first_word = effect\gsub(' .*', '') 341 | continue unless components[first_word] != nil 342 | 343 | modifiers = [word for word in effect\gmatch '[^ ]+'] 344 | line_type, classifier = modifiers[1], modifiers[2] 345 | 346 | if classifier == 'once' and line_type != 'code' 347 | error 'The `once` classifier is only valid on `code` lines.' 348 | 349 | interested_styles[line.style] = true unless classifier == 'once' 350 | 351 | component = 352 | interested_styles: {[line.style]: true} 353 | interested_layers: nil 354 | interested_actors: nil 355 | disinterested_actors: nil 356 | interested_template_actors: nil 357 | disinterested_template_actors: nil 358 | interested_inline_fx: nil 359 | interested_syl_fx: nil 360 | repetitions: {} 361 | repetition_order: {} 362 | condition: nil 363 | cond_is_negated: false 364 | keep_tags: false 365 | multi: false 366 | noblank: false 367 | nok0: false 368 | notext: false 369 | merge_tags: true 370 | strip_trailing_space: true 371 | layer: line.layer 372 | template_actor: line.actor 373 | is_prefix: false 374 | 375 | func: nil -- present on `code` lines 376 | text: nil -- present on `template` and `mixin` lines 377 | 378 | if line_type == 'code' 379 | func, err = load line.text, 'code line', 't', tenv 380 | error err if err != nil 381 | 382 | component.func = func 383 | else 384 | component.text = line.text 385 | 386 | j = 3 387 | while j <= #modifiers 388 | modifier = modifiers[j] 389 | j += 1 390 | switch modifier 391 | when 'cond', 'if', 'unless' 392 | if component.condition != nil 393 | error 'Encountered multiple `cond` modifiers on a single component.' 394 | path = modifiers[j] 395 | j += 1 396 | for pattern in *{'[^A-Za-z0-9_.]', '%.%.', '^[0-9.]', '%.$'} 397 | if path\match pattern 398 | error "Invalid condition path: #{path}" 399 | component.condition = path 400 | if modifier == 'unless' 401 | component.cond_is_negated = true 402 | 403 | when 'loop', 'repeat' 404 | loop_var, loop_count = modifiers[j], tonumber modifiers[j + 1] 405 | j += 2 406 | if component.repetitions[loop_var] != nil 407 | error "Encountered multiple `#{loop_var}` repetitions on a single component." 408 | component.repetitions[loop_var] = loop_count 409 | table.insert component.repetition_order, loop_var 410 | 411 | when 'style' 412 | style_name = modifiers[j] 413 | j += 1 414 | interested_styles[style_name] = true 415 | component.interested_styles[style_name] = true 416 | 417 | when 'anystyle' 418 | interested_styles = all_styles 419 | component.interested_styles = all_styles 420 | 421 | when 'noblank' 422 | if classifier == 'once' 423 | error 'The `noblank` modifier is invalid for `once` components.' 424 | component.noblank = true 425 | 426 | when 'nok0' 427 | unless classifier == 'syl' or classifier == 'char' 428 | error 'The `nok0` modifier is only valid for `syl` and `char` components.' 429 | component.nok0 = true 430 | 431 | when 'keeptags', 'multi' 432 | unless classifier == 'syl' 433 | error "The `#{modifier}` modifier is only valid for `syl` components." 434 | error "The `#{modifier}` modifier is not yet implemented." 435 | 436 | when 'notext' 437 | unless line_type == 'template' 438 | error 'The `notext` modifier is only valid for templates.' 439 | component.notext = true 440 | 441 | when 'layer' 442 | unless line_type == 'mixin' 443 | error 'The `layer` modifier is only valid for mixins.' 444 | layer = tonumber modifiers[j] 445 | if layer == nil 446 | error "Invalid layer number: `#{modifiers[j]}`" 447 | j += 1 448 | component.interested_layers or= {} 449 | component.interested_layers[layer] = true 450 | 451 | when 'actor', 'noactor', 'sylfx', 'inlinefx' 452 | if classifier == 'once' 453 | error "The `#{modifier}` modifier is invalid for `once` components." 454 | 455 | if (modifier == 'sylfx' or modifier == 'inlinefx') and not (classifier == 'syl' or classifier == 'char') 456 | error "The `#{modifier}` modifier is only valid for `syl` and `char` components." 457 | 458 | name = modifiers[j] 459 | j += 1 460 | 461 | field = switch modifier 462 | when 'noactor' then 'disinterested_actors' 463 | when 'actor' then 'interested_actors' 464 | when 'sylfx' then 'interested_syl_fx' 465 | when 'inlinefx' then 'interested_inline_fx' 466 | 467 | component[field] or= {} 468 | component[field][name] = true 469 | 470 | when 't_actor', 'no_t_actor' 471 | unless line_type == 'mixin' 472 | error "The `#{modifier}` modifier is only valid for mixins." 473 | actor = modifiers[j] 474 | j += 1 475 | 476 | field = switch modifier 477 | when 't_actor' then 'interested_template_actors' 478 | when 'no_t_actor' then 'disinterested_template_actors' 479 | 480 | component[field] or= {} 481 | component[field][actor] = true 482 | 483 | when 'nomerge' 484 | unless line_type == 'template' 485 | error 'The `nomerge` modifier is only valid for templates.' 486 | component.merge_tags = false 487 | 488 | when 'keepspace' 489 | unless line_type == 'template' 490 | error 'The `keepspace` modifier is only valid for templates.' 491 | component.strip_trailing_space = false 492 | 493 | when 'prefix' 494 | unless line_type == 'mixin' 495 | error 'The `prefix` modifier is only valid for mixins.' 496 | component.is_prefix = true 497 | 498 | else 499 | error "Unhandled modifier: `#{modifier}`" 500 | 501 | 502 | category = components[line_type] 503 | if category == nil 504 | error "Unhandled line type: `#{line_type}`" 505 | 506 | group = category[classifier] 507 | if group == nil 508 | error "Unhandled classifier: `#{line_type} #{classifier}`" 509 | 510 | table.insert group, component 511 | 512 | aegisub.progress.set 100 * i / #subs 513 | 514 | components, interested_styles 515 | 516 | -- Delete subtitle lines generated by previous templater invocations. 517 | remove_old_output = (subs) -> 518 | is_fx = (line) -> 519 | return false unless line.class == 'dialogue' 520 | return false unless line.effect == 'fx' 521 | return false if line.comment 522 | true 523 | 524 | in_range = false 525 | ranges = {} 526 | for i, line in ipairs subs 527 | check_cancel! 528 | if is_fx line 529 | if in_range 530 | ranges[#ranges][2] = i 531 | else 532 | in_range = true 533 | table.insert ranges, {i, i} 534 | else 535 | in_range = false 536 | 537 | for i = #ranges, 1, -1 538 | check_cancel! 539 | subs.deleterange ranges[i][1], ranges[i][2] 540 | 541 | -- Collect all lyrics that are fed into templates. 542 | collect_template_input = (subs, interested_styles) -> 543 | is_kara = (line) -> 544 | return false unless line.class == 'dialogue' 545 | return false unless line.effect == 'karaoke' or line.effect == 'kara' 546 | return false unless interested_styles[line.style] 547 | true 548 | 549 | lines = {} 550 | for i, line in ipairs subs 551 | if is_kara line 552 | line.comment = true 553 | subs[i] = line 554 | line.li = i 555 | table.insert lines, line 556 | lines 557 | 558 | -- Add additional data to the syls generated by karaskel. 559 | preproc_syls = (line) -> 560 | assert line.syls == nil, 'karaskel populated line.syls (this is unexpected)' 561 | line.syls = line.kara 562 | for syl in *line.syls 563 | with syl 564 | .is_blank = (#.text_stripped == 0) 565 | .is_space = (#.text_spacestripped == 0 and not .is_blank) 566 | 567 | -- This pattern is flawed, but matches karaskel's treatment 568 | if .inline_fx != '' and .inline_fx == .text\match('%{.*\\%-([^}\\]+)') 569 | .syl_fx = .inline_fx 570 | else 571 | .syl_fx = '' 572 | 573 | -- Generate word objects resembling the syl objects karaskel makes. 574 | preproc_words = (line) -> 575 | line.words = {} 576 | current_word = {chars: {}} 577 | 578 | local seen_space, only_space 579 | seen_space = false 580 | only_space = true 581 | 582 | for char in *line.chars 583 | if char.is_space and #line.words > 0 584 | seen_space = true 585 | 586 | if seen_space and not char.is_space and not only_space 587 | table.insert line.words, current_word 588 | current_word = {chars: {}} 589 | seen_space = false 590 | only_space = true 591 | 592 | char.word = current_word 593 | table.insert current_word.chars, char 594 | 595 | if char.is_space 596 | seen_space = true 597 | else 598 | seen_space = false 599 | only_space = false 600 | 601 | if #line.chars > 0 602 | assert #current_word.chars > 0, 'there should always be a word left over when the loop ends' 603 | table.insert line.words, current_word 604 | 605 | for i, word in ipairs line.words 606 | with word 607 | -- we want spaces to be a part of the text, but not contribute to metrics 608 | .wchars = [char for char in *.chars when not char.is_space] 609 | first_char = .wchars[1] 610 | last_char = .wchars[#.wchars] 611 | 612 | .text = table.concat [char.text for char in *.chars] 613 | .text_stripped = table.concat [char.text_stripped for char in *.chars] 614 | -- being a sibling of syl, kdur might not make sense if syls span words 615 | -- .kdur = 0 616 | .line = line 617 | .i = i 618 | pre, post = .text_stripped\match("(%s*)%S+(%s*)$") 619 | .prespace = pre or '' 620 | .postspace = post or '' 621 | .text_spacestripped = .text_stripped\gsub('^[ \t]*', '')\gsub('[ \t]*$', '') 622 | .width = 0 623 | .height = 0 624 | .prespacewidth = aegisub.text_extents line.styleref, .prespace 625 | .postspacewidth = aegisub.text_extents line.styleref, .postspace 626 | .left = first_char.left 627 | .right = last_char.right 628 | .center = (.left + .right) / 2 629 | 630 | for char in *.wchars 631 | .width += char.width 632 | .height = math.max .height, char.height 633 | 634 | .is_blank = (#.text_stripped == 0) 635 | .is_space = (#.text_spacestripped == 0 and not .is_blank) 636 | 637 | -- Generate char objects resembling the syl objects karaskel creates. 638 | preproc_chars = (line) -> 639 | line.chars = {} 640 | i = 1 641 | left = 0 642 | for syl in *line.syls 643 | syl.chars = {} 644 | for ch in unicode.chars syl.text_stripped 645 | char = {:syl, :line, :i} 646 | char.text = ch 647 | char.is_space = (ch == ' ' or ch == '\t') -- matches karaskel behavior 648 | char.chars = {char} 649 | 650 | char.width, char.height, char.descent, _ = aegisub.text_extents line.styleref, ch 651 | char.left = left 652 | char.center = left + char.width/2 653 | char.right = left + char.width 654 | 655 | left += char.width 656 | 657 | table.insert syl.chars, char 658 | table.insert line.chars, char 659 | 660 | i += 1 661 | 662 | -- TODO: more karaskel-esque info for char objects 663 | 664 | -- Give all objects within a line information about their position in terms of words, syls, and chars. 665 | populate_indices = (line) -> 666 | line.wi, line.si, line.ci = 1, 1, 1 667 | 668 | wi, ci = 1, 1 669 | for word in *line.words 670 | -- TODO: figure out how to give words syl-indices? might not be reasonably possible 671 | word.wi, word.ci = wi, ci 672 | for char in *word.chars 673 | char.wi, char.ci = wi, ci 674 | ci += 1 675 | wi += 1 676 | 677 | si, ci = 1, 1 678 | for syl in *line.syls 679 | syl.wi, syl.si, syl.ci = wi, si, ci 680 | for char in *syl.chars 681 | char.si = si 682 | ci += 1 683 | si += 1 684 | 685 | -- Given a list of tables, populate the `next` and `prev` fields of each to form an ad-hoc doubly linked list. 686 | link_list = (list) -> 687 | for i = 1, #list 688 | if i > 1 689 | list[i].prev = list[i - 1] 690 | if i < #list 691 | list[i].next = list[i + 1] 692 | 693 | -- Populate lines with extra information necessary for template evaluation. 694 | -- Includes both karaskel preprocessing and some additional custom data. 695 | preproc_lines = (subs, meta, styles, lines) -> 696 | link_list lines 697 | aegisub.progress.set 0 698 | for i, line in ipairs lines 699 | check_cancel! 700 | aegisub.progress.task "Preprocessing template input: line #{i}/#{#lines}" 701 | karaskel.preproc_line subs, meta, styles, line 702 | 703 | line.is_blank = (#line.text_stripped == 0) 704 | line.is_space = (line.text_stripped\find('[^ \t]') == nil) 705 | 706 | preproc_syls line 707 | preproc_chars line 708 | preproc_words line 709 | populate_indices line 710 | 711 | link_list line.syls 712 | link_list line.chars 713 | link_list line.words 714 | 715 | aegisub.progress.set 100 * i / #lines 716 | 717 | -- Traverse a path such as foo.bar.baz within a table. 718 | traverse_path = (path, root) -> 719 | node = root 720 | for segment in path\gmatch '[^.]+' 721 | node = node[segment] 722 | break if node == nil 723 | node 724 | 725 | -- If a component has a `cond` predicate, determine whether that predicate is satisfied. 726 | eval_cond = (path, tenv) -> 727 | return true if path == nil 728 | cond = traverse_path(path, tenv) 729 | switch cond 730 | when nil then error "Condition not found: #{path}", 2 731 | when true, false then cond 732 | else not (not cond!) 733 | 734 | -- In case of multiple (space-separated) values in the actor field, check for a match with any 735 | test_multi_actor = (interested_actors, actor_field) -> 736 | for actor in actor_field\gmatch '[^ ]+' 737 | return true if interested_actors[actor] 738 | false 739 | 740 | -- Determine whether a component should be executed at all. 741 | -- If using `loop`, runs on every iteration. 742 | should_eval = (component, tenv, obj, base_component) -> 743 | if tenv.orgline != nil 744 | -- `orgline` is nil iff the component is a `once` component. 745 | -- Style filtering is irrelevant for `once` components. 746 | return false unless component.interested_styles[tenv.orgline.style] 747 | 748 | -- man this syntax looks like a mistake compared to rust's `if let Some(...) = ... {` 749 | if layers = component.interested_layers 750 | -- Only mixins can have a `layer` modifier. 751 | return false unless layers[tenv.line.layer] 752 | 753 | if actors = component.interested_actors 754 | -- Actor filtering is irrelevant for `once` components. 755 | return false unless test_multi_actor actors, tenv.orgline.actor 756 | 757 | if actors = component.disinterested_actors 758 | return false if test_multi_actor actors, tenv.orgline.actor 759 | 760 | if actors = component.interested_template_actors 761 | -- Only mixins can have a `t_actor` modifier. 762 | return false unless test_multi_actor actors, base_component.template_actor 763 | 764 | if actors = component.disinterested_template_actors 765 | return false if test_multi_actor actors, base_component.template_actor 766 | 767 | if fxs = component.interested_syl_fx 768 | syl = tenv.syl or tenv.char.syl 769 | return false unless fxs[syl.syl_fx] 770 | 771 | if fxs = component.interested_inline_fx 772 | syl = tenv.syl or tenv.char.syl 773 | return false unless fxs[syl.inline_fx] 774 | 775 | if component.noblank 776 | -- `obj` is nil iff the component is a `once` component. 777 | -- No-blank filtering is irrelevant for `once` components. 778 | return false if obj.is_blank or obj.is_space 779 | 780 | if component.nok0 781 | -- syl objects have direct access to their duration 782 | -- char objects need to fetch it from their containing syl 783 | -- zero-length filtering is irrelevant for line, word, and once components 784 | return false if (obj.duration or obj.syl.duration) <= 0 785 | 786 | cond_val = eval_cond component.condition, tenv 787 | if component.cond_is_negated 788 | return false if cond_val 789 | else 790 | return false unless cond_val 791 | 792 | true 793 | 794 | -- Evaluate a dollar-variable. 795 | eval_inline_var = (tenv) -> (var) -> 796 | local syl 797 | if tenv.syl 798 | syl = tenv.syl 799 | elseif tenv.char 800 | syl = tenv.char.syl 801 | 802 | strip_prefix = (str, prefix) -> 803 | len = prefix\len! 804 | if str\sub(1, len) == prefix then str\sub(len + 1) else nil 805 | 806 | val = switch var 807 | when '$sylstart' then syl.start_time 808 | when '$sylend' then syl.end_time 809 | when '$syldur' then syl.duration 810 | when '$kdur', '$sylkdur' then syl.duration / 10 811 | when '$ldur' then tenv.orgline.duration 812 | when '$li' then tenv.orgline.li 813 | when '$si' then (tenv.syl or tenv.char or tenv.orgline).si 814 | when '$wi' then (tenv.word or tenv.char or tenv.orgline).wi 815 | when '$ci' then (tenv.char or tenv.syl or tenv.word or tenv.orgline).ci 816 | when '$cxf' then tenv.util.xf(tenv.char, tenv.orgline.chars) 817 | when '$sxf' then tenv.util.xf(tenv.syl or tenv.char.syl, tenv.orgline.syls) 818 | when '$wxf' then tenv.util.xf(tenv.word or tenv.char.word, tenv.orgline.words) 819 | else 820 | if name = strip_prefix var, '$loop_' 821 | tenv.loopctx.state[name] or 1 822 | elseif name = strip_prefix var, '$maxloop_' 823 | tenv.loopctx.max[name] or 1 824 | elseif name = strip_prefix var, '$mloop_' 825 | tenv.mloopctx.state[name] or 1 826 | elseif name = strip_prefix var, '$maxmloop_' 827 | tenv.mloopctx.max[name] or 1 828 | elseif name = strip_prefix var, '$env_' 829 | tenv[name] -- this turned out to be less useful than I expected 830 | else 831 | error "Unrecognized inline variable: #{var}" 832 | 833 | tostring val 834 | 835 | -- Evaluate an inline Lua expression. 836 | eval_inline_expr = (tenv) -> (expr) -> 837 | cache = tenv.__private.compilation_cache 838 | func = cache[expr] 839 | if func == nil 840 | actual_expr = expr\sub 2, -2 -- remove the `!`s 841 | func_body = "return (#{actual_expr});" 842 | func, err = load func_body, "inline expression `#{func_body}`", 't', tenv 843 | if err != nil 844 | aegisub.log 0, "Syntax error in inline expression `#{func_body}`: #{err}" 845 | aegisub.cancel! 846 | 847 | cache[expr] = func 848 | 849 | val = func! 850 | if val == nil then '' else tostring(val) 851 | 852 | -- Expand dollar-variables and inline Lua expressions within a template or mixin. 853 | eval_body = (text, tenv) -> 854 | text\gsub('%$[a-z_]+', eval_inline_var tenv)\gsub('!.-!', eval_inline_expr tenv) 855 | 856 | -- A collection of variables to iterate over in a particular order. 857 | class loopctx 858 | new: (component) => 859 | @vars = [var for var in *component.repetition_order] 860 | @state = {var, 1 for var in *@vars} 861 | @max = {var, max for var, max in pairs component.repetitions} 862 | @done = false 863 | 864 | incr: => 865 | if #@vars == 0 866 | @done = true 867 | return 868 | 869 | @state[@vars[1]] += 1 870 | for i, var in ipairs @vars 871 | next_var = @vars[i + 1] 872 | if @state[var] > @max[var] 873 | if next_var != nil 874 | @state[var] = 1 875 | @state[next_var] += 1 876 | else 877 | @done = true 878 | return 879 | 880 | -- Given a map of char indices to prepended text, evaluate the each mixin's body and insert its text at the appropriate index. 881 | apply_mixins = (template, mixins, objs, tenv, tags, cls) -> 882 | for obj in *objs 883 | did_insert = false 884 | if tenv[cls] == nil 885 | tenv[cls] = obj 886 | did_insert = true 887 | 888 | for mixin in *mixins 889 | tenv.mloopctx = loopctx mixin 890 | while not tenv.mloopctx.done 891 | check_cancel! 892 | if should_eval mixin, tenv, obj, template 893 | ci = if (cls == 'line' or mixin.is_prefix) then 0 else obj.ci 894 | tags[ci] or= {} 895 | 896 | mskipped = false 897 | tenv.mskip = (using mskipped) -> mskipped = true 898 | tenv.unmskip = (using mskipped) -> mskipped = false 899 | 900 | tag = eval_body mixin.text, tenv 901 | 902 | unless mskipped 903 | table.insert tags[ci], tag 904 | 905 | tenv.mloopctx\incr! 906 | tenv.mloopctx = nil 907 | 908 | if did_insert 909 | tenv[cls] = nil 910 | 911 | -- Combine the prefix generated from the `template` with the results of `apply_mixins` and the line's text itself. 912 | build_text = (prefix, chars, tags, template) -> 913 | segments = {prefix} 914 | if tags[0] 915 | table.insert segments, tag for tag in *tags[0] 916 | for char in *chars 917 | if tags[char.ci] != nil 918 | table.insert segments, tag for tag in *tags[char.ci] 919 | unless template.notext 920 | table.insert segments, char.text 921 | 922 | table.concat segments 923 | 924 | -- Where the magic happens. Run code, run templates, run components. 925 | apply_templates = (subs, lines, components, tenv) -> 926 | run_code = (cls, orgobj) -> 927 | for code in *components.code[cls] 928 | tenv.loopctx = loopctx code 929 | while not tenv.loopctx.done 930 | check_cancel! 931 | if should_eval code, tenv, orgobj 932 | code.func! 933 | tenv.loopctx\incr! 934 | tenv.loopctx = nil 935 | 936 | run_mixins = (classes, template) -> 937 | tags = {} 938 | for cls in *classes 939 | mixins = components.mixin[cls] 940 | objs = if cls == 'line' then {tenv.line} else tenv.line[cls .. 's'] 941 | apply_mixins template, mixins, objs, tenv, tags, cls 942 | tags 943 | 944 | run_templates = (cls, orgobj) -> 945 | for template in *components.template[cls] 946 | tenv.template_actor = template.template_actor 947 | tenv.loopctx = loopctx template 948 | while not tenv.loopctx.done 949 | check_cancel! 950 | if should_eval template, tenv, orgobj 951 | with tenv.line = table.copy tenv.orgline 952 | .comment = false 953 | .effect = 'fx' 954 | .layer = template.layer 955 | -- TODO: all this mutable access to the original is super sketchy. do something about it? 956 | .chars = orgobj.chars 957 | .words, .syls = switch cls 958 | when 'line' then .words, .syls 959 | when 'word' then {orgobj}, nil 960 | when 'syl' then nil, {orgobj} 961 | when 'char' then nil, nil 962 | 963 | -- I have no idea what I'm doing. 964 | --ci_offset = orgobj.chars[1].ci - 1 965 | --char.i -= ci_offset for char in *.chars 966 | 967 | --if .syls 968 | -- si_offset = .syls[1].si - 1 969 | -- syl.i -= si_offset for syl in *.syls 970 | 971 | --if .words 972 | -- wi_offset = .words[1].wi - 1 973 | -- word.i -= wi_offset for word in *.words 974 | 975 | skipped = false 976 | tenv.skip = (using skipped) -> skipped = true 977 | tenv.unskip = (using skipped) -> skipped = false 978 | 979 | prefix = eval_body template.text, tenv 980 | mixin_classes = switch cls 981 | when 'line' then {'line', 'word', 'syl', 'char'} 982 | when 'word' then {'line', 'word', 'char'} 983 | when 'syl' then {'line', 'syl', 'char'} 984 | when 'char' then {'line', 'char'} 985 | 986 | tags = run_mixins mixin_classes, template 987 | tenv.line.text = build_text prefix, tenv.line.chars, tags, template 988 | 989 | if template.merge_tags 990 | -- A primitive way of doing this. Patches welcome. 991 | -- Otherwise, if you're doing something fancy enough that this breaks it and `nomerge` isn't acceptable, you're on your own. 992 | tenv.line.text = tenv.line.text\gsub '}{', '' 993 | 994 | if template.strip_trailing_space 995 | -- Less primitive than the above thing, but still primitive. Might have worst-case quadratic performance. 996 | tenv.line.text = tenv.line.text\gsub ' *$', '' 997 | 998 | unless skipped 999 | subs.append tenv.line 1000 | 1001 | tenv.skip = nil 1002 | tenv.unskip = nil 1003 | 1004 | tenv.line = nil 1005 | tenv.loopctx\incr! 1006 | tenv.loopctx = nil 1007 | 1008 | run_code 'once' 1009 | 1010 | aegisub.progress.set 0 1011 | 1012 | for i, orgline in ipairs lines 1013 | aegisub.progress.task "Applying templates: line #{i}/#{#lines}" 1014 | 1015 | tenv.orgline = orgline 1016 | run_code 'line', orgline 1017 | run_templates 'line', orgline 1018 | 1019 | for orgword in *orgline.words 1020 | tenv.word = orgword 1021 | run_code 'word', orgword 1022 | run_templates 'word', orgword 1023 | tenv.word = nil 1024 | 1025 | for orgsyl in *orgline.syls 1026 | tenv.syl = orgsyl 1027 | -- TODO: `multi` support 1028 | run_code 'syl', orgsyl 1029 | run_templates 'syl', orgsyl 1030 | tenv.syl = nil 1031 | 1032 | for orgchar in *orgline.chars 1033 | tenv.char = orgchar 1034 | run_code 'char', orgchar 1035 | run_templates 'char', orgchar 1036 | tenv.char = nil 1037 | 1038 | tenv.orgline = nil 1039 | aegisub.progress.set 100 * i / #lines 1040 | 1041 | -- Entry point 1042 | main = (subs, sel, active) -> 1043 | math.randomseed os.time! 1044 | 1045 | task = aegisub.progress.task 1046 | 1047 | task 'Collecting header data...' 1048 | meta, styles = karaskel.collect_head subs, false 1049 | 1050 | tenv = template_env subs, meta, styles 1051 | 1052 | check_cancel! 1053 | task 'Parsing templates...' 1054 | components, interested_styles = parse_templates subs, tenv 1055 | 1056 | check_cancel! 1057 | task 'Removing old template output...' 1058 | remove_old_output subs 1059 | 1060 | check_cancel! 1061 | task 'Collecting template input...' 1062 | lines = collect_template_input subs, interested_styles 1063 | 1064 | check_cancel! 1065 | task 'Preprocessing template input...' 1066 | preproc_lines subs, meta, styles, lines 1067 | 1068 | task 'Applying templates...' 1069 | apply_templates subs, lines, components, tenv 1070 | 1071 | aegisub.set_undo_point 'apply karaoke template' 1072 | 1073 | remove_fx_main = (subs, _sel, _active) -> 1074 | remove_old_output subs 1075 | aegisub.set_undo_point 'remove generated fx' 1076 | 1077 | can_apply = (subs, _sel, _active) -> 1078 | for line in *subs 1079 | if line.comment == true and line.class == 'dialogue' 1080 | effect = line.effect 1081 | if #effect >= #'code syl' 1082 | word = effect\match('(%l+) ') 1083 | -- can't have a template that's just mixins 1084 | if word == 'code' or word == 'template' 1085 | return true 1086 | return false 1087 | 1088 | can_remove = (subs, _sel, _active) -> 1089 | for line in *subs 1090 | if line.effect == 'fx' and line.comment == false and line.class == 'dialogue' 1091 | return true 1092 | return false 1093 | 1094 | aegisub.register_macro script_name, 'Run the templater', main, can_apply 1095 | aegisub.register_macro 'Remove generated fx', 'Remove non-commented lines whose Effect field is `fx`', remove_fx_main, can_remove 1096 | 1097 | print_stacktrace = -> 1098 | moon = 1099 | errors: require 'moonscript.errors' 1100 | posmaps: require 'moonscript.line_tables' 1101 | 1102 | cache = {} 1103 | 1104 | aegisub.log 'Stack trace:\n' 1105 | 1106 | current_source = nil 1107 | 1108 | for i = 1, 30 1109 | info = debug.getinfo i, 'fnlS' 1110 | break unless info 1111 | 1112 | with info 1113 | posmap = moon.posmaps[.source] 1114 | if .name == nil 1115 | if .func == main 1116 | .name = 'main' 1117 | else 1118 | .name = '' 1119 | 1120 | .currentline = moon.errors.reverse_line_number .source, posmap, .currentline, cache 1121 | .linedefined = moon.errors.reverse_line_number .source, posmap, .linedefined, cache 1122 | 1123 | if .source != current_source 1124 | current_source = .source 1125 | short_path = .source\gsub '.*automation', 'automation' 1126 | aegisub.log " #{short_path}\n" 1127 | 1128 | aegisub.log "\tline #{.currentline} \t(in function #{.name}, defined on line #{.linedefined})\n" 1129 | 1130 | aegisub.log '\n' 1131 | --------------------------------------------------------------------------------