emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[nongnu] elpa/subed d0dfa1a 389/389: Preliminary support for .ass files


From: ELPA Syncer
Subject: [nongnu] elpa/subed d0dfa1a 389/389: Preliminary support for .ass files (Advanced SubStation Alpha)
Date: Fri, 3 Dec 2021 11:01:04 -0500 (EST)

branch: elpa/subed
commit d0dfa1a2ee7d09a12763bbb8308ef2cdd875c504
Author: Sacha Chua <sacha@sachachua.com>
Commit: Sacha Chua <sacha@sachachua.com>

    Preliminary support for .ass files (Advanced SubStation Alpha)
---
 README.org                      |   2 +-
 subed/subed-ass.el              | 461 ++++++++++++++++++++++++++++++++++++++++
 subed/subed.el                  |   4 +-
 tests/test-subed-ass.el         | 391 ++++++++++++++++++++++++++++++++++
 tests/test-subed-ass.el.license |   3 +
 tests/test-subed-vtt.el         |   2 +-
 6 files changed, 860 insertions(+), 3 deletions(-)

diff --git a/README.org b/README.org
index edd222b..47b40e1 100644
--- a/README.org
+++ b/README.org
@@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 * subed
 subed is an Emacs major mode for editing subtitles while playing the
 corresponding video with [[https://mpv.io/][mpv]].  At the moment, the only 
supported formats are
-SubRip ( ~.srt~) and WebVTT ( ~.vtt~ ).
+SubRip ( ~.srt~), WebVTT ( ~.vtt~ ), and Advanced SubStation Alpha ( ~.ass~, 
experimental ).
 
 [[file:https://raw.githubusercontent.com/rndusr/subed/master/screenshot.jpg]]
 
diff --git a/subed/subed-ass.el b/subed/subed-ass.el
new file mode 100644
index 0000000..b2b0d93
--- /dev/null
+++ b/subed/subed-ass.el
@@ -0,0 +1,461 @@
+;;; subed-ass.el --- Advanced SubStation Alpha implementation for subed  -*- 
lexical-binding: t; -*-
+
+;;; License:
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; This is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING.  If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+
+;;; Commentary:
+
+;; Advanced SubStation Alpha implementation for subed-mode.
+;; Since ASS doesn't use IDs, we'll use the starting timestamp.
+
+;;; Code:
+
+(require 'subed-config)
+(require 'subed-debug)
+(require 'subed-common)
+
+;;; Syntax highlighting
+
+(defconst subed-ass-font-lock-keywords
+  (list
+   '("\\([0-9]+:\\)?[0-9]+:[0-9]+\\.[0-9]+" . 'subed-ass-time-face)
+   '(",[0-9]+ \\(-->\\) [0-9]+:" 1 'subed-ass-time-separator-face t)
+   '("^.*$" . 'subed-ass-text-face))
+  "Highlighting expressions for `subed-mode'.")
+
+
+;;; Parsing
+
+(defconst subed-ass--regexp-timestamp 
"\\(\\([0-9]+\\):\\)?\\([0-9]+\\):\\([0-9]+\\)\\(\\.\\([0-9]+\\)\\)?")
+(defconst subed-ass--regexp-start 
"\\(?:Dialogue\\|Comment\\|Picture\\|Sound\\|Movie\\|Command\\): [0-9]+,")
+(defconst subed-ass--regexp-separator "\n")
+
+(defun subed-ass--timestamp-to-msecs (time-string)
+  "Find HH:MM:SS.MS pattern in TIME-STRING and convert it to milliseconds.
+Return nil if TIME-STRING doesn't match the pattern."
+  (save-match-data
+    (when (string-match subed-ass--regexp-timestamp time-string)
+      (let ((hours (string-to-number (or (match-string 2 time-string) "0")))
+            (mins  (string-to-number (match-string 3 time-string)))
+            (secs  (string-to-number (match-string 4 time-string)))
+            (msecs (if (match-string 6 time-string) (string-to-number 
(subed--right-pad (match-string 6 time-string) 3 ?0)) 0)))
+        (+ (* (truncate hours) 3600000)
+           (* (truncate mins) 60000)
+           (* (truncate secs) 1000)
+           (truncate msecs))))))
+
+(defun subed-ass--msecs-to-timestamp (msecs)
+  "Convert MSECS to string in the format H:MM:SS.CS."
+  ;; We need to wrap format-seconds in save-match-data because it does regexp
+  ;; stuff and we need to preserve our own match-data.
+  (concat (save-match-data (format-seconds "%h:%02m:%02s" (/ msecs 1000)))
+          "." (format "%02d" (/ (mod msecs 1000) 10))))
+
+(defun subed-ass--subtitle-id ()
+  "Return the ID of the subtitle at point or nil if there is no ID."
+  (save-excursion
+    (when (subed-ass--jump-to-subtitle-id)
+      (when (looking-at subed-ass--regexp-timestamp)
+        (match-string 0)))))
+
+(defun subed-ass--subtitle-id-max ()
+  "Return the ID of the last subtitle or nil if there are no subtitles."
+  (save-excursion
+    (goto-char (point-max))
+    (subed-ass--subtitle-id)))
+
+(defun subed-ass--subtitle-id-at-msecs (msecs)
+  "Return the ID of the subtitle at MSECS milliseconds.
+Return nil if there is no subtitle at MSECS."
+  (save-match-data
+    (save-excursion
+      (goto-char (point-min))
+      (let* ((secs       (/ msecs 1000))
+             (only-hours (truncate (/ secs 3600)))
+             (only-mins  (truncate (/ (- secs (* only-hours 3600)) 60))))
+        ;; Move to first subtitle in the relevant hour
+        (when (re-search-forward (format "\\(%s\\|\\`\\)%02d:" 
subed-ass--regexp-separator only-hours) nil t)
+          (beginning-of-line)
+          ;; Move to first subtitle in the relevant hour and minute
+          (re-search-forward (format "\\(\n\n\\|\\`\\)%02d:%02d" only-hours 
only-mins) nil t)))
+      ;; Move to first subtitle that starts at or after MSECS
+      (catch 'subtitle-id
+        (while (<= (or (subed-ass--subtitle-msecs-start) -1) msecs)
+          ;; If stop time is >= MSECS, we found a match
+          (let ((cur-sub-end (subed-ass--subtitle-msecs-stop)))
+            (when (and cur-sub-end (>= cur-sub-end msecs))
+              (throw 'subtitle-id (subed-ass--subtitle-id))))
+          (unless (subed-ass--forward-subtitle-id)
+            (throw 'subtitle-id nil)))))))
+
+(defun subed-ass--subtitle-msecs-start (&optional sub-id)
+  "Subtitle start time in milliseconds or nil if it can't be found.
+If SUB-ID is not given, use subtitle on point."
+  (let ((timestamp (save-excursion
+                     (when (subed-ass--jump-to-subtitle-time-start sub-id)
+                       (when (looking-at subed-ass--regexp-timestamp)
+                         (match-string 0))))))
+    (when timestamp
+      (subed-ass--timestamp-to-msecs timestamp))))
+
+(defun subed-ass--subtitle-msecs-stop (&optional sub-id)
+  "Subtitle stop time in milliseconds or nil if it can't be found.
+If SUB-ID is not given, use subtitle on point."
+  (let ((timestamp (save-excursion
+                     (when (subed-ass--jump-to-subtitle-time-stop sub-id)
+                       (when (looking-at subed-ass--regexp-timestamp)
+                         (match-string 0))))))
+    (when timestamp
+      (subed-ass--timestamp-to-msecs timestamp))))
+
+(defun subed-ass--subtitle-text (&optional sub-id)
+  "Return subtitle's text or an empty string.
+If SUB-ID is not given, use subtitle on point."
+  (or (save-excursion
+        (let ((beg (subed-ass--jump-to-subtitle-text sub-id))
+              (end (subed-ass--jump-to-subtitle-end sub-id)))
+          (when (and beg end)
+            (buffer-substring beg end))))
+      ""))
+
+(defun subed-ass--subtitle-relative-point ()
+  "Point relative to subtitle's ID or nil if ID can't be found."
+  (let ((start-point (save-excursion
+                       (when (subed-ass--jump-to-subtitle-id)
+                         (point)))))
+    (when start-point
+      (- (point) start-point))))
+
+;;; Traversing
+
+(defun subed-ass--jump-to-subtitle-id (&optional sub-id)
+  "Move to the ID of a subtitle and return point.
+If SUB-ID is not given, focus the current subtitle's ID.
+Return point or nil if no subtitle ID could be found.
+ASS doesn't use IDs, so we use the starting timestamp instead."
+  (interactive)
+  (save-match-data
+    (if (stringp sub-id)
+        (let* ((orig-point (point))
+               (find-ms (subed-ass--timestamp-to-msecs sub-id))
+               (regex (concat "^\\(?:" subed-ass--regexp-start "\\)\\(" 
subed-ass--regexp-timestamp "\\)"))
+               done)
+          (goto-char (point-min))
+          (while (not done)
+            (if (re-search-forward regex nil t)
+                (when (= (subed-ass--timestamp-to-msecs (match-string 1)) 
find-ms)
+                  (setq done 'found)
+                  (goto-char (match-beginning 1)))
+              (setq done 'not-found)
+              (goto-char orig-point)))
+          (when (eq done 'found)
+            (beginning-of-line)
+            (point)))
+      (end-of-line)
+      (let* ((regex (concat "^\\(?:" subed-ass--regexp-start "\\)\\(" 
subed-ass--regexp-timestamp "\\)"))
+             (match-found (re-search-backward regex nil t)))
+        (when (or match-found (re-search-forward regex nil t))           ;; 
maybe at the beginning?
+          (goto-char (match-beginning 0))
+          (point))))))
+
+(defun subed-ass--jump-to-subtitle-id-at-msecs (msecs)
+  "Move point to the ID of the subtitle that is playing at MSECS.
+Return point or nil if point is still on the same subtitle.
+See also `subed-ass--subtitle-id-at-msecs'."
+  (let ((current-sub-id (subed-ass--subtitle-id))
+        (target-sub-id (subed-ass--subtitle-id-at-msecs msecs)))
+    (when (and target-sub-id current-sub-id (not (equal target-sub-id 
current-sub-id)))
+      (subed-ass--jump-to-subtitle-id target-sub-id))))
+
+(defun subed-ass--jump-to-subtitle-text-at-msecs (msecs)
+  "Move point to the text of the subtitle that is playing at MSECS.
+Return point or nil if point is still on the same subtitle.
+See also `subed-ass--subtitle-id-at-msecs'."
+  (when (subed-ass--jump-to-subtitle-id-at-msecs msecs)
+    (subed-ass--jump-to-subtitle-text)))
+
+(defun subed-ass--jump-to-subtitle-time-start (&optional sub-id)
+  "Move point to subtitle's start time.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if no start time could be found."
+  (interactive)
+  (save-match-data
+    (when (subed-ass--jump-to-subtitle-id sub-id)
+      (when (re-search-forward subed-ass--regexp-timestamp (line-end-position) 
t)
+        (goto-char (match-beginning 0))
+        (point)))))
+
+(defun subed-ass--jump-to-subtitle-time-stop (&optional sub-id)
+  "Move point to subtitle's stop time.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if no stop time could be found."
+  (interactive)
+  (save-match-data
+    (when (subed-ass--jump-to-subtitle-id sub-id)
+      (re-search-forward (concat "\\(?:" subed-ass--regexp-timestamp "\\),") 
(point-at-eol) t)
+      (when (looking-at subed-ass--regexp-timestamp)
+        (point)))))
+
+(defun subed-ass--jump-to-subtitle-text (&optional sub-id)
+  "Move point on the first character of subtitle's text.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if a the subtitle's text can't be found."
+  (interactive)
+  (when (subed-ass--jump-to-subtitle-id sub-id)
+    (beginning-of-line)
+    (when (looking-at ".*?,.*?,.*?,.*?,.*?,.*?,.*?,.*?,.*?,")
+      (goto-char (match-end 0)))
+    (point)))
+
+(defun subed-ass--jump-to-subtitle-end (&optional sub-id)
+  "Move point after the last character of the subtitle's text.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if point did not change or if no subtitle end
+can be found."
+  (interactive)
+  (save-match-data
+    (let ((orig-point (point)))
+      (when (subed-ass--jump-to-subtitle-text sub-id)
+        (end-of-line)
+        (unless (= orig-point (point))
+          (point))))))
+
+(defun subed-ass--forward-subtitle-id ()
+  "Move point to next subtitle's ID.
+Return point or nil if there is no next subtitle."
+  (interactive)
+  (save-match-data
+    (let ((pos (point)))
+      (forward-line 1)
+      (beginning-of-line)
+      (while (not (or (eobp) (looking-at subed-ass--regexp-start)))
+        (forward-line 1))
+      (if (looking-at subed-ass--regexp-start)
+          (point)
+        (goto-char pos)
+        nil))))
+
+(defun subed-ass--backward-subtitle-id ()
+  "Move point to previous subtitle's ID.
+Return point or nil if there is no previous subtitle."
+  (interactive)
+  (let ((orig-point (point)))
+    (when (subed-ass--jump-to-subtitle-id)
+      (forward-line -1)
+      (while (not (or (bobp) (looking-at subed-ass--regexp-start)))
+        (forward-line -1))
+      (if (looking-at subed-ass--regexp-start)
+          (point)
+        (goto-char orig-point)
+        nil))))
+
+(defun subed-ass--forward-subtitle-text ()
+  "Move point to next subtitle's text.
+Return point or nil if there is no next subtitle."
+  (interactive)
+  (when (subed-ass--forward-subtitle-id)
+    (subed-ass--jump-to-subtitle-text)))
+
+(defun subed-ass--backward-subtitle-text ()
+  "Move point to previous subtitle's text.
+Return point or nil if there is no previous subtitle."
+  (interactive)
+  (when (subed-ass--backward-subtitle-id)
+    (subed-ass--jump-to-subtitle-text)))
+
+(defun subed-ass--forward-subtitle-end ()
+  "Move point to end of next subtitle.
+Return point or nil if there is no next subtitle."
+  (interactive)
+  (when (subed-ass--forward-subtitle-id)
+    (subed-ass--jump-to-subtitle-end)))
+
+(defun subed-ass--backward-subtitle-end ()
+  "Move point to end of previous subtitle.
+Return point or nil if there is no previous subtitle."
+  (interactive)
+  (when (subed-ass--backward-subtitle-id)
+    (subed-ass--jump-to-subtitle-end)))
+
+(defun subed-ass--forward-subtitle-time-start ()
+  "Move point to next subtitle's start time."
+  (interactive)
+  (when (subed-ass--forward-subtitle-id)
+    (subed-ass--jump-to-subtitle-time-start)))
+
+(defun subed-ass--backward-subtitle-time-start ()
+  "Move point to previous subtitle's start time."
+  (interactive)
+  (when (subed-ass--backward-subtitle-id)
+    (subed-ass--jump-to-subtitle-time-start)))
+
+(defun subed-ass--forward-subtitle-time-stop ()
+  "Move point to next subtitle's stop time."
+  (interactive)
+  (when (subed-ass--forward-subtitle-id)
+    (subed-ass--jump-to-subtitle-time-stop)))
+
+(defun subed-ass--backward-subtitle-time-stop ()
+  "Move point to previous subtitle's stop time."
+  (interactive)
+  (when (subed-ass--backward-subtitle-id)
+    (subed-ass--jump-to-subtitle-time-stop)))
+
+;;; Manipulation
+
+(defun subed-ass--set-subtitle-time-start (msecs &optional sub-id)
+  "Set subtitle start time to MSECS milliseconds.
+
+If SUB-ID is not given, set the start of the current subtitle.
+
+Return the new subtitle start time in milliseconds."
+  (save-excursion
+    (when (or (not sub-id)
+              (and sub-id (subed-ass--jump-to-subtitle-id sub-id)))
+      (subed-ass--jump-to-subtitle-time-start)
+      (when (looking-at subed-ass--regexp-timestamp)
+        (replace-match (subed-ass--msecs-to-timestamp msecs))))))
+
+(defun subed-ass--set-subtitle-time-stop (msecs &optional sub-id)
+  "Set subtitle stop time to MSECS milliseconds.
+
+If SUB-ID is not given, set the stop of the current subtitle.
+
+Return the new subtitle stop time in milliseconds."
+  (save-excursion
+    (when (or (not sub-id)
+              (and sub-id (subed-ass--jump-to-subtitle-id sub-id)))
+      (subed-ass--jump-to-subtitle-time-stop)
+      (when (looking-at subed-ass--regexp-timestamp)
+        (replace-match (subed-ass--msecs-to-timestamp msecs))))))
+
+(defun subed-ass--make-subtitle (&optional id start stop text)
+  "Generate new subtitle string.
+
+ID, START default to 0.
+STOP defaults to (+ START `subed-subtitle-spacing')
+TEXT defaults to an empty string.
+
+A newline is appended to TEXT, meaning you'll get two trailing
+newlines if TEXT is nil or empty."
+  (interactive "P")
+  (format "Dialogue: 0,%s,%s,Default,,0,0,0,,%s\n"
+          (subed-ass--msecs-to-timestamp (or start 0))
+          (subed-ass--msecs-to-timestamp (or stop (+ (or start 0)
+                                                     
subed-default-subtitle-length)))
+          (replace-regexp-in-string "\n" "\\n" (or text ""))))
+
+(defun subed-ass--prepend-subtitle (&optional id start stop text)
+  "Insert new subtitle before the subtitle at point.
+
+ID and START default to 0.
+STOP defaults to (+ START `subed-subtitle-spacing')
+TEXT defaults to an empty string.
+
+Move point to the text of the inserted subtitle.
+Return new point."
+  (interactive "P")
+  (subed-ass--jump-to-subtitle-id)
+  (insert (subed-ass--make-subtitle id start stop text))
+  (forward-line -1)
+  (subed-ass--jump-to-subtitle-text))
+
+(defun subed-ass--append-subtitle (&optional id start stop text)
+  "Insert new subtitle after the subtitle at point.
+
+ID, START default to 0.
+STOP defaults to (+ START `subed-subtitle-spacing')
+TEXT defaults to an empty string.
+
+Move point to the text of the inserted subtitle.
+Return new point."
+  (interactive "P")
+  (unless (subed-ass--forward-subtitle-id)
+    ;; Point is on last subtitle or buffer is empty
+    (subed-ass--jump-to-subtitle-end)
+    (unless (bolp) (insert "\n")))
+  (insert (subed-ass--make-subtitle id start stop text))
+  (forward-line -1)
+  (subed-ass--jump-to-subtitle-text))
+
+(defun subed-ass--kill-subtitle ()
+  "Remove subtitle at point."
+  (interactive)
+  (let ((beg (save-excursion (subed-ass--jump-to-subtitle-id)
+                             (point)))
+        (end (save-excursion (subed-ass--jump-to-subtitle-id)
+                             (when (subed-ass--forward-subtitle-id)
+                               (point)))))
+    (if (not end)
+        ;; Removing the last subtitle because forward-subtitle-id returned nil
+        (setq beg (save-excursion (goto-char beg)
+                                  (subed-ass--backward-subtitle-end)
+                                  (1+ (point)))
+              end (save-excursion (goto-char (point-max)))))
+    (delete-region beg end)))
+
+(defun subed-ass--merge-with-next ()
+  "Merge the current subtitle with the next subtitle.
+Update the end timestamp accordingly."
+  (interactive)
+  (save-excursion
+    (subed-ass--jump-to-subtitle-end)
+    (let ((pos (point)) new-end)
+      (if (subed-ass--forward-subtitle-time-stop)
+          (progn
+            (when (looking-at subed-ass--regexp-timestamp)
+              (setq new-end (subed-ass--timestamp-to-msecs (match-string 0))))
+            (subed-ass--jump-to-subtitle-text)
+            (delete-region pos (point))
+            (insert " ")
+            (subed-ass--set-subtitle-time-stop new-end))
+        (error "No subtitle to merge into")))))
+
+
+;;; Maintenance
+
+(defun subed-ass--regenerate-ids ()
+  "Not applicable to ASS."
+  (interactive))
+
+(defvar-local subed-ass--regenerate-ids-soon-timer nil)
+(defun subed-ass--regenerate-ids-soon ()
+  "Not applicable to ASS."
+  (interactive))
+
+(defun subed-ass--sanitize ()
+  "Not yet implemented."
+  (interactive))
+
+(defun subed-ass--validate ()
+  "Not yet implemented."
+  (interactive))
+
+(defun subed-ass--sort ()
+  "Not yet implemented."
+  (interactive))
+
+(defun subed-ass--init ()
+  "This function is called when subed-mode is entered for a ASS file."
+  (setq-local subed--subtitle-format "ass")
+  (setq-local font-lock-defaults '(subed-ass-font-lock-keywords)))
+
+(provide 'subed-ass)
+;;; subed-ass.el ends here
diff --git a/subed/subed.el b/subed/subed.el
index 6ba7a13..773e7f4 100644
--- a/subed/subed.el
+++ b/subed/subed.el
@@ -37,6 +37,7 @@
 (require 'subed-common)
 (require 'subed-srt)
 (require 'subed-vtt)
+(require 'subed-ass)
 (require 'subed-mpv)
 
 (defconst subed-mpv-frame-step-map
@@ -91,7 +92,8 @@
 
 ;;;###autoload
 (defvar subed--init-alist '(("srt" . subed-srt--init)
-                            ("vtt" . subed-vtt--init))
+                            ("vtt" . subed-vtt--init)
+                            ("ass" . subed-ass--init))
   "Alist that maps file extensions to format-specific init functions.")
 
 ;;; Abstraction hack to support different subtitle formats
diff --git a/tests/test-subed-ass.el b/tests/test-subed-ass.el
new file mode 100644
index 0000000..8e79522
--- /dev/null
+++ b/tests/test-subed-ass.el
@@ -0,0 +1,391 @@
+;; -*- eval: (buttercup-minor-mode) -*-
+
+(add-to-list 'load-path "./subed")
+(require 'subed)
+
+(defvar mock-ass-data
+  "[Script Info]
+; Script generated by FFmpeg/Lavc58.134.100
+ScriptType: v4.00+
+PlayResX: 384
+PlayResY: 288
+ScaledBorderAndShadow: yes
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, 
OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, 
Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, 
MarginV, Encoding
+Style: 
Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,0
+
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: 0,0:00:11.12,0:00:14.00,Default,,0,0,0,,Hello, world!
+Dialogue: 0,0:00:14.00,0:00:16.80,Default,,0,0,0,,This is a test.
+Dialogue: 0,0:00:17.00,0:00:19.80,Default,,0,0,0,,I hope it works.
+")
+
+(defmacro with-temp-ass-buffer (&rest body)
+  "Call `subed-ass--init' in temporary buffer before running BODY."
+  `(with-temp-buffer
+     (subed-ass--init)
+     (progn ,@body)))
+
+(describe "ASS"
+  (describe "Getting"
+    (describe "the subtitle start/stop time"
+      (it "returns the time in milliseconds."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "0:00:14.00")
+         (expect (subed-ass--subtitle-msecs-start) :to-equal (* 14 1000))
+         (expect (subed-ass--subtitle-msecs-stop) :to-equal (+ (* 16 1000) 
800))))
+      (it "returns nil if time can't be found."
+        (with-temp-ass-buffer
+         (expect (subed-ass--subtitle-msecs-start) :to-be nil)
+         (expect (subed-ass--subtitle-msecs-stop) :to-be nil)))
+      )
+    (describe "the subtitle text"
+      (describe "when text is empty"
+        (it "and at the beginning with a trailing newline."
+          (with-temp-ass-buffer
+           (insert mock-ass-data)
+           (subed-ass--jump-to-subtitle-text "0:00:11.12")
+           (kill-line)
+           (expect (subed-ass--subtitle-text) :to-equal "")))))
+    (describe "when text is not empty"
+      (it "and has no linebreaks."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-text "0:00:14.00")
+         (expect (subed-ass--subtitle-text) :to-equal "This is a test.")))))
+  (describe "Jumping"
+    (describe "to current subtitle timestamp"
+      (it "can handle different formats of timestamps."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (expect (subed-ass--jump-to-subtitle-id "00:00:11.120") :to-equal 564)
+         (expect (subed-ass--subtitle-msecs-start) :to-equal 11120)))
+      (it "returns timestamp's point when point is already on the timestamp."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (subed-ass--jump-to-subtitle-id "0:00:11.12")
+         (expect (subed-ass--jump-to-subtitle-time-start) :to-equal (point))
+         (expect (looking-at subed-ass--regexp-timestamp) :to-be t)
+         (expect (match-string 0) :to-equal "0:00:11.12")))
+      (it "returns timestamp's point when point is on the text."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (search-backward "test")
+         (expect (thing-at-point 'word) :to-equal "test")
+         (expect (subed-ass--jump-to-subtitle-time-start) :to-equal 640)
+         (expect (looking-at subed-ass--regexp-timestamp) :to-be t)
+         (expect (match-string 0) :to-equal "0:00:14.00")))
+      (it "returns nil if buffer is empty."
+        (with-temp-ass-buffer
+         (expect (buffer-string) :to-equal "")
+         (expect (subed-ass--jump-to-subtitle-time-start) :to-equal nil))))
+    (describe "to specific subtitle by timestamp"
+      (it "returns timestamp's point if wanted time exists."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-max))
+         (expect (subed-ass--jump-to-subtitle-id "0:00:11.12") :to-equal 564)
+         (expect (looking-at (regexp-quote "Dialogue: 0,0:00:11.12")) :to-be t)
+         (expect (subed-ass--jump-to-subtitle-id "0:00:17.00") :to-equal 694)
+         (expect (looking-at (regexp-quote "Dialogue: 0,0:00:17.00")) :to-be 
t)))
+      (it "returns nil and does not move if wanted ID does not exists."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (search-forward "test")
+         (let ((stored-point (point)))
+           (expect (subed-ass--jump-to-subtitle-id "0:08:00") :to-equal nil)
+           (expect stored-point :to-equal (point))))))
+    (describe "to subtitle start time"
+      (it "returns start time's point if movement was successful."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (re-search-backward "world")
+         (expect (subed-ass--jump-to-subtitle-time-start) :to-equal 576)
+         (expect (looking-at subed-ass--regexp-timestamp) :to-be t)
+         (expect (match-string 0) :to-equal "0:00:11.12")))
+      (it "returns nil if movement failed."
+        (with-temp-ass-buffer
+         (expect (subed-ass--jump-to-subtitle-time-start) :to-equal nil))))
+    (describe "to subtitle stop time"
+      (it "returns stop time's point if movement was successful."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (re-search-backward "test")
+         (expect (subed-ass--jump-to-subtitle-time-stop) :to-equal 651)
+         (expect (looking-at subed-ass--regexp-timestamp) :to-be t)
+         (expect (match-string 0) :to-equal "0:00:16.80")))
+      (it "returns nil if movement failed."
+        (with-temp-ass-buffer
+         (expect (subed-ass--jump-to-subtitle-time-stop) :to-equal nil))))
+    (describe "to subtitle text"
+      (it "returns subtitle text's point if movement was successful."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (expect (subed-ass--jump-to-subtitle-text) :to-equal 614)
+         (expect (looking-at "Hello, world!") :to-equal t)
+         (forward-line 1)
+         (expect (subed-ass--jump-to-subtitle-text) :to-equal 678)
+         (expect (looking-at "This is a test.") :to-equal t)))
+      (it "returns nil if movement failed."
+        (with-temp-ass-buffer
+         (expect (subed-ass--jump-to-subtitle-time-stop) :to-equal nil))))
+    (describe "to end of subtitle text"
+      (it "returns point if subtitle end can be found."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (expect (subed-ass--jump-to-subtitle-end) :to-be 627)
+         (expect (looking-back "Hello, world!") :to-be t)
+         (forward-char 2)
+         (expect (subed-ass--jump-to-subtitle-end) :to-be 693)
+         (expect (looking-back "This is a test.") :to-be t)
+         (forward-char 2)
+         (expect (subed-ass--jump-to-subtitle-end) :to-be 760)
+         (expect (looking-back "I hope it works.") :to-be t)))
+      (it "returns nil if subtitle end cannot be found."
+        (with-temp-ass-buffer
+         (expect (subed-ass--jump-to-subtitle-end) :to-be nil)))
+      (it "returns nil if point did not move."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-text "0:00:11.12")
+         (subed-ass--jump-to-subtitle-end)
+         (expect (subed-ass--jump-to-subtitle-end) :to-be nil)))
+      (it "works if text is empty."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-text "00:00:11.12")
+         (kill-line)
+         (backward-char)
+         (expect (subed-ass--jump-to-subtitle-end) :to-be 614))))
+    (describe "to next subtitle ID"
+      (it "returns point when there is a next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:11.12")
+         (expect (subed-ass--forward-subtitle-id) :to-be 628)
+         (expect (looking-at (regexp-quote "Dialogue: 0,0:00:14.00")) :to-be 
t)))
+      (it "returns nil and doesn't move when there is no next subtitle."
+        (with-temp-ass-buffer
+         (expect (thing-at-point 'word) :to-equal nil)
+         (expect (subed-ass--forward-subtitle-id) :to-be nil))
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-text "0:00:17.00")
+         (expect (subed-ass--forward-subtitle-id) :to-be nil))))
+    (describe "to previous subtitle ID"
+      (it "returns point when there is a previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-text "00:00:14.00")
+         (expect (subed-ass--backward-subtitle-id) :to-be 564)))
+      (it "returns nil and doesn't move when there is no previous subtitle."
+        (with-temp-ass-buffer
+         (expect (subed-ass--backward-subtitle-id) :to-be nil))
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:11.12")
+         (expect (subed-ass--backward-subtitle-id) :to-be nil))))
+    (describe "to next subtitle text"
+      (it "returns point when there is a next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:14.00")
+         (expect (subed-ass--forward-subtitle-text) :to-be 744)
+         (expect (thing-at-point 'word) :to-equal "I")))
+      (it "returns nil and doesn't move when there is no next subtitle."
+        (with-temp-ass-buffer
+         (goto-char (point-max))
+         (insert (concat mock-ass-data "\n\n"))
+         (subed-ass--jump-to-subtitle-id "00:00:17.00")
+         (expect (subed-ass--forward-subtitle-text) :to-be nil))))
+    (describe "to previous subtitle text"
+      (it "returns point when there is a previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:14.00")
+         (expect (subed-ass--backward-subtitle-text) :to-be 614)
+         (expect (thing-at-point 'word) :to-equal "Hello")))
+      (it "returns nil and doesn't move when there is no previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (subed-ass--forward-subtitle-time-start)
+         (expect (looking-at (regexp-quote "0:00:11.12")) :to-be t)
+         (expect (subed-ass--backward-subtitle-text) :to-be nil)
+         (expect (looking-at (regexp-quote "0:00:11.12")) :to-be t))))
+    (describe "to next subtitle end"
+      (it "returns point when there is a next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-text "00:00:14.00")
+         (expect (thing-at-point 'word) :to-equal "This")
+         (expect (subed-ass--forward-subtitle-end) :to-be 760)))
+      (it "returns nil and doesn't move when there is no next subtitle."
+        (with-temp-ass-buffer
+         (insert (concat mock-ass-data "\n\n"))
+         (subed-ass--jump-to-subtitle-text "00:00:17.00")
+         (expect (subed-ass--forward-subtitle-end) :to-be nil))))
+    (describe "to previous subtitle end"
+      (it "returns point when there is a previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:14.00")
+         (expect (subed-ass--backward-subtitle-end) :to-be 627)))
+      (it "returns nil and doesn't move when there is no previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (subed-ass--forward-subtitle-id)
+         (expect (looking-at (regexp-quote "Dialogue: 0,0:00:11.12")) :to-be t)
+         (expect (subed-ass--backward-subtitle-text) :to-be nil)
+         (expect (looking-at (regexp-quote "Dialogue: 0,0:00:11.12")) :to-be 
t))))
+    (describe "to next subtitle start time"
+      (it "returns point when there is a next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:14.00")
+         (expect (subed-ass--forward-subtitle-time-start) :to-be 706)))
+      (it "returns nil and doesn't move when there is no next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:17.00")
+         (let ((pos (point)))
+           (expect (subed-ass--forward-subtitle-time-start) :to-be nil)
+           (expect (point) :to-be pos)))))
+    (describe "to previous subtitle stop"
+      (it "returns point when there is a previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:14.00")
+         (expect (subed-ass--backward-subtitle-time-stop) :to-be 587)))
+      (it "returns nil and doesn't move when there is no previous subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (goto-char (point-min))
+         (subed-ass--forward-subtitle-id)
+         (expect (subed-ass--backward-subtitle-time-stop) :to-be nil)
+         (expect (looking-at (regexp-quote "Dialogue: 0,0:00:11.12")) :to-be 
t))))
+    (describe "to next subtitle stop time"
+      (it "returns point when there is a next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:14.00")
+         (expect (subed-ass--forward-subtitle-time-stop) :to-be 717)))
+      (it "returns nil and doesn't move when there is no next subtitle."
+        (with-temp-ass-buffer
+         (insert mock-ass-data)
+         (subed-ass--jump-to-subtitle-id "00:00:17.00")
+         (let ((pos (point)))
+           (expect (subed-ass--forward-subtitle-time-stop) :to-be nil)
+           (expect (point) :to-be pos))))))
+
+  (describe "Setting start/stop time"
+    (it "of subtitle should set it."
+      (with-temp-ass-buffer
+       (insert mock-ass-data)
+       (subed-ass--jump-to-subtitle-id "00:00:14.00")
+       (subed-ass--set-subtitle-time-start (+ (* 15 1000) 400))
+       (expect (subed-ass--subtitle-msecs-start) :to-be (+ (* 15 1000) 400)))))
+
+  (describe "Inserting a subtitle"
+    (describe "in an empty buffer"
+      (describe "before the current subtitle"
+        (it "creates an empty subtitle when passed nothing."
+          (with-temp-ass-buffer
+           (subed-ass--prepend-subtitle)
+           (expect (buffer-string) :to-equal (concat "Dialogue: 
0,0:00:00.00,0:00:01.00,Default,,0,0,0,,\n"))))
+        (it "creates a subtitle with a start time."
+          (with-temp-ass-buffer
+           (subed-ass--prepend-subtitle nil 12340)
+           (expect (buffer-string) :to-equal (concat "Dialogue: 
0,0:00:12.34,0:00:13.34,Default,,0,0,0,,\n"))))
+        (it "creates a subtitle with a start time and stop time."
+          (with-temp-ass-buffer
+           (subed-ass--prepend-subtitle nil 60000 65000) 
+           (expect (buffer-string) :to-equal "Dialogue: 
0,0:01:00.00,0:01:05.00,Default,,0,0,0,,\n")))
+        (it "creates a subtitle with start time, stop time and text."
+          (with-temp-ass-buffer
+           (subed-ass--prepend-subtitle nil 60000 65000 "Hello world")
+           (expect (buffer-string) :to-equal "Dialogue: 
0,0:01:00.00,0:01:05.00,Default,,0,0,0,,Hello world\n"))))
+      (describe "after the current subtitle"
+        (it "creates an empty subtitle when passed nothing."
+          (with-temp-ass-buffer
+           (subed-ass--append-subtitle)
+           (expect (buffer-string) :to-equal (concat "Dialogue: 
0,0:00:00.00,0:00:01.00,Default,,0,0,0,,\n"))))
+        (it "creates a subtitle with a start time."
+          (with-temp-ass-buffer
+           (subed-ass--append-subtitle nil 12340)
+           (expect (buffer-string) :to-equal (concat "Dialogue: 
0,0:00:12.34,0:00:13.34,Default,,0,0,0,,\n"))))
+        (it "creates a subtitle with a start time and stop time."
+          (with-temp-ass-buffer
+           (subed-ass--append-subtitle nil 60000 65000) 
+           (expect (buffer-string) :to-equal "Dialogue: 
0,0:01:00.00,0:01:05.00,Default,,0,0,0,,\n")))
+        (it "creates a subtitle with start time, stop time and text."
+          (with-temp-ass-buffer
+           (subed-ass--append-subtitle nil 60000 65000 "Hello world")
+           (expect (buffer-string) :to-equal "Dialogue: 
0,0:01:00.00,0:01:05.00,Default,,0,0,0,,Hello world\n"))))))
+  (describe "in a non-empty buffer"
+    (describe "before the current subtitle"
+      (describe "with point on the first subtitle"
+        (it "creates the subtitle before the current one."
+          (with-temp-ass-buffer
+           (insert mock-ass-data)
+           (subed-ass--jump-to-subtitle-time-stop)
+           (subed-ass--prepend-subtitle)
+           (expect (buffer-substring (line-beginning-position) 
(line-end-position))
+                   :to-equal (concat "Dialogue: 
0,0:00:00.00,0:00:01.00,Default,,0,0,0,,")))))
+      (describe "with point on a middle subtitle"
+        (it "creates the subtitle before the current one."
+          (with-temp-ass-buffer
+           (insert mock-ass-data)
+           (subed-ass--jump-to-subtitle-time-stop "0:00:14.00")
+           (subed-ass--prepend-subtitle)
+           (expect (buffer-substring (line-beginning-position) 
(line-end-position))
+                   :to-equal (concat "Dialogue: 
0,0:00:00.00,0:00:01.00,Default,,0,0,0,,"))
+           (forward-line 1)
+           (beginning-of-line)
+           (expect (looking-at "Dialogue: 0,0:00:14.00")))))
+      )
+    (describe "after the current subtitle"
+      (describe "with point on a subtitle"
+        (it "creates the subtitle after the current one."
+          (with-temp-ass-buffer
+           (insert mock-ass-data)
+           (subed-ass--jump-to-subtitle-time-stop "0:00:14.00")
+           (subed-ass--append-subtitle)
+           (expect (buffer-substring (line-beginning-position) 
(line-end-position))
+                   :to-equal (concat "Dialogue: 
0,0:00:00.00,0:00:01.00,Default,,0,0,0,,"))
+           (forward-line -1)
+           (expect (subed-ass--subtitle-msecs-start) :to-be 14000))))))
+  (describe "Killing a subtitle"
+    (it "removes the first subtitle."
+      (with-temp-ass-buffer
+       (insert mock-ass-data)
+       (subed-ass--jump-to-subtitle-text "0:00:11.12")
+       (subed-ass--kill-subtitle)
+       (expect (subed-ass--subtitle-msecs-start) :to-be 14000)
+       (forward-line -1)
+       (beginning-of-line)
+       (expect (looking-at "Format: Layer")))))
+  (it "removes it in between."
+    (with-temp-ass-buffer
+     (insert mock-ass-data)
+     (subed-ass--jump-to-subtitle-text "00:00:14.00")
+     (subed-ass--kill-subtitle)
+     (expect (subed-ass--subtitle-msecs-start) :to-be 17000)))
+  (it "removes the last subtitle."
+    (with-temp-ass-buffer
+     (insert mock-ass-data)
+     (subed-ass--jump-to-subtitle-text "00:00:17.00")
+     (subed-ass--kill-subtitle)
+     (expect (subed-ass--subtitle-msecs-start) :to-be 14000)))
+  (describe "Converting msecs to timestamp"
+    (it "uses the right format"
+      (with-temp-ass-buffer
+       (expect (subed-ass--msecs-to-timestamp 1410) :to-equal "0:00:01.41")))))
diff --git a/tests/test-subed-ass.el.license b/tests/test-subed-ass.el.license
new file mode 100644
index 0000000..80098a5
--- /dev/null
+++ b/tests/test-subed-ass.el.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2020 The subed Authors
+
+SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/tests/test-subed-vtt.el b/tests/test-subed-vtt.el
index cfcbc93..7c7f962 100644
--- a/tests/test-subed-vtt.el
+++ b/tests/test-subed-vtt.el
@@ -35,7 +35,7 @@ Baz.
         (with-temp-vtt-buffer
          (insert mock-vtt-data)
          (subed-vtt--jump-to-subtitle-id "00:03:03.45")
-         (expect (save-excursion (subed-jump-to-subtitle-time-start)
+         (expect (save-excursion (subed-vtt--jump-to-subtitle-time-start)
                                  (thing-at-point 'line)) :to-equal 
"00:03:03.45 --> 00:03:15.5\n")
          (expect (subed-vtt--subtitle-msecs-start) :to-equal (+ (* 3 60 1000) 
(*  3 1000) 450))
          (expect (subed-vtt--subtitle-msecs-stop)  :to-equal (+ (* 3 60 1000) 
(* 15 1000) 500))))



reply via email to

[Prev in Thread] Current Thread [Next in Thread]