[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))))
- [nongnu] elpa/subed a83ee74 357/389: Remove trailing space, (continued)
- [nongnu] elpa/subed a83ee74 357/389: Remove trailing space, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed da4dac9 358/389: Two spaces after sentence to make `make test` pass, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 49ddccc 359/389: Move subed--init-alist to subed.el, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 8263b33 360/389: subed-split-subtitle: Use offset or text fraction, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 1c52f1b 361/389: Add tests for splitting subtitles and handle more cases, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed e065632 362/389: Fix previous commit for subed-set-subtitle-text, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed ec7b222 368/389: Enable CPS showing by default and improve CPS toggling functions, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed f28ad22 379/389: Make subed-mpv-jump-to-current-subtitle interactive, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed c76ba50 387/389: Prompt for playback speed factor, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 9d0aa0f 388/389: Make make-subtitle a generic function, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed d0dfa1a 389/389: Preliminary support for .ass files (Advanced SubStation Alpha),
ELPA Syncer <=
- [nongnu] elpa/subed f30780e 249/389: Default keybinding: C-M-i -> subed-insert-subtitle-adjacent, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed f8793fb 330/389: Move motion hooks from subed-config.el to subed-common.el, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 31556c6 372/389: Add functions for bold and italic and change keybindings, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed ba22919 289/389: Add subed-mpv-jump-to-current-subtitle, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 14ebbbb 293/389: subed-srt--subtitle-id-at-msecs: Return nil if no matching subtitle, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 2e18727 294/389: Pause video initially, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 3d5902b 295/389: Reword comment, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 3baf5c1 296/389: subed-mpv--client-filter: Store process mark in variable, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 08d5033 301/389: Remove unused variable subed-mode--enabled-p, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 070384b 302/389: subed--set-subtitle-loop: Don't croak on empty file, ELPA Syncer, 2021/12/03