branch: elpa/subed
commit 10b85e48a06fcc7a70d02a516e2886ea2936d4bf
Author: Sacha Chua <[email protected]>
Commit: Sacha Chua <[email protected]>
subed-vtt: Improve ID and comment handling
* subed/subed-common.el (jump-to-subtitle-comment): New.
(forward-subtitle-comment): New.
(backward-subtitle-comment): New.
(subtitle-comment): New.
(set-subtitle-comment): New.
(subed-subtitle-list-text): Check for empty comment.
* subed/subed-vtt.el (subed--subtitle-comment): Move implementation to
comment section, tweak how comments are recognized.
(subed--jump-to-subtitle-id): Jump to ID if specified.
(subed--forward-subtitle-id): Look for the ID or timestamp.
(subed--backward-subtitle-id): Look for the ID or timestamp.
(subed--jump-to-subtitle-comment): New.
(subed--set-subtitle-comment): New.
(subed--prepend-subtitle): Insert before the comment or ID.
(subed--append-subtitle): Insert before the comment or ID.
* tests/test-subed-vtt.el: Add tests for comments and IDs.
---
subed/subed-common.el | 37 ++++++-
subed/subed-vtt.el | 94 +++++++++++------
tests/test-subed-vtt.el | 262 +++++++++++++++++++++++++++++++++++++++++++-----
3 files changed, 337 insertions(+), 56 deletions(-)
diff --git a/subed/subed-common.el b/subed/subed-common.el
index 017e3af542..c3dd0ab104 100644
--- a/subed/subed-common.el
+++ b/subed/subed-common.el
@@ -218,6 +218,12 @@ 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))
+(subed-define-generic-function jump-to-subtitle-comment (&optional sub-id)
+ "Move point on the first character of subtitle's comment.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if a the subtitle's comment can't be found."
+ (interactive))
+
(subed-define-generic-function 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.
@@ -265,6 +271,28 @@ Return point or nil if there is no previous subtitle."
(when (subed-backward-subtitle-id)
(subed-jump-to-subtitle-text)))
+(subed-define-generic-function forward-subtitle-comment ()
+ "Move point to next subtitle's comment.
+Return point or nil if there is no next subtitle."
+ (interactive)
+ (let ((pos (point)))
+ (if (and (subed-forward-subtitle-id)
+ (subed-jump-to-subtitle-comment))
+ (point)
+ (goto-char pos)
+ nil)))
+
+(subed-define-generic-function backward-subtitle-comment ()
+ "Move point to previous subtitle's comment.
+Return point or nil if there is no previous subtitle."
+ (interactive)
+ (let ((pos (point)))
+ (if (and (subed-backward-subtitle-id)
+ (subed-jump-to-subtitle-comment))
+ (point)
+ (goto-char pos)
+ nil)))
+
(subed-define-generic-function forward-subtitle-end ()
"Move point to end of next subtitle.
Return point or nil if there is no next subtitle."
@@ -350,7 +378,12 @@ If SUB-ID is not given, set the text of the current
subtitle."
(- (point) start-point))))
(subed-define-generic-function subtitle-comment (&optional _)
- "Return the comment preceding this subtitle."
+ "Return subtitle comment or an empty string."
+ nil)
+
+(subed-define-generic-function set-subtitle-comment (comment &optional _)
+ "Set the current subtitle's comment to COMMENT.
+If COMMENT is nil or the empty string, remove the comment."
nil)
(subed-define-generic-function set-subtitle-time-start (msecs
@@ -609,7 +642,7 @@ If INCLUDE-COMMENTS is a function, call the function on
comments
before including them."
(mapconcat
(lambda (sub)
- (if (and include-comments (elt sub 4))
+ (if (and include-comments (elt sub 4) (not (string= (elt sub 4) "")))
(concat "\n"
(if (functionp include-comments)
(funcall include-comments (elt sub 4))
diff --git a/subed/subed-vtt.el b/subed/subed-vtt.el
index 93d4844277..f8b5a232fe 100644
--- a/subed/subed-vtt.el
+++ b/subed/subed-vtt.el
@@ -94,18 +94,6 @@ format-specific function for MAJOR-MODE."
(unless (subed-forward-subtitle-id)
(throw 'subtitle-id nil))))))
-(cl-defmethod subed--subtitle-comment (&context (major-mode subed-vtt-mode)
&optional sub-id)
- "Return the comment or comments before the current subtitle.
-If SUB-ID is specified, jump to that subtitle first.
-Use the format-specific function for MAJOR-MODE."
- (save-excursion
- (subed-jump-to-subtitle-id sub-id)
- (let ((sub-start-point (point)))
- (or (subed-backward-subtitle-end)
- (goto-char (point-min)))
- (when (re-search-forward "^\\(NOTE\\(.*\n\\)+\n+\\)" sub-start-point t)
- (match-string 0)))))
-
;;; Traversing
(cl-defmethod subed--jump-to-subtitle-id (&context (major-mode subed-vtt-mode)
&optional sub-id)
@@ -119,14 +107,13 @@ Use the format-specific function for MAJOR-MODE."
;; Look for a line that contains the timestamp, preceded by one or more
;; blank lines or the beginning of the buffer.
(let* ((regex (concat "\\(" subed--regexp-separator "\\|\\`\\)\\("
- (regexp-quote sub-id) "\\)")))
+ (regexp-quote sub-id) "\\)[ \n]")))
(goto-char (point-min))
(setq found (re-search-forward regex nil t))
(if found
(goto-char (match-beginning 2))
(goto-char orig-point)
nil))
-
;; Find two or more blank lines or the beginning of the buffer, followed
;; by an optional line and a timestamp.
(or (and (re-search-backward subed--regexp-separator nil t)
@@ -138,6 +125,17 @@ Use the format-specific function for MAJOR-MODE."
(point))
((looking-at (concat "\\(.*\\)\n" subed-vtt--regexp-timestamp " *--> *"
subed-vtt--regexp-timestamp " *\n"))
(point))
+ ((looking-at "^NOTE[ \n]")
+ ;; At a subtitle's comment; scan forward for the timestamp
+ (if (re-search-forward
+ (concat
+ subed--regexp-separator
+ (concat "\\(.*\n\\)?" subed-vtt--regexp-timestamp " *--> *"
subed-vtt--regexp-timestamp " *\n")) nil t)
+ (progn
+ (goto-char (or (match-beginning 1) (match-beginning 2)))
+ (point))
+ (goto-char orig-point)
+ nil))
(t
(goto-char orig-point)
nil)))))
@@ -200,11 +198,13 @@ can be found. Use the format-specific function for
MAJOR-MODE."
Return point or nil if there is no next subtitle. Use the
format-specific function for MAJOR-MODE."
(let ((orig-point (point)))
- (if (and
- (re-search-forward subed--regexp-separator nil t)
- (re-search-forward (concat "^" subed--regexp-timestamp
- " *--> *" subed--regexp-timestamp)
- nil t))
+ (subed-jump-to-subtitle-end)
+ (when (and (bolp) (> (point) (point-min))) (forward-char -1))
+ (if (re-search-forward (concat subed--regexp-separator
+ "\\(.*?\n\\)?"
+ subed--regexp-timestamp
+ " *--> *" subed--regexp-timestamp)
+ nil t)
(or (subed-jump-to-subtitle-id)
(progn
(goto-char orig-point)
@@ -220,7 +220,11 @@ format-specific function for MAJOR-MODE."
(subed-jump-to-subtitle-id)
(or
(catch 'found
- (while (re-search-backward subed--regexp-separator nil t)
+ (while (re-search-backward
+ (concat "^\\(.*?\\n\\)?"
+ subed--regexp-timestamp
+ " *--> *" subed--regexp-timestamp)
+ nil t)
(when (subed-jump-to-subtitle-id)
(throw 'found (point)))))
(progn (goto-char orig-point) nil))))
@@ -240,6 +244,46 @@ Make sure COMMENT ends with a blank line."
((string-match "\n" comment) (concat "NOTE\n" comment "\n\n"))
(t (concat "NOTE " comment "\n\n"))))
+(cl-defmethod subed--jump-to-subtitle-comment (&context (major-mode
subed-vtt-mode)
+ &optional sub-id)
+ "Move point to subtitle's comment.
+If SUB-ID is not given, use subtitle on point.
+Return point, or nil if no comment could be found.
+Use the format-specific function for MAJOR-MODE."
+ (let ((pos (point)))
+ (if (and (subed-jump-to-subtitle-id sub-id)
+ (re-search-backward "^NOTE"
+ (or (save-excursion
(subed-backward-subtitle-end)) (point-min)) t))
+ (progn (goto-char (match-beginning 0))
+ (point))
+ (goto-char pos)
+ nil)))
+
+(cl-defmethod subed--subtitle-comment (&context (major-mode subed-vtt-mode)
+ &optional sub-id)
+ "Return subtitle comment or an empty string.
+If SUB-ID is not given, use the subtitle on point.
+Use the format-specific function for MAJOR-MODE."
+ (save-excursion
+ (let ((comment-start (subed-jump-to-subtitle-comment sub-id)))
+ (if comment-start
+ (buffer-substring comment-start (subed-jump-to-subtitle-id sub-id))
+ ""))))
+
+(cl-defmethod subed--set-subtitle-comment (comment &context (major-mode
subed-vtt-mode)
+ &optional sub-id)
+ "Set the current subtitle's comment to COMMENT.
+If COMMENT is nil or the empty string, remove the comment.
+If SUB-ID is not given, use the subtitle on point.
+Use the format-specific function for MAJOR-MODE."
+ (let ((comment-start (subed-jump-to-subtitle-comment sub-id)))
+ ;; remove previous comment
+ (if comment-start
+ (delete-region comment-start (subed-jump-to-subtitle-id sub-id))
+ (subed-jump-to-subtitle-id sub-id))
+ (when (and comment (not (string= comment "")))
+ (insert (subed-vtt--format-comment comment)))))
+
(cl-defmethod subed--make-subtitle (&context (major-mode subed-vtt-mode)
&optional _ start stop text
comment)
"Generate new subtitle string.
@@ -268,7 +312,7 @@ TEXT defaults to an empty string.
Move point to the text of the inserted subtitle. Return new
point. Use the format-specific function for MAJOR-MODE."
- (subed-jump-to-subtitle-id)
+ (or (subed-jump-to-subtitle-comment) (subed-jump-to-subtitle-id))
(insert (subed-make-subtitle id start stop text comment))
(when (looking-at (concat "\\([[:space:]]*\\|^\\)" subed--regexp-timestamp))
(insert "\n"))
@@ -285,13 +329,7 @@ TEXT defaults to an empty string.
Move point to the text of the inserted subtitle. Return new
point. Use the format-specific function for MAJOR-MODE."
(let ((pos (point)))
- (if (subed-forward-subtitle-id)
- ;; Insert before any comments
- (progn
- (subed-backward-subtitle-end)
- (cond
- ((eobp) (insert "\n\n"))
- ((looking-at " *\n\n") (goto-char (match-end 0)))))
+ (unless (or (subed-forward-subtitle-comment) (subed-forward-subtitle-id))
;; Point is on last subtitle or buffer is empty
(subed-jump-to-subtitle-end)
(when (looking-at "[[:space:]]+")
diff --git a/tests/test-subed-vtt.el b/tests/test-subed-vtt.el
index 06528fee92..b49a21ed47 100644
--- a/tests/test-subed-vtt.el
+++ b/tests/test-subed-vtt.el
@@ -216,24 +216,120 @@ Baz.
(expect (looking-at subed--regexp-timestamp) :to-be t)
(expect (match-string 0) :to-equal "00:01:01.000")))
)
- (describe "to specific subtitle by timestamp"
- (it "returns timestamp's point if wanted time exists."
- (with-temp-vtt-buffer
- (insert mock-vtt-data)
- (goto-char (point-max))
- (expect (subed-jump-to-subtitle-id "00:02:02.234") :to-equal 45)
- (expect (looking-at (regexp-quote "00:02:02.234")) :to-be t)
- (expect (subed-jump-to-subtitle-id "00:01:01.000") :to-equal 9)
- (expect (looking-at (regexp-quote "00:01:01.000")) :to-be t)))
- (it "returns nil and does not move if wanted ID does not exists."
- (with-temp-vtt-buffer
- (insert mock-vtt-data)
- (goto-char (point-min))
- (search-forward "Foo")
- (let ((stored-point (point)))
- (expect (subed-jump-to-subtitle-id "0:08:00") :to-equal nil)
- (expect stored-point :to-equal (point)))))
- )
+ (describe "to subtitle ID"
+ (describe "in the current subtitle"
+ (it "returns nil in the header."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-data)
+ (goto-char (point-min))
+ (expect (subed-jump-to-subtitle-id) :to-be nil)))
+ (it "goes to the ID if specified."
+ (with-temp-vtt-buffer
+ (insert "WEBVTT
+
+1
+00:01:01.000 --> 00:01:05.123
+Foo.
+
+00:02:02.234 --> 00:02:10.345
+Bar.
+")
+ (re-search-backward "Foo")
+ (expect (subed-jump-to-subtitle-id) :not :to-be nil)
+ (expect (looking-at "1") :to-be t)))
+ (it "goes to the timestamp if there is no ID."
+ (with-temp-vtt-buffer
+ (insert "WEBVTT
+
+1
+00:01:01.000 --> 00:01:05.123
+Foo.
+
+00:02:02.234 --> 00:02:10.345
+Bar.
+")
+ (re-search-backward "Bar")
+ (expect (subed-jump-to-subtitle-id) :not :to-be nil)
+ (expect (looking-at "00:02:02.234") :to-be t)))
+ (describe "when called from a comment"
+ (it "goes to the ID of the subtitle after the comment."
+ (with-temp-vtt-buffer
+ (insert "WEBVTT
+
+NOTE
+This is a comment
+
+1
+00:01:01.000 --> 00:01:05.123
+Foo.
+
+00:02:02.234 --> 00:02:10.345
+Bar.
+")
+ (re-search-backward "This is a
comment")
+ (expect (subed-jump-to-subtitle-id)
:not :to-be nil)
+ (expect (looking-at "1\n") :to-be
t)))
+ (it "goes to the timestamp of the subtitle after the comment if no
ID is specified."
+ (with-temp-vtt-buffer
+ (insert "WEBVTT
+
+1
+00:01:01.000 --> 00:01:05.123
+Foo.
+
+NOTE
+This is a comment
+
+00:02:02.234 --> 00:02:10.345
+Bar.
+")
+ (re-search-backward "This is a
comment")
+ (expect (subed-jump-to-subtitle-id)
:not :to-be nil)
+ (expect (looking-at (regexp-quote
"00:02:02.234")) :to-be t)))))
+ (describe "when given an ID"
+ (it "returns ID's point if wanted time exists."
+ (with-temp-vtt-buffer
+ (insert "WEBVTT
+
+1
+00:01:01.000 --> 00:01:05.123
+Foo.
+
+NOTE
+This is a comment
+
+00:02:02.234 --> 00:02:10.345
+Bar.
+")
+ (goto-char (point-max))
+ (expect (subed-jump-to-subtitle-id "1") :not :to-be nil)
+ (expect (looking-at "1\n") :to-be t)))
+ (it "returns nil and does not move if wanted ID does not exists."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-data)
+ (goto-char (point-min))
+ (search-forward "Foo")
+ (let ((stored-point (point)))
+ (expect (subed-jump-to-subtitle-id "3") :to-equal nil)
+ (expect stored-point :to-equal (point))))))
+ (describe "when given a timestamp"
+ (it "returns timestamp's point if wanted time exists."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-data)
+ (goto-char (point-max))
+ (expect (subed-jump-to-subtitle-id "00:02:02.234") :to-equal 45)
+ (expect (looking-at (regexp-quote "00:02:02.234")) :to-be t)
+ (expect (subed-jump-to-subtitle-id "00:01:01.000") :to-equal 9)
+ (expect (looking-at (regexp-quote "00:01:01.000")) :to-be t)))
+ (it "returns nil and does not move if wanted ID does not exists."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-data)
+ (goto-char (point-min))
+ (search-forward "Foo")
+ (let ((stored-point (point)))
+ (expect (subed-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-vtt-buffer
@@ -575,8 +671,7 @@ Baz.
(expect (looking-at (regexp-quote "00:01:01.000")) :to-be t)
(expect (subed-backward-subtitle-time-stop) :to-be nil)
(expect (looking-at (regexp-quote "00:01:01.000")) :to-be t)))
- )
- )
+ ))
(describe "Setting start/stop time"
(it "of current subtitle updates it."
@@ -848,7 +943,8 @@ Baz.
"00:00:05.000 --> 00:00:06.000\n"
"Bar.\n"))
(subed-jump-to-subtitle-time-start "00:00:01.000")
- (expect (subed-append-subtitle) :to-equal 67)
+ (subed-append-subtitle)
+ (expect (point) :to-equal 67)
(expect (buffer-string) :to-equal (concat "00:00:01.000 -->
00:00:02.000\n"
"Foo.\n\n"
"00:00:00.000 -->
00:00:01.000\n"
@@ -1289,9 +1385,9 @@ Baz.
(with-temp-vtt-buffer
(expect (subed-msecs-to-timestamp 1401) :to-equal "00:00:01.401"))))
(describe "Working with comments"
- (it "ignores the comment when jumping to the end of the subtitle"
- (with-temp-vtt-buffer
- (insert "WEBVTT
+ (before-each
+ (setq mock-vtt-comments-data
+ "WEBVTT
00:00:00.000 --> 00:00:01.000
This is a test.
@@ -1301,13 +1397,127 @@ and have more text as needed.
00:01:00.000 --> 00:00:02.000
This is another test here.
-")
+"))
+ (it "ignores the comment when jumping to the end of the subtitle"
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
(goto-char (point-min))
(subed-forward-subtitle-end)
(expect (current-word) :to-equal "test")
(subed-forward-subtitle-end)
- (expect (current-word) :to-equal "here"))))
+ (expect (current-word) :to-equal "here")))
+ (describe "jumping to the comment"
+ (it "returns nil when there is no comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (re-search-backward "This is a test")
+ (expect (subed-jump-to-subtitle-comment) :to-be nil)))
+ (it "jumps to the comment for the current subtitle."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (goto-char (point-max))
+ (subed-jump-to-subtitle-comment)
+ (expect (looking-at "NOTE A comment") :to-be t))))
+ (describe "getting the comment"
+ (it "returns an empty string when there is no comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (re-search-backward "This is a test")
+ (expect (subed-subtitle-comment) :to-equal "")))
+ (it "returns the comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (goto-char (point-max))
+ (expect (subed-subtitle-comment) :to-equal "NOTE A comment can go
here\nand have more text as needed.\n\n"))))
+ (describe "setting the comment"
+ (it "sets the comment when there isn't one yet."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (re-search-backward "This is a test")
+ (subed-set-subtitle-comment "Skip")
+ (expect (buffer-string) :to-equal
+ "WEBVTT
+
+NOTE Skip
+
+00:00:00.000 --> 00:00:01.000
+This is a test.
+
+NOTE A comment can go here
+and have more text as needed.
+
+00:01:00.000 --> 00:00:02.000
+This is another test here.
+")))
+ (it "replaces the comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (goto-char (point-max))
+ (subed-set-subtitle-comment "Replaced.")
+ (expect (buffer-string) :to-equal
+ "WEBVTT
+
+00:00:00.000 --> 00:00:01.000
+This is a test.
+NOTE Replaced.
+
+00:01:00.000 --> 00:00:02.000
+This is another test here.
+")))
+ (it "clears the comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (goto-char (point-max))
+ (subed-set-subtitle-comment nil)
+ (expect (buffer-string) :to-equal
+ "WEBVTT
+
+00:00:00.000 --> 00:00:01.000
+This is a test.
+
+00:01:00.000 --> 00:00:02.000
+This is another test here.
+"))))
+ (describe "going to the next subtitle's comment"
+ (it "returns nil in an empty buffer."
+ (with-temp-vtt-buffer
+ (expect (subed-forward-subtitle-comment) :to-be nil)))
+ (it "returns nil at the end of the file."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (expect (subed-forward-subtitle-comment) :to-be nil)))
+ (it "returns nil if the next subtitle does not have a comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (save-excursion (subed-append-subtitle))
+ (expect (subed-forward-subtitle-comment) :to-be nil)))
+ (it "jumps to the next subtitle's comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (re-search-backward "This is a test")
+ (expect (subed-forward-subtitle-comment) :not :to-be nil)
+ (expect (looking-at "NOTE ") :to-be t))))
+ (describe "going to the previous comment"
+ (it "returns nil in an empty buffer."
+ (with-temp-vtt-buffer
+ (expect (subed-backward-subtitle-comment) :to-be nil)))
+ (it "returns nil at the start of the file."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (goto-char (point-min))
+ (expect (subed-backward-subtitle-comment) :to-be nil)))
+ (it "returns nil if the previous subtitle does not have a comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (re-search-backward "This is another test here")
+ (expect (subed-backward-subtitle-comment) :to-be nil)))
+ (it "jumps to the previous subtitle's comment."
+ (with-temp-vtt-buffer
+ (insert mock-vtt-comments-data)
+ (subed-append-subtitle)
+ (expect (subed-backward-subtitle-comment) :not :to-be nil)
+ (expect (looking-at "NOTE ") :to-be t)))))
(describe "Merging with next subtitle"
(it "throws an error in an empty buffer."
(with-temp-vtt-buffer