branch: elpa/subed commit f8ce6dc1b5ef8a392063134582f8feb0941ab865 Author: Sacha Chua <sa...@sachachua.com> Commit: Sacha Chua <sa...@sachachua.com>
Change VTT to support multiple components in a cue * subed/subed-common.el (in-header-p): New generic function. (in-comment-p): New generic function. * subed/subed-vtt.el (subed-vtt--regexp-timestamp): Don't capture groups. (subed-vtt--regexp-separator): Tweak this regexp. (subed-vtt--regexp-blank-separator): New regexp just focuses on the blank lines that separate WebVTT components. Note that we allow spaces in between them even though the spec doesn't allow them. That's because spaces might accidentally get included. We remove them during the subed-sanitize step. (subed-vtt--regexp-note): New regexp. (subed-vtt--regexp-timing): New regexp for timestamp --> timestamp. (subed-vtt--regexp-maybe-identifier-and-timing): New regexp for ID and timing or just timing. (subed--timestamp-to-msecs): Use our own regular expression so that we can capture the correct groups. (subed--subtitle-id): Don't rely on match data from subed-jump-to-subtitle-id. (subed--subtitle-id-at-msecs): Move to the first subtitle since calling subtitle info functions from the header returns nil. * subed/subed-vtt.el (subed--in-header-p): New VTT-specific method. (subed--in-comment-p): New VTT-specific method. (subed--jump-to-subtitle-id): Handle linebreaks separating blocks in the same cue. (subed--jump-to-subtitle-time-start): Might as well use the specific regexes. (subed--jump-to-subtitle-time-stop): Handle tabs. (subed--jump-to-subtitle-end): Scan forward each separator. (subed--forward-subtitle-id): Tweak based on regexps. (subed--jump-to-subtitle-comment): Tweak. (subed--prepend-subtitle): Try using spaces. * tests/test-subed-vtt.el ("subed-vtt"): Add tests for header, comments, and components. --- subed/subed-common.el | 14 +- subed/subed-vtt.el | 236 ++++++-- tests/test-subed-vtt.el | 1191 +++++++++++++++++++++++++++++++---------- tests/test-subed-word-data.el | 26 +- 4 files changed, 1121 insertions(+), 346 deletions(-) diff --git a/subed/subed-common.el b/subed/subed-common.el index 4ac9a07abe..872fa3eaf8 100644 --- a/subed/subed-common.el +++ b/subed/subed-common.el @@ -273,11 +273,19 @@ See also `subed-subtitle-id-at-msecs'." (subed-jump-to-subtitle-id target-sub-id)))) (subed-define-generic-function jump-to-subtitle-text-at-msecs (msecs) - "Move point to the text of the subtitle that is playing at 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-vtt--subtitle-id-at-msecs'." - (when (subed-jump-to-subtitle-id-at-msecs msecs) - (subed-jump-to-subtitle-text))) + (when (subed-jump-to-subtitle-id-at-msecs msecs) + (subed-jump-to-subtitle-text))) + +(subed-define-generic-function in-header-p () + "Return non-nil if the point is in the file header." + nil) + +(subed-define-generic-function in-comment-p () + "Return non-nil if the point is in a comment." + nil) (subed-define-generic-function forward-subtitle-start-pos () "Move point to the beginning of the next subtitle. diff --git a/subed/subed-vtt.el b/subed/subed-vtt.el index c519b4bed5..a053e951fd 100644 --- a/subed/subed-vtt.el +++ b/subed/subed-vtt.el @@ -43,19 +43,31 @@ ;;; Parsing -(defconst subed-vtt--regexp-timestamp "\\(\\([0-9]+\\):\\)?\\([0-9]+\\):\\([0-9]+\\)\\(?:\\.\\([0-9]+\\)\\)?") -(defconst subed-vtt--regexp-separator "\\(?:[[:blank:]]*\n\n+NOTE[ \n]\\(?:.+?\n\\)+\\)*\n\n+") +(defconst subed-vtt--regexp-timestamp "\\(?:\\(?:[0-9]+\\):\\)?\\(?:[0-9]+\\):\\(?:[0-9]+\\)\\(?:\\.\\([0-9]+\\)\\)?") +(defconst subed-vtt--regexp-separator "\\(?:\\(?:[ \t]*\n\\)+\\(?:NOTE[ \t\n]*[ \t]*\n[ \t]*\n\\)?\\)" + "Blank lines and possibly a comment.") +(defconst subed-vtt--regexp-blank-separator "[ \t]*\n\\(?:[ \t]*\n\\)+") +(defconst subed-vtt--regexp-note "\\(NOTE[ \s\t\n]\\)") (defconst subed-vtt--regexp-identifier ;; According to https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API ;; Cues can start with an identifier which is a non empty line that does ;; not contain "-->". "^[ \t]*[^ \t\n-]\\(?:[^\n-]\\|-[^\n-]\\|--[^\n>]\\)*[ \t]*\n") +(defconst subed-vtt--regexp-timing + (concat subed-vtt--regexp-timestamp "[ \t]*-->[ \t]*" subed-vtt--regexp-timestamp)) +(defconst subed-vtt--regexp-maybe-identifier-and-timing + (format "%s\\(%s%s\\|%s\\)" + subed-vtt--regexp-blank-separator + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing)) (cl-defmethod subed--timestamp-to-msecs (time-string &context (major-mode subed-vtt-mode)) "Find HH:MM:SS.MS pattern in TIME-STRING and convert it to milliseconds. Return nil if TIME-STRING doesn't match the pattern. Use the format-specific function for MAJOR-MODE." - (when (string-match subed--regexp-timestamp time-string) + (when (string-match "\\(\\([0-9]+\\):\\)?\\([0-9]+\\):\\([0-9]+\\)\\(?:\\.\\([0-9]+\\)\\)?" + 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))) @@ -76,7 +88,11 @@ Use the format-specific function for MAJOR-MODE." Use the format-specific function for MAJOR-MODE." (save-excursion (when (subed-jump-to-subtitle-id) - (match-string 1)))) + (cond + ((looking-at (concat "[ \t]*\\(" subed-vtt--regexp-timestamp "\\)")) + (match-string 1)) + ((looking-at subed-vtt--regexp-identifier) + (string-trim (match-string 0))))))) (cl-defmethod subed--subtitle-id-at-msecs (msecs &context (major-mode subed-vtt-mode)) "Return the ID of the subtitle at MSECS milliseconds. @@ -84,6 +100,8 @@ Return nil if there is no subtitle at MSECS. Use the format-specific function for MAJOR-MODE." (save-excursion (goto-char (point-min)) + (unless (subed-subtitle-id) + (subed-forward-subtitle-time-start)) ;; Move to first subtitle that starts at or after MSECS (catch 'subtitle-id (while (<= (or (subed-subtitle-msecs-start) -1) msecs) @@ -96,17 +114,78 @@ format-specific function for MAJOR-MODE." ;;; Traversing +(cl-defmethod subed--in-header-p (&context (major-mode subed-vtt-mode)) + "Return non-nil if the point is in the file header." + (save-excursion + (let ((orig-point (point))) + (goto-char (point-min)) + (if (looking-at (format "[ \t\n]*\\(%s%s\\|%s\\|%s\\)" + subed-vtt--regexp-note + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing)) + (< orig-point (match-beginning 1)) + (if (re-search-forward (format "%s\\(%s\\|%s%s\\|%s\\)" + subed-vtt--regexp-blank-separator + subed-vtt--regexp-note + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing) + nil t) + (< orig-point (match-beginning 1)) + t))))) + +(cl-defmethod subed--in-comment-p (&context (major-mode subed-vtt-mode)) + "Return non-nil if the point is in a comment." + (let* ((orig-point (point)) + (previous-comment + (save-excursion + (forward-line 1) + (when (re-search-backward + (format "%s\\(%s\\)" + subed-vtt--regexp-blank-separator + subed-vtt--regexp-note) + nil t) + (match-beginning 1)))) + (previous-timestamp + (save-excursion + (or (re-search-forward subed-vtt--regexp-blank-separator nil t) + (goto-char (point-max))) + (when (re-search-backward subed-vtt--regexp-maybe-identifier-and-timing nil t) + (match-beginning 1))))) + (save-excursion + (cond + ((null previous-comment) nil) + ((null previous-timestamp) + (>= orig-point previous-comment)) + ((and + (< previous-comment previous-timestamp) + (>= orig-point previous-timestamp)) + nil) + ;; there's a previous comment after a previous timestamp + ((< previous-timestamp previous-comment) t) + ((and (> previous-timestamp previous-comment) + (<= previous-timestamp orig-point)) + nil) + (t t))))) + (cl-defmethod subed--jump-to-subtitle-id (&context (major-mode subed-vtt-mode) &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. WebVTT IDs are optional. If an ID is not specified, use the timestamp instead. Use the format-specific function for MAJOR-MODE." - (let ((orig-point (point)) found) + (let ((orig-point (point)) + found + (timestamp-line + (format "\\(?:\\`\\|\n[ \t]*\n+\\)\\(\\(%s\\)%s\\|\\(%s\\)\\)" + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing))) (if (stringp sub-id) ;; 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 "\\|\\`\\)\\(" + (let* ((regex (concat "\\(" subed-vtt--regexp-blank-separator "\\|\\`\\)\\(" (regexp-quote sub-id) "\\)[ \n]"))) (goto-char (point-min)) (setq found (re-search-forward regex nil t)) @@ -114,31 +193,46 @@ Use the format-specific function for MAJOR-MODE." (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) - (goto-char (match-end 0))) - (goto-char (point-min))) + ;; are we in a comment? (cond - ((looking-at (concat "\\(" subed-vtt--regexp-timestamp "\\) *--> *" subed-vtt--regexp-timestamp "\\(?:\s+.+\\)?\n")) - ;; no ID, use the timestamp - (point)) - ((looking-at (concat "\\(.*\\)\n" subed-vtt--regexp-timestamp " *--> *" subed-vtt--regexp-timestamp "\\(?:\s+.+\\)?\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 "\\)\\(?:\s+.+\\)?*\n")) nil t) - (progn - (goto-char (or (match-beginning 1) (match-beginning 2) (point))) - (point)) - (goto-char orig-point) - nil)) + ((subed-in-header-p) nil) + ((subed-in-comment-p) + (re-search-backward subed-vtt--regexp-blank-separator nil t) + (when (re-search-forward subed-vtt--regexp-maybe-identifier-and-timing nil t) + (goto-char (match-beginning 1)) + (point))) (t - (goto-char orig-point) - nil))))) + ;; we could be looking at a subtitle ID + (goto-char (line-beginning-position)) + (or + (re-search-backward (format "\\(\\`[ \t\n]*\\|%s\\)" + subed-vtt--regexp-blank-separator) + nil t) + (goto-char (point-min))) + (cond + ((looking-at subed-vtt--regexp-maybe-identifier-and-timing) + (if (< orig-point (match-beginning 1)) + ;; we are at a subtitle boundary, but before the next ID + ;; go backwards to find the ID + (when (re-search-backward subed-vtt--regexp-maybe-identifier-and-timing nil t) + (goto-char (match-beginning 1)) + (point)) + (goto-char (match-beginning 1)) + (point))) + ((and (bobp) + (looking-at (format "[ \t\n]*\\(%s%s\\|%s\\)" + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing))) + ;; at the beginning of the buffer, looking at an ID + (goto-char (match-beginning 1)) + (point)) + (t + (or (re-search-forward subed-vtt--regexp-blank-separator nil t) + (goto-char (point-max))) + (when (re-search-backward subed-vtt--regexp-maybe-identifier-and-timing nil t) + (goto-char (match-beginning 1)) + (point))))))))) (cl-defmethod subed--jump-to-subtitle-time-start (&context (major-mode subed-vtt-mode) &optional sub-id) "Move point to subtitle's start time. @@ -146,9 +240,9 @@ If SUB-ID is not given, use subtitle on point. Return point or nil if no start time could be found. Use the format-specific function for MAJOR-MODE." (when (subed-jump-to-subtitle-id sub-id) - (if (looking-at subed--regexp-timestamp) + (if (looking-at subed-vtt--regexp-timing) (point) - (when (re-search-forward subed--regexp-timestamp nil t) + (when (re-search-forward subed-vtt--regexp-timing nil t) (goto-char (match-beginning 0)) (point))))) @@ -158,7 +252,7 @@ If SUB-ID is not given, use subtitle on point. Return point or nil if no stop time could be found. Use the format-specific function for MAJOR-MODE." (when (subed-jump-to-subtitle-time-start sub-id) - (re-search-forward " *--> *" (line-end-position) t) + (re-search-forward "[ \t]*-->[ \t]*" (line-end-position) t) (when (looking-at subed--regexp-timestamp) (point)))) @@ -176,31 +270,60 @@ Use the format-specific function for MAJOR-MODE." 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. Use the format-specific function for MAJOR-MODE." - (let ((orig-point (point))) + (let* ((orig-point (point)) + (case-fold-search nil)) + ;; go back to the text (subed-jump-to-subtitle-text sub-id) - ;; Look for next separator or end of buffer. - (let ((regex subed-vtt--regexp-separator)) - (if (eolp) - (unless (= (point) orig-point) - (point)) - (if (re-search-forward regex nil t) - (goto-char (match-beginning 0)) - (goto-char (point-max)) - (skip-syntax-backward " ")) - (unless (= (point) orig-point) - (point)))))) + (skip-chars-backward " \t\n") + (cond + ((looking-at (format "%s\\(NOTE[ \t\n]\\|%s%s\\|%s\\)" + subed-vtt--regexp-blank-separator + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing)) + ;; empty subtitle + (forward-line 1) + (point)) + ;; end of buffer + ((save-excursion (skip-chars-forward " \t\n") + (eobp)) + (forward-line 1) + (point)) + (t + ;; scan forward each separator + (catch 'done + (while (re-search-forward subed-vtt--regexp-blank-separator nil t) + (goto-char (match-beginning 0)) + (if (looking-at + (format "%s\\(NOTE[ \t\n]\\|%s%s\\|%s\\)" + subed-vtt--regexp-blank-separator + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing)) + (throw 'done (point)) + (skip-chars-forward " \t\n"))) + (goto-char (point-max)) + (skip-chars-backward " \t\n")))) + (unless (= (point) orig-point) + (point)))) (cl-defmethod subed--forward-subtitle-id (&context (major-mode subed-vtt-mode)) "Move point to next subtitle's ID. Return point or nil if there is no next subtitle. Use the format-specific function for MAJOR-MODE." (let ((orig-point (point))) - (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) + (when (subed-subtitle-id) + (subed-jump-to-subtitle-end)) + (unless (looking-at (format "\\(%s%s\\|%s\\)" + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing)) + (skip-chars-backward " \n\t")) + (if (re-search-forward (format "%s\\(%s%s\\|%s\\)" + subed-vtt--regexp-blank-separator + subed-vtt--regexp-identifier + subed-vtt--regexp-timing + subed-vtt--regexp-timing) nil t) (or (subed-jump-to-subtitle-id) (progn @@ -249,9 +372,11 @@ 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)) + (re-search-backward + (concat subed-vtt--regexp-blank-separator + "\\(NOTE[ \t\n]\\)") + (or (save-excursion (subed-backward-subtitle-end)) (point-min)) t)) + (progn (goto-char (match-beginning 1)) (point)) (goto-char pos) nil))) @@ -315,8 +440,9 @@ point. Use the format-specific function for MAJOR-MODE." (insert (subed-make-subtitle id start stop text comment)) (when (looking-at (concat "\\([[:space:]]*\\|^\\)" subed--regexp-timestamp)) (insert "\n")) - (forward-line -2) - (subed-jump-to-subtitle-text)) + (skip-chars-backward " \t\n") + (subed-jump-to-subtitle-text) + (point)) (cl-defmethod subed--append-subtitle (&context (major-mode subed-vtt-mode) &optional id start stop text comment) "Insert new subtitle after the subtitle at point. diff --git a/tests/test-subed-vtt.el b/tests/test-subed-vtt.el index 0c55e4b9fe..858f2c8904 100644 --- a/tests/test-subed-vtt.el +++ b/tests/test-subed-vtt.el @@ -21,254 +21,343 @@ Baz. (defmacro with-temp-vtt-buffer (&rest body) "Call `subed-vtt--init' in temporary buffer before running BODY." `(with-temp-buffer - (subed-vtt-mode) - (progn ,@body))) + (subed-vtt-mode) + (progn ,@body))) + +;; (defmacro with-temp-vtt-buffer (&rest body) +;; "Call `subed-vtt--init' in temporary buffer before running BODY." +;; `(with-current-buffer (get-buffer-create "*test*") +;; (erase-buffer) +;; (unless (derived-mode-p 'subed-vtt-mode) (subed-vtt-mode)) +;; (display-buffer (current-buffer)) +;; (progn ,@body))) (describe "subed-vtt" - (describe "Getting" - (describe "the subtitle ID" - (it "returns the subtitle ID if it can be found." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:01:01.000") - (expect (subed-subtitle-id) :to-equal "00:01:01.000"))) - (it "returns nil if no subtitle ID can be found." - (with-temp-vtt-buffer - (expect (subed-subtitle-id) :to-equal nil))) - (it "handles extra attributes" + (describe "Detecting" + (describe "whether you're in the file header" + (it "returns t in an empty buffer." (with-temp-vtt-buffer - (insert "WEBVTT - -00:00:01.000 --> 00:00:02.000 align:start position:0% -Hello world") - (expect (subed-subtitle-id) :to-equal "00:00:01.000")))) - (describe "the subtitle ID at playback time" - (it "returns subtitle ID if time is equal to start time." + (expect (subed-in-header-p) :to-be t))) + (it "works at the beginning of the header." (with-temp-vtt-buffer (insert mock-vtt-data) - (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:01:01.000")) - :to-equal "00:01:01.000"))) - (it "returns subtitle ID if time is equal to stop time." + (goto-char (point-min)) + (expect (subed-in-header-p) :to-be t))) + (it "works in the middle of the header." (with-temp-vtt-buffer (insert mock-vtt-data) - (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:02:10.345")) - :to-equal "00:02:02.234"))) - (it "returns subtitle ID if time is between start and stop time." + (goto-char (+ (point-min) 2)) + (expect (subed-in-header-p) :to-be t))) + (it "returns t on the line before a comment." (with-temp-vtt-buffer - (insert mock-vtt-data) - (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:02:05.345")) - :to-equal "00:02:02.234"))) - (it "returns nil if time is before the first subtitle's start time." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (let ((msecs (- (save-excursion - (goto-char (point-min)) - (subed-forward-subtitle-id) - (subed-subtitle-msecs-start)) - 1))) - (expect (subed-subtitle-id-at-msecs msecs) :to-equal nil)))) - (it "returns nil if time is after the last subtitle's start time." + (insert "WEBVTT + +NOTE +This is a comment +") + (re-search-backward "\nNOTE") + (expect (subed-in-header-p) :to-be t))) + (describe "when the buffer starts with a cue timestamp" + (it "returns nil from the timing line." + (with-temp-vtt-buffer + (insert "00:04:02.234 --> 00:04:10.345 +Baz.") + (goto-char (point-min)) + (expect (subed-in-header-p) :to-be nil))) + (it "returns nil from the cue text." + (with-temp-vtt-buffer + (insert "00:04:02.234 --> 00:04:10.345 +Baz.") + (expect (subed-in-header-p) :to-be nil)))) + (it "returns nil at the beginning of a comment." (with-temp-vtt-buffer - (insert mock-vtt-data) - (let ((msecs (+ (save-excursion - (goto-char (point-max)) - (subed-subtitle-msecs-stop)) 1))) - (expect (subed-subtitle-id-at-msecs msecs) :to-equal nil)))) - (it "returns nil if time is between subtitles." + (insert "WEBVTT + +NOTE +This is a comment +") + (re-search-backward "NOTE") + (expect (subed-in-header-p) :to-be nil))) + (it "returns nil in the middle of a comment." (with-temp-vtt-buffer - (insert mock-vtt-data) - (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:01:06.123")) - :to-equal nil)))) - (describe "the subtitle start/stop time" - (it "returns the time in milliseconds." + (insert "WEBVTT + +NOTE +This is a comment +") + (re-search-backward "comment") + (expect (subed-in-header-p) :to-be nil))) + (it "returns nil at the start of an ID." (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-id "00:02:02.234") - (expect (subed-subtitle-msecs-start) :to-equal (+ (* 2 60000) (* 2 1000) 234)) - (expect (subed-subtitle-msecs-stop) :to-equal (+ (* 2 60000) (* 10 1000) 345)))) - (it "handles lack of digits in milliseconds gracefully." + (insert "WEBVTT + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "^1") + (expect (subed-in-header-p) :to-be nil))) + (it "returns nil at the start of a timestamp." (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-id "00:03:03.45") - (expect (save-excursion (subed-jump-to-subtitle-time-start) - (thing-at-point 'line)) :to-equal "00:03:03.45 --> 00:03:15.5\n") - (expect (subed-subtitle-msecs-start) :to-equal (+ (* 3 60 1000) (* 3 1000) 450)) - (expect (subed-subtitle-msecs-stop) :to-equal (+ (* 3 60 1000) (* 15 1000) 500)))) - (it "handles lack of hours in milliseconds gracefully." + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "^0") + (expect (subed-in-header-p) :to-be nil))) + (it "returns nil in the middle of timing information." (with-temp-vtt-buffer - (insert "WEBVTT\n\n01:02.000 --> 03:04.000\nHello\n") - (expect (subed-subtitle-msecs-start) :to-equal (+ (* 1 60 1000) (* 2 1000))) - (expect (subed-subtitle-msecs-stop) :to-equal (+ (* 3 60 1000) (* 4 1000))))) - (it "returns nil if time can't be found." + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "--") + (expect (subed-in-header-p) :to-be nil))) + (it "returns nil in the middle of a cue." (with-temp-vtt-buffer - (expect (subed-subtitle-msecs-start) :to-be nil) - (expect (subed-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-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:01:01.000") - (kill-line) - (expect (subed-subtitle-text) :to-equal ""))) - (it "and at the beginning without a trailing newline." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:01:01.000") - (kill-whole-line) - (expect (subed-subtitle-text) :to-equal ""))) - (it "and in the middle." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:02:02.234") - (kill-line) - (expect (subed-subtitle-text) :to-equal ""))) - (it "and at the end with a trailing newline." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:03:03.45") - (kill-line) - (expect (subed-subtitle-text) :to-equal ""))) - (it "and at the end without a trailing newline." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:03:03.45") - (kill-whole-line) - (expect (subed-subtitle-text) :to-equal ""))) - ) - (describe "when text is not empty" - (it "and has no linebreaks." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:02:02.234") - (expect (subed-subtitle-text) :to-equal "Bar."))) - (it "and has linebreaks." - (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-text "00:02:02.234") - (insert "Bar.\n") - (expect (subed-subtitle-text) :to-equal "Bar.\nBar."))) + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "This") + (expect (subed-in-header-p) :to-be nil)) ) - ) - (describe "the point within the subtitle" - (it "returns the relative point if we can find an ID." + (it "returns nil in the middle of a cue with the text WEBVTT." (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-id "00:02:02.234") - (expect (subed-subtitle-relative-point) :to-equal 0) - (forward-line) - (expect (subed-subtitle-relative-point) :to-equal 30) - (forward-char) - (expect (subed-subtitle-relative-point) :to-equal 31) - (forward-line) - (expect (subed-subtitle-relative-point) :to-equal 35) - (forward-line) - (expect (subed-subtitle-relative-point) :to-equal 0))) - (it "returns nil if we can't find an ID." + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 +WEBVTT +") + (expect (subed-in-header-p) :to-be nil)))) + (describe "whether you're in a comment" + (it "returns nil in the header." (with-temp-vtt-buffer - (insert mock-vtt-data) - (subed-jump-to-subtitle-id "00:01:01.000") - (insert "foo") - (expect (subed-subtitle-relative-point) :to-equal nil))) - ) - (describe "the subtitle start position" - (it "returns the start from inside a subtitle." + (insert "WEBVTT + +NOTE This is a test + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (goto-char (point-min)) + (expect (subed-in-comment-p) :to-be nil))) + (it "returns t at the beginning of a NOTE." (with-temp-vtt-buffer - (insert mock-vtt-data) - (re-search-backward "Bar") - (expect (subed-subtitle-start-pos) :to-equal 45))) - (it "returns the start from the beginning of the line." + (insert "WEBVTT + +NOTE This is a test + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "NOTE") + (expect (subed-in-comment-p) :to-be t))) + (it "returns t in the middle of NOTE." (with-temp-vtt-buffer - (insert mock-vtt-data) - (re-search-backward "00:02:02\\.234") - (expect (subed-subtitle-start-pos) :to-equal 45))) - (it "returns the start of a comment" + (insert "WEBVTT + +NOTE This is a test + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "OTE") + (expect (subed-in-comment-p) :to-be t))) + (it "returns t in the middle of NOTE text." (with-temp-vtt-buffer - (insert mock-vtt-data) - (re-search-backward "00:02:02\\.234") - (insert "NOTE\n\nThis is a comment\n\n") - (expect (subed-subtitle-start-pos) :to-equal 45))))) - (describe "Converting to msecs" - (it "works with numbers." - (expect (with-temp-vtt-buffer (subed-to-msecs 5123)) :to-equal 5123)) - (it "works with numbers as strings." - (expect (with-temp-vtt-buffer (subed-to-msecs "5123")) :to-equal 5123)) - (it "works with timestamps." - (expect (with-temp-vtt-buffer - (subed-to-msecs "00:00:05.124")) :to-equal 5124))) - (describe "Jumping" - (describe "to current subtitle timestamp" - (it "returns timestamp's point when point is already on the timestamp." + (insert "WEBVTT + +NOTE This is a test + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "test") + (expect (subed-in-comment-p) :to-be t))) + (it "returns t in the middle of a multi-line NOTE." (with-temp-vtt-buffer - (insert mock-vtt-data) - (goto-char (point-min)) - (subed-jump-to-subtitle-id "00:01:01.000") - (expect (subed-jump-to-subtitle-time-start) :to-equal (point)) - (expect (looking-at subed--regexp-timestamp) :to-be t) - (expect (match-string 0) :to-equal "00:01:01.000"))) - (it "returns timestamp's point when point is on the text." + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "multiple") + (expect (subed-in-comment-p) :to-be t))) + (it "returns t in an empty line before an ID." (with-temp-vtt-buffer - (insert mock-vtt-data) - (search-backward "Baz.") - (expect (thing-at-point 'word) :to-equal "Baz") - (expect (subed-jump-to-subtitle-time-start) :to-equal 81) - (expect (looking-at subed--regexp-timestamp) :to-be t) - (expect (match-string 0) :to-equal "00:03:03.45"))) - (it "returns timestamp's point when point is between subtitles." + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "\n1") + (expect (subed-in-comment-p) :to-be t))) + (it "returns t in an empty line before a timestamp." (with-temp-vtt-buffer - (insert mock-vtt-data) - (goto-char (point-min)) - (search-forward "Bar.\n") - (expect (thing-at-point 'line) :to-equal "\n") - (expect (subed-jump-to-subtitle-time-start) :to-equal 45) - (expect (looking-at subed--regexp-timestamp) :to-be t) - (expect (match-string 0) :to-equal "00:02:02.234"))) - (it "returns nil if buffer is empty." + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "\n0") + (expect (subed-in-comment-p) :to-be t))) + (it "returns nil at the beginning of an ID." (with-temp-vtt-buffer - (expect (buffer-string) :to-equal "") - (expect (subed-jump-to-subtitle-time-start) :to-equal nil))) - (it "returns timestamp's point when buffer starts with blank lines." + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "^1") + (expect (subed-in-comment-p) :to-be nil))) + (it "returns nil at the beginning of a timestamp." (with-temp-vtt-buffer - (insert (concat "WEBVTT \n \t \n" (replace-regexp-in-string "WEBVTT" "" mock-vtt-data))) - (search-backward "Foo.") - (expect (thing-at-point 'line) :to-equal "Foo.\n") - (expect (subed-jump-to-subtitle-time-start) :to-equal 15) - (expect (looking-at subed--regexp-timestamp) :to-be t) - (expect (match-string 0) :to-equal "00:01:01.000"))) - (it "returns timestamp's point when subtitles are separated with blank lines." + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "^0") + (expect (subed-in-comment-p) :to-be nil))) + (it "returns nil in the middle of timing information." (with-temp-vtt-buffer - (insert mock-vtt-data) - (goto-char (point-min)) - (search-forward "Foo.\n") - (insert " \n \t \n") - (expect (subed-jump-to-subtitle-time-start) :to-equal 9) - (expect (looking-at subed--regexp-timestamp) :to-be t) - (expect (match-string 0) :to-equal "00:01:01.000"))) - (it "works with short timestamps from a comment." + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "--") + (expect (subed-in-comment-p) :to-be nil))) + (it "returns t if there's a comment between the cursor and the previous cue." (with-temp-vtt-buffer - (insert "WEBVTT\n\nNOTE A comment goes here + (insert "WEBVTT -09:34.900 --> 00:09:37.659 -Subtitle 1 +1 +00:00:00.000 --> 00:00:01.000 +This is a subtitle + +NOTE +This is a comment +with multiple lines. + +2 +00:00:00.000 --> 00:00:01.000 +This is another subtitle") + (re-search-backward "multiple") + (expect (subed-in-comment-p) :to-be t))) + (it "returns nil if there's a cue between the cursor and the previous comment." + (with-temp-vtt-buffer + (insert "WEBVTT + +NOTE +This is a comment +with multiple lines. + +1 +00:00:00.000 --> 00:00:01.000 +This is the first subtitle + +2 +00:00:00.000 --> 00:00:01.000 +This is a subtitle +") + (re-search-backward "first") + (expect (subed-in-comment-p) :to-be nil))) + (it "returns nil if there's no comment." + (with-temp-vtt-buffer + (insert "WEBVTT + +1 +00:00:00.000 --> 00:00:01.000 +This is the first subtitle + +2 +00:00:00.000 --> 00:00:01.000 +This is the second subtitle +") + (re-search-backward "second") + (expect (subed-in-comment-p) :to-be nil))))) + (describe "Jumping" + (describe "to subtitle ID" + (describe "in the current subtitle" + (describe "from the header" + (it "returns nil when the next subtitle starts with a timestamp." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (goto-char (point-min)) + (expect (subed-jump-to-subtitle-id) :to-be nil))) + (it "returns nil when the next subtitle starts with a comment." + (with-temp-vtt-buffer + (insert "WEBVTT + +NOTE + +00:01:01.000 --> 00:01:05.123 +Foo. + +00:02:02.000 --> 00:03:05.123 +Bar. + +") + (goto-char (point-min)) + (expect (subed-jump-to-subtitle-id) :to-be nil)) + ) + (it "returns nil when the next subtitle starts with an ID." + (with-temp-vtt-buffer + (insert "WEBVTT + +NOTE + +1 +00:01:01.000 --> 00:01:05.123 +Foo. -00:10:34.900 --> 00:11:37.659 -Subtitle 2") - (re-search-backward "NOTE") - (goto-char (line-beginning-position)) - (expect (subed-jump-to-subtitle-time-start) :to-equal 35))) +00:02:02.000 --> 00:03:05.123 +Bar. - ) - (describe "to subtitle start pos" - (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-start-pos) :to-be nil))) - (it "goes to the ID if specified." - (with-temp-vtt-buffer - (insert "WEBVTT +") + (goto-char (point-min)) + (expect (subed-jump-to-subtitle-id) :to-be nil)))) + (describe "when there is no comment" + (it "goes to the ID if specified." + (with-temp-vtt-buffer + (insert "WEBVTT 1 00:01:01.000 --> 00:01:05.123 @@ -277,12 +366,12 @@ Foo. 00:02:02.234 --> 00:02:10.345 Bar. ") - (re-search-backward "Foo") - (expect (subed-jump-to-subtitle-start-pos) :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 + (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 @@ -290,15 +379,34 @@ Foo. 00:02:02.234 --> 00:02:10.345 Bar. + +00:04:02.234 --> 00:04:10.345 +Baz. + ") - (re-search-backward "Bar") - (expect (subed-jump-to-subtitle-start-pos) :not :to-be nil) - (expect (looking-at "00:02:02.234") :to-be t))) - (it "goes to the comment if there is one." - (with-temp-vtt-buffer - (insert "WEBVTT + (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 there is no header" + (it "goes to the timestamp if there is no ID." + (with-temp-vtt-buffer + (insert "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 "Foo") + (expect (subed-jump-to-subtitle-id) :to-equal 1))) + ) + (describe "when there is a comment" + (it "goes to the ID if specified." + (with-temp-vtt-buffer + (insert "WEBVTT + +NOTE + +Hello world 1 00:01:01.000 --> 00:01:05.123 @@ -307,71 +415,95 @@ Foo. 00:02:02.234 --> 00:02:10.345 Bar. ") - (re-search-backward "Foo") - (expect (subed-jump-to-subtitle-start-pos) :not :to-be nil) - (expect (looking-at "NOTE This is a comment") :to-be t))) - (describe "when called from a comment" - (it "goes to the start of the comment." - (with-temp-vtt-buffer - (insert "WEBVTT - -NOTE -This is a comment + (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. +NOTE + +This is a comment + 00:02:02.234 --> 00:02:10.345 Bar. + +00:04:02.234 --> 00:04:10.345 +Baz. + ") - (re-search-backward "This is a comment") - (expect (subed-jump-to-subtitle-start-pos) :not :to-be nil) - (expect (looking-at "NOTE\nThis is a comment") :to-be t)))))) - (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 + (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 there are multiple blocks" + (it "goes to the ID if specified." + (with-temp-vtt-buffer + (insert "WEBVTT 1 00:01:01.000 --> 00:01:05.123 Foo. +NOTE + +This is a comment + +2 00:02:02.234 --> 00:02:10.345 + +Apparently a subtitle can have multiple comements. + Bar. + +00:04:02.234 --> 00:04:10.345 +Baz. + ") - (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 + (re-search-backward "Bar") + (expect (subed-jump-to-subtitle-id) :not :to-be nil) + (expect (looking-at "2") :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. +NOTE + +This is a comment + 00:02:02.234 --> 00:02:10.345 + +Apparently a subtitle can have multiple comements. + Bar. + +00:04:02.234 --> 00:04:10.345 +Baz. + ") - (re-search-backward "Bar") - (expect (subed-jump-to-subtitle-id) :not :to-be nil) - (expect (looking-at "00:02:02.234") :to-be t))) + (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 +00:00:00.000 --> 00:00:01.000 +Something goes here + NOTE This is a comment -1 +2 00:01:01.000 --> 00:01:05.123 Foo. @@ -380,7 +512,27 @@ 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))) + (expect (looking-at "2\n") :to-be t))) + (it "goes to the ID of the subtitle after the comment even at the NOTE line." + (with-temp-vtt-buffer + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 +Something goes here + +NOTE +This is a comment + +2 +00:01:01.000 --> 00:01:05.123 +Foo. + +00:02:02.234 --> 00:02:10.345 +Bar. +") + (re-search-backward "NOTE") + (expect (subed-jump-to-subtitle-id) :not :to-be nil) + (expect (looking-at "2\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 @@ -421,7 +573,12 @@ Baz. (re-search-backward "This is a comment") (expect (subed-jump-to-subtitle-id) :not :to-be nil) (expect (looking-at (regexp-quote "02:02.234")) :to-be t))) - )) + (it "goes to the timestamp of the last subtitle." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (re-search-backward "00:03:03") + (expect (subed-jump-to-subtitle-id) :not :to-be nil) + (expect (looking-at (regexp-quote "00:03:03")) :to-be t))))) (describe "when given an ID" (it "returns ID's point if wanted time exists." (with-temp-vtt-buffer @@ -465,6 +622,93 @@ Bar. (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 pos" + (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-start-pos) :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-start-pos) :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-start-pos) :not :to-be nil) + (expect (looking-at "00:02:02.234") :to-be t))) + (it "goes to the comment if there is one." + (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 "Foo") + (expect (subed-jump-to-subtitle-start-pos) :not :to-be nil) + (expect (looking-at "NOTE This is a comment") :to-be t))) + (describe "when called from a comment" + (it "goes to the start of 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-start-pos) :not :to-be nil) + (expect (looking-at "NOTE\nThis is a comment") :to-be t))) + (it "goes to the start of 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 "OTE") + (expect (subed-jump-to-subtitle-start-pos) :not :to-be nil) + (expect (looking-at "NOTE\nThis is a comment") :to-be t)))))) + (describe "to subtitle start time" (it "returns start time's point if movement was successful." @@ -508,6 +752,69 @@ Bar. (with-temp-vtt-buffer (expect (subed-jump-to-subtitle-time-stop) :to-equal nil))) ) + (describe "to current subtitle timestamp" + (it "returns timestamp's point when point is already on the timestamp." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (goto-char (point-min)) + (subed-jump-to-subtitle-id "00:01:01.000") + (expect (subed-jump-to-subtitle-time-start) :to-equal (point)) + (expect (looking-at subed--regexp-timestamp) :to-be t) + (expect (match-string 0) :to-equal "00:01:01.000"))) + (it "returns timestamp's point when point is on the text." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (search-backward "Baz.") + (expect (thing-at-point 'word) :to-equal "Baz") + (expect (subed-jump-to-subtitle-time-start) :to-equal 81) + (expect (looking-at subed--regexp-timestamp) :to-be t) + (expect (match-string 0) :to-equal "00:03:03.45"))) + (it "returns timestamp's point when point is between subtitles." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (goto-char (point-min)) + (search-forward "Bar.\n") + (expect (thing-at-point 'line) :to-equal "\n") + (expect (subed-jump-to-subtitle-time-start) :to-equal 45) + (expect (looking-at subed--regexp-timestamp) :to-be t) + (expect (match-string 0) :to-equal "00:02:02.234"))) + (it "returns nil if buffer is empty." + (with-temp-vtt-buffer + (expect (buffer-string) :to-equal "") + (expect (subed-jump-to-subtitle-time-start) :to-equal nil))) + (it "returns timestamp's point when buffer starts with blank lines." + (with-temp-vtt-buffer + (insert (concat "WEBVTT \n \t \n" (replace-regexp-in-string "WEBVTT" "" mock-vtt-data))) + (search-backward "Foo.") + (expect (thing-at-point 'line) :to-equal "Foo.\n") + (expect (subed-jump-to-subtitle-time-start) :to-equal 15) + (expect (looking-at subed--regexp-timestamp) :to-be t) + (expect (match-string 0) :to-equal "00:01:01.000"))) + ;; I'm not sure this is actually supported by the spec. + ;; (it "returns timestamp's point when subtitles are separated with blank lines." + ;; (with-temp-vtt-buffer + ;; (insert mock-vtt-data) + ;; (goto-char (point-min)) + ;; (search-forward "Foo.\n") + ;; (insert " \n \t \n") + ;; (expect (subed-jump-to-subtitle-time-start) :to-equal 9) + ;; (expect (looking-at subed--regexp-timestamp) :to-be t) + ;; (expect (match-string 0) :to-equal "00:01:01.000"))) + (it "works with short timestamps from a comment." + (with-temp-vtt-buffer + (insert "WEBVTT\n\nNOTE A comment goes here + +09:34.900 --> 00:09:37.659 +Subtitle 1 + +00:10:34.900 --> 00:11:37.659 +Subtitle 2") + (re-search-backward "NOTE") + (goto-char (line-beginning-position)) + (expect (subed-jump-to-subtitle-time-start) :to-equal 35))) + + ) + (describe "to subtitle text" (it "returns subtitle text's point if movement was successful." (with-temp-vtt-buffer @@ -537,6 +844,15 @@ Subtitle 2") (re-search-backward "NOTE") (goto-char (line-beginning-position)) (expect (subed-jump-to-subtitle-text) :to-equal 62))) + (it "works even when the subtitle has no text and is the only subtitle." + (with-temp-vtt-buffer + (insert "00:00:00.000 --> 00:00:01.000 + +") + (goto-char (point-min)) + (subed-jump-to-subtitle-text) + (expect (looking-back "\\.000\n") :to-be t)) + ) ) (describe "to end of subtitle text" (it "returns point if subtitle end can be found." @@ -608,6 +924,20 @@ Subtitle 2") (backward-char) (expect (subed-jump-to-subtitle-end) :to-be 98) (expect (looking-at "^$") :to-be t))) + (it "handles linebreaks at the beginning." + (with-temp-vtt-buffer + (insert "WEBVTT +Kind: captions +Language: en + +00:00:02.459 --> 00:00:05.610 align:start position:0% + +Hello world +") + (goto-char (point-min)) + (subed-forward-subtitle-id) + (subed-jump-to-subtitle-end) + (expect (point) :to-be-greater-than (- (point-max) 2)))) (it "works with short timestamps from a comment." (with-temp-vtt-buffer (insert "WEBVTT\n\nNOTE A comment goes here @@ -630,11 +960,63 @@ This is first subtitle. 123456789 2 -00:00:01.000 --> 00:00:0.,000 +00:00:01.000 --> 00:02:00.000 This is second subtitle. ") (re-search-backward "This is first") - (expect (subed-jump-to-subtitle-end) :to-be 74)))) + (expect (subed-jump-to-subtitle-end) :to-be 74))) + + (it "works with multiple blocks in a subtitle." + (with-temp-vtt-buffer + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 + +A subtitle can consist + +of multiple blocks + +00:00:01.000 --> 00:02:00.000 +This is the second subtitle. +") + (re-search-backward "A subtitle can") + (expect (subed-jump-to-subtitle-end) :to-be 82))) + (it "ignores ending blank lines and spaces." + (with-temp-vtt-buffer + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 + +A subtitle can consist + +of multiple blocks + + + + + +00:00:01.000 --> 00:02:00.000 +This is the second subtitle. +") + (re-search-backward "A subtitle can") + (expect (subed-jump-to-subtitle-end) :to-be 82))) + (it "ignores ending blank lines at the end of the buffer." + (with-temp-vtt-buffer + (insert "WEBVTT + +00:00:00.000 --> 00:00:01.000 + +A subtitle can consist + +of multiple blocks + + + + + +") + (re-search-backward "A subtitle can") + (expect (subed-jump-to-subtitle-end) :to-be 82)))) (describe "to next subtitle ID" (it "returns point when there is a next subtitle." (with-temp-vtt-buffer @@ -730,6 +1112,19 @@ This is second subtitle. (expect (looking-at (regexp-quote "00:03:03.45")) :to-be t) (expect (subed-forward-subtitle-text) :to-be nil) (expect (looking-at (regexp-quote "00:03:03.45")) :to-be t))) + (it "handles blank lines at the start of a caption." + (with-temp-vtt-buffer + (insert "WEBVTT +Kind: captions +Language: en + +00:00:02.459 --> 00:00:05.610 align:start position:0% + +hi<00:00:03.459><c> welcome</c><00:00:03.850><c> to</c><00:00:03.999><c> another</c><00:00:04.149><c> episode</c><00:00:04.509><c> of</c><00:00:05.020><c> Emacs</c> +") + (goto-char (point-min)) + (subed-forward-subtitle-text) + (expect (looking-at "\nhi") :to-be t))) ) (describe "to previous subtitle text" (it "returns point when there is a previous subtitle." @@ -846,6 +1241,190 @@ This is second subtitle. (expect (subed-backward-subtitle-time-stop) :to-be nil) (expect (looking-at (regexp-quote "00:01:01.000")) :to-be t))) )) + (describe "Getting" + (describe "the subtitle ID" + (it "returns the subtitle ID if it can be found." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (re-search-backward "00:01:01.000") + (expect (subed-subtitle-id) :to-equal "00:01:01.000"))) + (it "returns nil if no subtitle ID can be found." + (with-temp-vtt-buffer + (expect (subed-subtitle-id) :to-equal nil))) + (it "handles extra attributes" + (with-temp-vtt-buffer + (insert "WEBVTT + +00:00:01.000 --> 00:00:02.000 align:start position:0% +Hello world") + (expect (subed-subtitle-id) :to-equal "00:00:01.000")))) + (describe "the subtitle ID at playback time" + (it "returns subtitle ID if time is equal to start time." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:01:01.000")) + :to-equal "00:01:01.000"))) + (it "returns subtitle ID if time is equal to stop time." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:02:10.345")) + :to-equal "00:02:02.234"))) + (it "returns subtitle ID if time is between start and stop time." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:02:05.345")) + :to-equal "00:02:02.234"))) + (it "returns nil if time is before the first subtitle's start time." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (let ((msecs (- (save-excursion + (goto-char (point-min)) + (subed-forward-subtitle-id) + (subed-subtitle-msecs-start)) + 1))) + (expect (subed-subtitle-id-at-msecs msecs) :to-equal nil)))) + (it "returns nil if time is after the last subtitle's start time." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (let ((msecs (+ (save-excursion + (goto-char (point-max)) + (subed-subtitle-msecs-stop)) 1))) + (expect (subed-subtitle-id-at-msecs msecs) :to-equal nil)))) + (it "returns nil if time is between subtitles." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (expect (subed-subtitle-id-at-msecs (subed-timestamp-to-msecs "00:01:06.123")) + :to-equal nil)))) + (describe "the subtitle start/stop time" + (it "returns the time in milliseconds." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-id "00:02:02.234") + (expect (subed-subtitle-msecs-start) :to-equal (+ (* 2 60000) (* 2 1000) 234)) + (expect (subed-subtitle-msecs-stop) :to-equal (+ (* 2 60000) (* 10 1000) 345)))) + (it "handles lack of digits in milliseconds gracefully." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-id "00:03:03.45") + (expect (save-excursion (subed-jump-to-subtitle-time-start) + (thing-at-point 'line)) :to-equal "00:03:03.45 --> 00:03:15.5\n") + (expect (subed-subtitle-msecs-start) :to-equal (+ (* 3 60 1000) (* 3 1000) 450)) + (expect (subed-subtitle-msecs-stop) :to-equal (+ (* 3 60 1000) (* 15 1000) 500)))) + (it "handles lack of hours in milliseconds gracefully." + (with-temp-vtt-buffer + (insert "WEBVTT\n\n01:02.000 --> 03:04.000\nHello\n") + (expect (subed-subtitle-msecs-start) :to-equal (+ (* 1 60 1000) (* 2 1000))) + (expect (subed-subtitle-msecs-stop) :to-equal (+ (* 3 60 1000) (* 4 1000))))) + (it "returns nil if time can't be found." + (with-temp-vtt-buffer + (expect (subed-subtitle-msecs-start) :to-be nil) + (expect (subed-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-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:01:01.000") + (kill-line) + (expect (subed-subtitle-text) :to-equal ""))) + (it "and at the beginning without a trailing newline." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:01:01.000") + (kill-whole-line) + (expect (subed-subtitle-text) :to-equal ""))) + (it "and in the middle." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:02:02.234") + (kill-line) + (expect (subed-subtitle-text) :to-equal ""))) + (it "and at the end with a trailing newline." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:03:03.45") + (kill-line) + (expect (subed-subtitle-text) :to-equal ""))) + (it "and at the end without a trailing newline." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:03:03.45") + (kill-whole-line) + (expect (subed-subtitle-text) :to-equal ""))) + ) + (describe "when text is not empty" + (it "handles no linebreaks." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:02:02.234") + (expect (subed-subtitle-text) :to-equal "Bar."))) + (it "handles linebreaks." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-text "00:02:02.234") + (insert "Bar.\n") + (expect (subed-subtitle-text) :to-equal "Bar.\nBar."))) + (it "handles linebreaks at the beginning." + (with-temp-vtt-buffer + (insert "WEBVTT +Kind: captions +Language: en + +00:00:02.459 --> 00:00:05.610 align:start position:0% + +Hello world +") + (subed-jump-to-subtitle-text "00:00:02.459") + (expect (subed-subtitle-text) :to-equal "\nHello world"))) + ) + ) + (describe "the point within the subtitle" + (it "returns the relative point if we can find an ID." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-id "00:02:02.234") + (expect (subed-subtitle-relative-point) :to-equal 0) + (forward-line) + (expect (subed-subtitle-relative-point) :to-equal 30) + (forward-char) + (expect (subed-subtitle-relative-point) :to-equal 31) + (forward-line) + (expect (subed-subtitle-relative-point) :to-equal 35) + (forward-line) + (expect (subed-subtitle-relative-point) :to-equal 0))) + (it "returns nil if we can't find an ID." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (subed-jump-to-subtitle-id "00:01:01.000") + (insert "foo") + (expect (subed-subtitle-relative-point) :to-equal nil))) + ) + (describe "the subtitle start position" + (it "returns the start from inside a subtitle." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (re-search-backward "Bar") + (expect (subed-subtitle-start-pos) :to-equal 45))) + (it "returns the start from the beginning of the line." + (with-temp-vtt-buffer + (insert mock-vtt-data) + (re-search-backward "00:02:02\\.234") + (expect (subed-subtitle-start-pos) :to-equal 45))) + (it "returns the start of a comment" + (with-temp-vtt-buffer + (insert mock-vtt-data) + (re-search-backward "00:02:02\\.234") + (insert "NOTE\n\nThis is a comment\n\n") + (expect (subed-subtitle-start-pos) :to-equal 45))))) + (describe "Converting to msecs" + (it "works with numbers." + (expect (with-temp-vtt-buffer (subed-to-msecs 5123)) :to-equal 5123)) + (it "works with numbers as strings." + (expect (with-temp-vtt-buffer (subed-to-msecs "5123")) :to-equal 5123)) + (it "works with timestamps." + (expect (with-temp-vtt-buffer + (subed-to-msecs "00:00:05.124")) :to-equal 5124))) (describe "Setting start/stop time" (it "of current subtitle updates it." @@ -924,22 +1503,22 @@ This is second subtitle. (describe "Inserting a subtitle" (describe "in an empty buffer" (describe "before" - (it "passing nothing." + (it "creates a cue with default values." (with-temp-vtt-buffer (expect (subed-prepend-subtitle) :to-equal 31) (expect (buffer-string) :to-equal (concat "00:00:00.000 --> 00:00:01.000\n\n")) (expect (point) :to-equal 31))) - (it "passing start time." + (it "creates a cue with a start time." (with-temp-vtt-buffer (expect (subed-prepend-subtitle nil 60000) :to-equal 31) (expect (buffer-string) :to-equal (concat "00:01:00.000 --> 00:01:01.000\n\n")) (expect (point) :to-equal 31))) - (it "passing start time and stop time." + (it "creates a cue with a start time and stop time." (with-temp-vtt-buffer (expect (subed-prepend-subtitle nil 60000 65000) :to-equal 31) (expect (buffer-string) :to-equal (concat "00:01:00.000 --> 00:01:05.000\n\n")) (expect (point) :to-equal 31))) - (it "passing start time, stop time and text." + (it "creates a cue with a start time, stop time and text." (with-temp-vtt-buffer (expect (subed-prepend-subtitle nil 60000 65000 "Foo. bar\nbaz.") :to-equal 31) (expect (buffer-string) :to-equal (concat "00:01:00.000 --> 00:01:05.000\n" @@ -1204,20 +1783,20 @@ This is second subtitle. ) (describe "before a comment" (it "inserts before the comment." - (with-temp-vtt-buffer - (insert (concat "00:00:01.000 --> 00:00:02.000\n" - "Foo.\n\n" - "NOTE comment\n\n00:00:05.000 --> 00:00:06.000\n" - "Bar.\n")) - (subed-jump-to-subtitle-time-start "00:00:01.000") - (expect (subed-append-subtitle nil 2500 4000 "Baz.") :to-equal 67) - (expect (buffer-string) :to-equal (concat "00:00:01.000 --> 00:00:02.000\n" - "Foo.\n\n" - "00:00:02.500 --> 00:00:04.000\n" - "Baz.\n\n" - "NOTE comment\n\n00:00:05.000 --> 00:00:06.000\n" - "Bar.\n")) - (expect (point) :to-equal 67)) + (with-temp-vtt-buffer + (insert (concat "00:00:01.000 --> 00:00:02.000\n" + "Foo.\n\n" + "NOTE comment\n\n00:00:05.000 --> 00:00:06.000\n" + "Bar.\n")) + (subed-jump-to-subtitle-time-start "00:00:01.000") + (expect (subed-append-subtitle nil 2500 4000 "Baz.") :to-equal 67) + (expect (buffer-string) :to-equal (concat "00:00:01.000 --> 00:00:02.000\n" + "Foo.\n\n" + "00:00:02.500 --> 00:00:04.000\n" + "Baz.\n\n" + "NOTE comment\n\n00:00:05.000 --> 00:00:06.000\n" + "Bar.\n")) + (expect (point) :to-equal 67)) ) ) (it "when point is on empty text." @@ -1597,6 +2176,30 @@ This is second subtitle. (it "uses the right format" (with-temp-vtt-buffer (expect (subed-msecs-to-timestamp 1401) :to-equal "00:00:01.401")))) + (describe "Getting the list of subtitles" + (it "ignores things that look like comments in cue text." + (with-temp-vtt-buffer + (insert "WEBVTT + +NOTE this is real comment that should be ignored + +00:00:00.000 --> 00:00:01.000 +NOTE text + +NOTE +this is also a real comment that should be ignored +this is also a real comment that should be ignored + +00:00:01.000 --> 00:00:02.000 +NOTE text +NOTE text2") + (let ((list (subed-subtitle-list))) + (expect (elt (elt list 0) 3) :to-equal "NOTE text") + (expect (elt (elt list 0) 4) :to-equal "this is real comment that should be ignored") + (expect (elt (elt list 1) 3) :to-equal "NOTE text\nNOTE text2") + (expect (elt (elt list 1) 4) :to-equal "this is also a real comment that should be ignored\nthis is also a real comment that should be ignored"))) + ) + ) (describe "Working with comments" (before-each (setq mock-vtt-comments-data @@ -1834,7 +2437,21 @@ Again") (let (result) (subed-for-each-subtitle (point-min) (point-max) nil (add-to-list 'result (point))) - (expect (length result) :to-equal 3))))) + (expect (length result) :to-equal 3)))) + (it "handles blank lines at the start of a caption." + (with-temp-vtt-buffer + (insert "WEBVTT +Kind: captions +Language: en + +00:00:02.459 --> 00:00:05.610 align:start position:0% + +hi<00:00:03.459><c> welcome</c><00:00:03.850><c> to</c><00:00:03.999><c> another</c><00:00:04.149><c> episode</c><00:00:04.509><c> of</c><00:00:05.020><c> Emacs</c> +") + (let (result) + (subed-for-each-subtitle (point-min) (point-max) nil + (push (point) result)) + (expect (length result) :to-equal 1))))) (describe "backwards" (it "handles headers." (with-temp-vtt-buffer diff --git a/tests/test-subed-word-data.el b/tests/test-subed-word-data.el index 9b7bd4efd3..56fd28b5e5 100644 --- a/tests/test-subed-word-data.el +++ b/tests/test-subed-word-data.el @@ -5,6 +5,30 @@ (require 'subed-word-data) (describe "subed-word-data" + (it "gets word data from YouTube VTTs." + (let ((words (subed-word-data--extract-words-from-youtube-vtt + "WEBVTT +Kind: captions +Language: en + +00:00:02.459 --> 00:00:05.610 align:start position:0% + +hi<00:00:03.459><c> welcome</c><00:00:03.850><c> to</c><00:00:03.999><c> another</c><00:00:04.149><c> episode</c><00:00:04.509><c> of</c><00:00:05.020><c> Emacs</c> + +00:00:05.610 --> 00:00:05.620 align:start position:0% +hi welcome to another episode of Emacs + + +00:00:05.620 --> 00:00:07.860 align:start position:0% +hi welcome to another episode of Emacs +chat<00:00:05.950><c> i'm</c><00:00:06.160><c> sasha</c><00:00:06.520><c> schewe</c><00:00:06.939><c> and</c><00:00:07.149><c> today</c><00:00:07.450><c> we</c><00:00:07.660><c> have</c> + +00:00:07.860 --> 00:00:07.870 align:start position:0% +chat i'm sasha schewe and today we have + + +" t))) +words)) (describe "Finding approximate matches" (it "handles early oopses." (let ((result (subed-word-data-find-approximate-match @@ -49,6 +73,6 @@ (expect (car result) :to-be-less-than 0.001) (expect (string-join (cdr result) " ") :to-equal "We're lucky if we can get an exact match."))) -) + ) )