branch: elpa/subed commit 6eba992e13f2204b2f992b5227ef77fcb43dd8d0 Author: Troy Brown <brow...@users.noreply.github.com> Commit: Troy Brown <brow...@users.noreply.github.com>
Add ability to proportionally scale subtitles. --- README.org | 11 ++ subed/subed-common.el | 136 ++++++++++++++ subed/subed.el | 2 + tests/test-subed-common.el | 443 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 592 insertions(+) diff --git a/README.org b/README.org index fe471d2..edd222b 100644 --- a/README.org +++ b/README.org @@ -30,6 +30,17 @@ SubRip ( ~.srt~) and WebVTT ( ~.vtt~ ). - Shift the current subtitle forward (~C-M-f~) or backward (~C-M-b~) together with all following subtitles. This is basically a convenience shortcut for ~C-SPC M-> C-M-n/p~. + - Scale all subtitles or all marked subtitles forward (~C-M-x~) or backward + (~C-M-S-x~) in time without changing subtitle duration. A prefix argument + sets the number of milliseconds for the current session (e.g. ~C-u 500 + C-M-x~ moves the last [or last marked] subtitle forward 500ms and + proportionally scales all [or all marked] subtitles based on this time + extension. Similarly, ~C-u 500 C-M-S-x~ moves the last [or last marked] + subtitle backward 500ms and proportionally scales all [or all marked] + subtitles based on this time contraction). This can be extremely useful to + correct synchronization issues in existing subtitle files. First, adjust + the starting time if necessary (e.g. ~C-M-f~), then adjust the ending and + scale constituent subtitles (e.g. ~C-M-x~). - Show CPS (characters per second) for the current subtitle. - Insert HTML-like tags (~C-c C-t C-t~, with an optional attribute when prefixed by ~C-u~), in particular italics (~C-c C-t C-i~) or diff --git a/subed/subed-common.el b/subed/subed-common.el index 91a9cdd..ebe6795 100644 --- a/subed/subed-common.el +++ b/subed/subed-common.el @@ -27,6 +27,7 @@ ;;; Code: +(require 'cl-macs) (require 'subed-config) (require 'subed-debug) (require 'subed-mpv) @@ -339,6 +340,88 @@ but we move the start time first." (cl-flet ((move-subtitle (subed--get-move-subtitle-func msecs))) (move-subtitle msecs))))) +(defun subed--scale-subtitles-in-region (msecs beg end) + "Scale subtitles in region specified by BEG and END after moving END MSECS milliseconds." + (let* ((beg-point (save-excursion ; normalized to fixed location over BEG + (goto-char beg) + (subed-jump-to-subtitle-end) + (point))) + (beg-next-point (save-excursion + (goto-char beg-point) + (subed-forward-subtitle-end) + (point))) + (end-point (save-excursion ; normalized to fixed location over END + (goto-char end) + (subed-jump-to-subtitle-end) + (point))) + (end-prev-point (save-excursion + (goto-char end-point) + (subed-backward-subtitle-end) + (point))) + (beg-start-msecs (save-excursion + (goto-char beg-point) + (subed-subtitle-msecs-start))) + (old-end-start-msecs (save-excursion + (goto-char end-point) + (subed-subtitle-msecs-start)))) + ;; check for improper range (BEG after END) + (unless (<= beg end) + (user-error "Can't scale with improper range")) + ;; check for 0 or 1 subtitle scenario + (unless (/= beg-point end-point) + (user-error "Can't scale with fewer than 3 subtitles")) + ;; check for 2 subtitle scenario + (unless (/= beg-point end-prev-point) + (user-error "Can't scale with only 2 subtitles")) + ;; check for missing timestamps + (unless beg-start-msecs + (user-error "Can't scale when first subtitle timestamp missing")) + (unless old-end-start-msecs + (user-error "Can't scale when last subtitle timestamp missing")) + ;; check for range with 0 time interval + (unless (/= beg-start-msecs old-end-start-msecs) + (user-error "Can't scale subtitle range with 0 time interval")) + + (unless (= msecs 0) + (subed-with-subtitle-replay-disabled + (cl-flet ((move-subtitle (subed--get-move-subtitle-func msecs))) + (let* ((new-end-start-msecs (+ old-end-start-msecs msecs)) + (scale-factor (/ (float (- new-end-start-msecs beg-start-msecs)) + (float (- old-end-start-msecs beg-start-msecs)))) + (scale-subtitles + (lambda (&optional reverse) + (subed-for-each-subtitle beg-next-point end-prev-point reverse + (let ((old-start-msecs (subed-subtitle-msecs-start))) + (unless old-start-msecs + (user-error "Can't scale when subtitle timestamp missing")) + (let* ((new-start-msecs + (+ beg-start-msecs + (round (* (- old-start-msecs beg-start-msecs) scale-factor)))) + (delta-msecs (- new-start-msecs old-start-msecs))) + (unless (and (<= beg-start-msecs old-start-msecs) + (>= old-end-start-msecs old-start-msecs)) + (user-error "Can't scale when nonchronological subtitles exist")) + (move-subtitle delta-msecs :ignore-negative-duration))))))) + (atomic-change-group + (if (> msecs 0) + (save-excursion + ;; Moving forward - Start on last subtitle to see if we + ;; can move forward. + (goto-char end) + (let ((adjusted-msecs (move-subtitle msecs))) + (unless (and adjusted-msecs + (= msecs adjusted-msecs)) + (user-error "Can't scale when extension would overlap subsequent subtitles"))) + (funcall scale-subtitles :reverse)) + (save-excursion + ;; Moving backward - Make sure the last subtitle will not + ;; precede the first subtitle. + (unless (> new-end-start-msecs beg-start-msecs) + (user-error "Can't scale when contraction would eliminate region")) + (goto-char end) + (move-subtitle msecs :ignore-negative-duration) + (funcall scale-subtitles)))))))))) + (defun subed--move-subtitles-in-region (msecs beg end) "Move subtitles in region specified by BEG and END by MSECS milliseconds." (unless (= msecs 0) @@ -374,6 +457,59 @@ but we move the start time first." (subed-for-each-subtitle (point) end nil (move-subtitle msecs :ignore-negative-duration))))))))) +(defun subed-scale-subtitles (msecs &optional beg end) + "Scale subtitles between BEG and END after moving END MSECS. +Use a negative MSECS value to move END backward. +If END is nil, END will be the last subtitle in the buffer. +If BEG is nil, BEG will be the first subtitle in the buffer." + (let ((beg (or beg (point-min))) + (end (or end (point-max)))) + (subed--scale-subtitles-in-region msecs beg end) + (when (subed-replay-adjusted-subtitle-p) + (save-excursion + (goto-char end) + (subed-jump-to-subtitle-id) + (subed-mpv-jump (subed-subtitle-msecs-start)))))) + +(defun subed-scale-subtitles-forward (&optional arg) + "Scale subtitles after region is extended `subed-milliseconds-adjust'. + +Scaling adjusts start and stop by the same amount, preserving +subtitle duration. + +All subtitles that are fully or partially in the active region +are moved so they are placed proportionally in the new range. + +If prefix argument ARG is given, it is used to extend the end of the region +`subed-milliseconds-adjust' before proportionally adjusting subtitles. If the +prefix argument is given but not numerical, +`subed-milliseconds-adjust' is reset to its default value. + +Example usage: + \\[universal-argument] 1000 \\[subed-scale-subtitles-forward] Extend region 1000ms forward in time and scale subtitles in region + \\[subed-scale-subtitles-forward] Extend region another 1000ms forward in time and scale subtitles again + \\[universal-argument] 500 \\[subed-scale-subtitles-forward] Extend region 500ms forward in time and scale subtitles in region + \\[subed-scale-subtitles-forward] Extend region another 500ms forward in time and scale subtitles again + \\[universal-argument] \\[subed-scale-subtitles-forward] Extend region 100ms (the default) forward in time and scale subtitles in region + \\[subed-scale-subtitles-forward] Extend region another 100ms (the default) forward in time and scale subtitles again" + (interactive "P") + (let ((deactivate-mark nil) + (msecs (subed-get-milliseconds-adjust arg)) + (beg (when mark-active (region-beginning))) + (end (when mark-active (region-end)))) + (subed-scale-subtitles msecs beg end))) + +(defun subed-scale-subtitles-backward (&optional arg) + "Scale subtitles after region is shortened `subed-milliseconds-adjust'. + +See `subed-scale-subtitles-forward' about ARG." + (interactive "P") + (let ((deactivate-mark nil) + (msecs (* -1 (subed-get-milliseconds-adjust arg))) + (beg (when mark-active (region-beginning))) + (end (when mark-active (region-end)))) + (subed-scale-subtitles msecs beg end))) + (defun subed-move-subtitles (msecs &optional beg end) "Move subtitles between BEG and END MSECS milliseconds forward. Use a negative MSECS value to move subtitles backward. diff --git a/subed/subed.el b/subed/subed.el index ee4edbc..e148fe7 100644 --- a/subed/subed.el +++ b/subed/subed.el @@ -60,6 +60,8 @@ (define-key subed-mode-map (kbd "C-M-p") #'subed-move-subtitle-backward) (define-key subed-mode-map (kbd "C-M-f") #'subed-shift-subtitle-forward) (define-key subed-mode-map (kbd "C-M-b") #'subed-shift-subtitle-backward) + (define-key subed-mode-map (kbd "C-M-x") #'subed-scale-subtitles-forward) + (define-key subed-mode-map (kbd "C-M-S-x") #'subed-scale-subtitles-backward) (define-key subed-mode-map (kbd "M-i") #'subed-insert-subtitle) (define-key subed-mode-map (kbd "C-M-i") #'subed-insert-subtitle-adjacent) (define-key subed-mode-map (kbd "M-k") #'subed-kill-subtitle) diff --git a/tests/test-subed-common.el b/tests/test-subed-common.el index efe38c4..f0f1490 100644 --- a/tests/test-subed-common.el +++ b/tests/test-subed-common.el @@ -2423,3 +2423,446 @@ This is another. (subed-backward-subtitle-time-start) (expect (subed-subtitle-msecs-stop) :to-equal 63061) (expect (subed-subtitle-text) :to-equal "Foo."))))) + +(describe "Scaling subtitles" + (it "without providing beginning and end." + (with-temp-srt-buffer + (insert mock-srt-data) + (spy-on 'subed-scale-subtitles :and-call-through) + (subed-scale-subtitles-forward 1000) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122734) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130845) + (expect (subed-subtitle-msecs-start 3) :to-equal 184450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 196500) + (subed-scale-subtitles-backward 1000) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122234) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130345) + (expect (subed-subtitle-msecs-start 3) :to-equal 183450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 195500) + (expect (spy-calls-all-args 'subed-scale-subtitles) :to-equal + '((+1000 nil nil) + (-1000 nil nil))))) + (it "without providing end." + (with-temp-srt-buffer + (insert mock-srt-data) + (subed-scale-subtitles 1000 (point-min) nil) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122734) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130845) + (expect (subed-subtitle-msecs-start 3) :to-equal 184450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 196500) + (subed-scale-subtitles -1000 (point-min) nil) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122234) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130345) + (expect (subed-subtitle-msecs-start 3) :to-equal 183450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 195500))) + (it "without providing beginning." + (with-temp-srt-buffer + (insert mock-srt-data) + (subed-scale-subtitles 1000 nil (point-max)) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122734) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130845) + (expect (subed-subtitle-msecs-start 3) :to-equal 184450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 196500) + (subed-scale-subtitles -1000 nil (point-max)) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122234) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130345) + (expect (subed-subtitle-msecs-start 3) :to-equal 183450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 195500))) + (it "with active region on entire buffer." + (with-temp-srt-buffer + (insert mock-srt-data) + (let ((beg (point-min)) + (end (point-max))) + (spy-on 'subed-scale-subtitles :and-call-through) + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (setq mark-active t) + (subed-scale-subtitles-forward 1000) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122734) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130845) + (expect (subed-subtitle-msecs-start 3) :to-equal 184450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 196500) + (subed-scale-subtitles-backward 1000) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122234) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130345) + (expect (subed-subtitle-msecs-start 3) :to-equal 183450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 195500) + (expect (spy-calls-all-args 'subed-scale-subtitles) :to-equal + `((+1000 ,beg ,end) + (-1000 ,beg ,end)))))) + (it "with a zero msec extension/contraction." + (with-temp-srt-buffer + (insert mock-srt-data) + (subed-scale-subtitles-forward 0) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122234) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130345) + (expect (subed-subtitle-msecs-start 3) :to-equal 183450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 195500) + (subed-scale-subtitles-backward 0) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 122234) + (expect (subed-subtitle-msecs-stop 2) :to-equal 130345) + (expect (subed-subtitle-msecs-start 3) :to-equal 183450) + (expect (subed-subtitle-msecs-stop 3) :to-equal 195500))) + (it "with active region on one subtitle." + (with-temp-srt-buffer + (insert mock-srt-data) + (let ((beg 77) ; point at ID of third subtitle + (end (point-max))) + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (spy-on 'user-error :and-call-through) + (setq mark-active t) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with fewer than 3 subtitles"))) + (expect (buffer-string) :to-equal mock-srt-data)))) + (it "with active region on two subtitles." + (with-temp-srt-buffer + (insert mock-srt-data) + (let ((beg 39) ; point at ID of second subtitle + (end (point-max))) + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (spy-on 'user-error :and-call-through) + (setq mark-active t) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with only 2 subtitles"))) + (expect (buffer-string) :to-equal mock-srt-data)))) + (it "with active region contraction." + (with-temp-srt-buffer + (insert (concat "1\n" + "00:00:43,233 --> 00:00:45,861\n" + "a\n" + "\n" + "2\n" + "00:00:51,675 --> 00:00:54,542\n" + "b\n" + "\n" + "3\n" + "00:01:00,717 --> 00:01:02,378\n" + "c\n" + "\n" + "4\n" + "00:01:02,452 --> 00:01:05,216\n" + "d\n")) + (let ((beg (point-min)) + (end 103)) ; point at TEXT of third subtitle + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (setq mark-active t) + (subed-scale-subtitles-backward 1000) + (expect (subed-subtitle-msecs-start 1) :to-equal 43233) + (expect (subed-subtitle-msecs-stop 1) :to-equal 45861) + (expect (subed-subtitle-msecs-start 2) :to-equal 51192) + (expect (subed-subtitle-msecs-stop 2) :to-equal 54059) + (expect (subed-subtitle-msecs-start 3) :to-equal 59717) + (expect (subed-subtitle-msecs-stop 3) :to-equal 61378) + (expect (subed-subtitle-msecs-start 4) :to-equal 62452) + (expect (subed-subtitle-msecs-stop 4) :to-equal 65216)))) + (it "with active region extension." + (with-temp-srt-buffer + (insert (concat "1\n" + "00:00:43,233 --> 00:00:45,861\n" + "a\n" + "\n" + "2\n" + "00:00:51,192 --> 00:00:54,059\n" + "b\n" + "\n" + "3\n" + "00:00:59,717 --> 00:01:01,378\n" + "c\n" + "\n" + "4\n" + "00:01:02,452 --> 00:01:05,216\n" + "d\n")) + (let ((beg (point-min)) + (end 103)) ; point at TEXT of third subtitle + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (setq mark-active t) + (setq-local subed-subtitle-spacing 0) + (subed-scale-subtitles-forward 1000) + (expect (subed-subtitle-msecs-start 1) :to-equal 43233) + (expect (subed-subtitle-msecs-stop 1) :to-equal 45861) + (expect (subed-subtitle-msecs-start 2) :to-equal 51675) + (expect (subed-subtitle-msecs-stop 2) :to-equal 54542) + (expect (subed-subtitle-msecs-start 3) :to-equal 60717) + (expect (subed-subtitle-msecs-stop 3) :to-equal 62378) + (expect (subed-subtitle-msecs-start 4) :to-equal 62452) + (expect (subed-subtitle-msecs-stop 4) :to-equal 65216)))) + (it "when active region extension overlaps next subtitle." + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:00:43,233 --> 00:00:45,861\n" + "a\n" + "\n" + "2\n" + "00:00:51,675 --> 00:00:54,542\n" + "b\n" + "\n" + "3\n" + "00:01:00,717 --> 00:01:02,378\n" + "c\n" + "\n" + "4\n" + "00:01:02,452 --> 00:01:05,216\n" + "d\n")) + (beg 1) + (end 103)) ; point at TEXT of third subtitle + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (spy-on 'user-error :and-call-through) + (insert initial-contents) + (setq mark-active t) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when extension would overlap subsequent subtitles"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "when end subtitle start time moved to same time as begin subtitle start time." + (with-temp-srt-buffer + (insert mock-srt-data) + (let ((beg (point-min)) + (end (point-max)) + (delta (- (subed-subtitle-msecs-start 3) + (subed-subtitle-msecs-start 1)))) + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (spy-on 'user-error :and-call-through) + (setq mark-active t) + (expect (subed-scale-subtitles-backward delta) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when contraction would eliminate region"))) + (expect (buffer-string) :to-equal mock-srt-data)))) + (it "when end subtitle start time moved to just before begin subtitle start time." + (with-temp-srt-buffer + (insert mock-srt-data) + (let ((beg (point-min)) + (end (point-max)) + (delta (- (subed-subtitle-msecs-start 3) + (subed-subtitle-msecs-start 1)))) + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (spy-on 'user-error :and-call-through) + (setq mark-active t) + (expect (subed-scale-subtitles-backward (+ delta 1)) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when contraction would eliminate region"))) + (expect (buffer-string) :to-equal mock-srt-data)))) + (it "when end subtitle start time moved to just after begin subtitle start time." + (with-temp-srt-buffer + (insert mock-srt-data) + (let ((beg (point-min)) + (end (point-max)) + (delta (- (subed-subtitle-msecs-start 3) + (subed-subtitle-msecs-start 1)))) + (spy-on 'region-beginning :and-return-value beg) + (spy-on 'region-end :and-return-value end) + (setq mark-active t) + (subed-scale-subtitles-backward (- delta 1)) + (expect (subed-subtitle-msecs-start 1) :to-equal 61000) + (expect (subed-subtitle-msecs-stop 1) :to-equal 65123) + (expect (subed-subtitle-msecs-start 2) :to-equal 61001) + (expect (subed-subtitle-msecs-stop 2) :to-equal 69112) + (expect (subed-subtitle-msecs-start 3) :to-equal 61001) + (expect (subed-subtitle-msecs-stop 3) :to-equal 73051)))) + (it "when begin start time same as end start time." + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:01:01,000 --> 00:01:05,123\n" + "Foo.\n" + "\n" + "2\n" + "00:01:01,000 --> 00:01:05,123\n" + "Bar.\n" + "\n" + "3\n" + "00:01:01,000 --> 00:01:05,123\n" + "Baz.\n"))) + (spy-on 'user-error :and-call-through) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale subtitle range with 0 time interval"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale subtitle range with 0 time interval"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "when buffer is empty." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with fewer than 3 subtitles"))) + (expect (buffer-string) :to-equal "") + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with fewer than 3 subtitles"))) + (expect (buffer-string) :to-equal ""))) + (it "when buffer contains one subtitle." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:01:01,000 --> 00:01:05,123\n" + "Foo.\n"))) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with fewer than 3 subtitles"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with fewer than 3 subtitles"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "when buffer contains two subtitles." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:01:01,000 --> 00:01:05,123\n" + "Foo.\n" + "\n" + "2\n" + "00:02:02,234 --> 00:02:10,345\n" + "Bar.\n"))) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with only 2 subtitles"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with only 2 subtitles"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "with subtitle in region containing start time after end start time." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:01:01,000 --> 00:01:05,123\n" + "Foo.\n" + "\n" + "2\n" + "00:03:03,45 --> 00:03:15,5\n" + "Baz.\n" + "\n" + "3\n" + "00:02:02,234 --> 00:02:10,345\n" + "Bar.\n"))) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when nonchronological subtitles exist"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when nonchronological subtitles exist"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "with first subtitle containing no timestamp." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "a\n" + "\n" + "2\n" + "00:00:51,675 --> 00:00:54,542\n" + "b\n" + "\n" + "3\n" + "00:01:00,717 --> 00:01:02,378\n" + "c\n"))) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when first subtitle timestamp missing"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when first subtitle timestamp missing"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "with last subtitle containing no timestamp." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:00:43,233 --> 00:00:45,861\n" + "a\n" + "\n" + "2\n" + "00:00:51,675 --> 00:00:54,542\n" + "b\n" + "\n" + "3\n" + "c\n"))) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when last subtitle timestamp missing"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when last subtitle timestamp missing"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "with subtitle in region containing no timestamp." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (let ((initial-contents + (concat "1\n" + "00:00:43,233 --> 00:00:45,861\n" + "a\n" + "\n" + "2\n" + "b\n" + "\n" + "3\n" + "00:01:00,717 --> 00:01:02,378\n" + "c\n"))) + (insert initial-contents) + (expect (subed-scale-subtitles-forward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when subtitle timestamp missing"))) + (expect (buffer-string) :to-equal initial-contents) + (spy-calls-reset 'user-error) + (expect (subed-scale-subtitles-backward 1000) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale when subtitle timestamp missing"))) + (expect (buffer-string) :to-equal initial-contents)))) + (it "with out-of-order range." + (spy-on 'user-error :and-call-through) + (with-temp-srt-buffer + (expect (subed-scale-subtitles 1000 5 4) :to-throw 'error) + (expect (spy-calls-all-args 'user-error) :to-equal + '(("Can't scale with improper range"))))))