branch: elpa/subed commit a1c61d7d4d303dcc6fe7b7dbd20264cda146b639 Author: Sacha Chua <sa...@sachachua.com> Commit: Sacha Chua <sa...@sachachua.com>
Parse and retain comments in VTT files, and simplify creating files * subed/subed-common.el (subtitle-comment): New function to get the comment before the current subtitle. (make-subtitle): Add comment. (prepend-subtitle): Add comment. (append-subtitle): Add comment. (subed-subtitle): New function. (subed-create-file): New function to simplify creating a file with subtitles in it. (subed-convert): Handle buffers that are not visiting files. Use subed-create-file if visiting a file. * subed/subed-vtt.el (subed--make-subtitle): Add comment. (subed--prepend-subtitle): Add comment. (subed--append-subtitle): Add comment. (subed--subtitle-comment): Parse comments before the current subtitle. (subed-vtt--format-comment): New function. (subed--sanitize-format): Do not remove comments when sanitizing. * subed/subed-ass.el, subed/subed-srt.el, subed/subed-tsv.el (subed--make-subtitle): Add comment. (subed--prepend-subtitle): Add comment. (subed--append-subtitle): Add comment. * tests/test-subed-common.el ("COMMON"): Add comment to subtitle list output. * tests/test-subed-vtt.el ("VTT"): Test that comments are retained. --- NEWS.org | 17 +++++++++ subed/subed-ass.el | 11 +++--- subed/subed-common.el | 87 ++++++++++++++++++++++++++++++---------------- subed/subed-srt.el | 6 ++-- subed/subed-tsv.el | 15 ++++---- subed/subed-vtt.el | 42 +++++++++++++++++----- subed/subed.el | 2 +- tests/test-subed-common.el | 11 +++--- tests/test-subed-vtt.el | 15 +++++++- 9 files changed, 148 insertions(+), 58 deletions(-) diff --git a/NEWS.org b/NEWS.org index 2ca917fd27..f2f17e03ce 100644 --- a/NEWS.org +++ b/NEWS.org @@ -2,6 +2,22 @@ * subed news +** Version 1.0.22 - 2022-11-17 - Sacha Chua + +VTT comments are now parsed and returned as part of ~subed-subtitle~ +and ~subed-subtitle-list~. This makes it easier to build workflows +that use the comment information, such as adding NOTE lines for +chapters and then creating a new file based on those lines and the +subtitles following them. + +A new function ~subed-create-file~ helps create a file with a list of +subtitles. + +Sanitizing VTT files with ~subed-sanitize~ should retain comments now. + +~subed-convert~ should now create a buffer instead of a file if the +source is a buffer that isn't a file. + ** Version 1.0.21 - 2022-11-16 - Sacha Chua - subed-align-options is a new variable that will be passed to aeneas @@ -143,3 +159,4 @@ you have any code that refers to functions like =subed-vtt--timestamp-to-msecs=, you will need to change your code to use generic functions such as =subed-timestamp-to-msecs=. + diff --git a/subed/subed-ass.el b/subed/subed-ass.el index b31a2a4638..f2ca52ccd9 100644 --- a/subed/subed-ass.el +++ b/subed/subed-ass.el @@ -205,12 +205,13 @@ format-specific function for MAJOR-MODE." ;;; Manipulation -(cl-defmethod subed--make-subtitle (&context (major-mode subed-ass-mode) &optional id start stop text) +(cl-defmethod subed--make-subtitle (&context (major-mode subed-ass-mode) &optional id start stop text comment) "Generate new subtitle string. ID, START default to 0. STOP defaults to (+ START `subed-subtitle-spacing') TEXT defaults to an empty string. +COMMENT is ignored. A newline is appended to TEXT, meaning you'll get two trailing newlines if TEXT is nil or empty. Use the format-specific @@ -218,15 +219,16 @@ function for MAJOR-MODE." (format "Dialogue: 0,%s,%s,Default,,0,0,0,,%s\n" (subed-msecs-to-timestamp (or start 0)) (subed-msecs-to-timestamp (or stop (+ (or start 0) - subed-default-subtitle-length))) + subed-default-subtitle-length))) (replace-regexp-in-string "\n" "\\n" (or text "")))) -(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-ass-mode) &optional id start stop text) +(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-ass-mode) &optional id start stop text comment) "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. +COMMENT is ignored. Move point to the text of the inserted subtitle. Return new point. Use the format-specific function for MAJOR-MODE." @@ -235,12 +237,13 @@ point. Use the format-specific function for MAJOR-MODE." (forward-line -1) (subed-jump-to-subtitle-text)) -(cl-defmethod subed--append-subtitle (&context (major-mode subed-ass-mode) &optional id start stop text) +(cl-defmethod subed--append-subtitle (&context (major-mode subed-ass-mode) &optional id start stop text comment) "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. +COMMENT is ignored. Move point to the text of the inserted subtitle. Return new point. Use the format-specific function for MAJOR-MODE." diff --git a/subed/subed-common.el b/subed/subed-common.el index 62c1267d1a..84c17f6487 100644 --- a/subed/subed-common.el +++ b/subed/subed-common.el @@ -257,6 +257,10 @@ If SUB-ID is not given, set the text of the current subtitle." (when start-point (- (point) start-point)))) +(subed-define-generic-function subtitle-comment (&optional sub-id) + "Return the comment preceding this subtitle." + nil) + (subed-define-generic-function set-subtitle-time-start (msecs &optional sub-id) "Set subtitle start time to MSECS milliseconds. @@ -283,31 +287,34 @@ Return the new subtitle stop time in milliseconds." (replace-match (save-match-data (subed-msecs-to-timestamp msecs)))))) -(subed-define-generic-function make-subtitle (&optional id start stop text) +(subed-define-generic-function make-subtitle (&optional id start stop text comment) "Generate new subtitle string. ID, START default to 0. STOP defaults to (+ START `subed-subtitle-spacing') -TEXT defaults to an empty string." +TEXT defaults to an empty string. +COMMENT defaults to nil." (interactive "P")) -(subed-define-generic-function prepend-subtitle (&optional id start stop text) +(subed-define-generic-function prepend-subtitle (&optional id start stop text comment) "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. +COMMENT defaults to nil. Move point to the text of the inserted subtitle. Return new point." (interactive "P")) -(subed-define-generic-function append-subtitle (&optional id start stop text) +(subed-define-generic-function append-subtitle (&optional id start stop text comment) "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. +COMMENT defaults to nil. Move point to the text of the inserted subtitle. Return new point." @@ -347,9 +354,19 @@ Otherwise, initialize the mode based on the filename." (subed-tsv-mode)))) (subed-subtitle-list)))) +(defun subed-subtitle () + "Return the subtitle at point as a list. +The list is of the form (id start stop text comment)." + (list + (subed-subtitle-id) + (subed-subtitle-msecs-start) + (subed-subtitle-msecs-stop) + (subed-subtitle-text) + (subed-subtitle-comment))) + (defun subed-subtitle-list (&optional beg end) "Return the subtitles from BEG to END as a list. -The list will contain entries of the form (id start stop text). +The list will contain entries of the form (id start stop text comment). If BEG and END are not specified, use the whole buffer." (let (result) (subed-for-each-subtitle @@ -357,14 +374,7 @@ If BEG and END are not specified, use the whole buffer." (or end (point-max)) nil (when (subed-subtitle-msecs-start) - (setq result - (cons - (list - (subed-subtitle-id) - (subed-subtitle-msecs-start) - (subed-subtitle-msecs-stop) - (subed-subtitle-text)) - result)))) + (setq result (cons (subed-subtitle) result)))) (nreverse result))) (subed-define-generic-function sanitize () @@ -1920,28 +1930,47 @@ If LIST is nil, use the subtitles in the current buffer." (interactive) nil) +(defun subed-create-file (filename subtitles &optional ok-if-exists mode) + "Create FILENAME, set it to MODE, and prepopulate it with SUBTITLES. +Overwrites existing files." + (when (and (file-exists-p filename) (not ok-if-exists)) + (error "File %s already exists." filename)) + (let ((subed-auto-play-media nil)) + (find-file filename) + (erase-buffer) + (if mode (funcall mode)) + (subed-auto-insert) + (mapc (lambda (sub) (apply #'subed-append-subtitle nil (cdr sub))) subtitles))) + (defun subed-convert (format) "Create a buffer with the current subtitles converted to FORMAT. You may need to add some extra information to the buffer." (interactive (list (completing-read "To format: " '("VTT" "SRT" "ASS" "TSV" "TXT")))) (let* ((subtitles (subed-subtitle-list)) - (new-filename (concat (file-name-base (or (buffer-file-name) (buffer-name))) "." (downcase format))) - buf) - (when (or (not (file-exists-p new-filename)) - (yes-or-no-p (format "%s exists. Overwrite? " new-filename))) - (find-file new-filename) + (new-filename (concat (file-name-base (or (buffer-file-name) (buffer-name))) "." + (downcase format))) + (mode-func (pcase format + ("VTT" (require 'subed-vtt) 'subed-vtt-mode) + ("SRT" (require 'subed-srt) 'subed-srt-mode) + ("ASS" (require 'subed-ass) 'subed-ass-mode) + ("TSV" (require 'subed-tsv) 'subed-tsv-mode)))) + (if (buffer-file-name) + ;; Create a new file + (when (or (not (file-exists-p new-filename)) + (yes-or-no-p (format "%s exists. Overwrite? " new-filename))) + (if (string= format "TXT") + (progn + (with-temp-file new-filename + (insert (mapconcat (lambda (o) (elt o 3)) subtitles "\n"))) + (find-file new-filename)) + (subed-create-file new-filename subtitles t mode-func)) + (current-buffer)) + ;; Create a temporary buffer + (switch-to-buffer (get-buffer-create new-filename)) (erase-buffer) - (save-excursion - (if (string= format "TXT") - (insert (mapconcat (lambda (o) (elt o 3)) subtitles "\n")) - (pcase format - ("VTT" (require 'subed-vtt) (subed-vtt-mode)) - ("SRT" (require 'subed-srt) (subed-srt-mode)) - ("ASS" (require 'subed-ass) (subed-ass-mode)) - ("TSV" (require 'subed-tsv) (subed-tsv-mode))) - (subed-auto-insert) - (mapc (lambda (sub) (apply #'subed-append-subtitle nil (cdr sub))) subtitles) - (subed-regenerate-ids))) + (funcall mode-func) + (subed-auto-insert) + (mapc (lambda (sub) (apply #'subed-append-subtitle nil (cdr sub))) subtitles) (current-buffer)))) (provide 'subed-common) diff --git a/subed/subed-srt.el b/subed/subed-srt.el index eeb52e5b44..51412b8a20 100644 --- a/subed/subed-srt.el +++ b/subed/subed-srt.el @@ -196,7 +196,7 @@ Use the format-specific function for MAJOR-MODE." ;;; Manipulation -(cl-defmethod subed--make-subtitle (&context (major-mode subed-srt-mode) &optional id start stop text) +(cl-defmethod subed--make-subtitle (&context (major-mode subed-srt-mode) &optional id start stop text comment) "Generate new subtitle string. ID, START default to 0. @@ -213,7 +213,7 @@ function for MAJOR-MODE." subed-default-subtitle-length))) (or text ""))) -(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-srt-mode) &optional id start stop text) +(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-srt-mode) &optional id start stop text comment) "Insert new subtitle before the subtitle at point. ID and START default to 0. @@ -229,7 +229,7 @@ Return new point. Use the format-specific function for MAJOR-MODE." (forward-line -2) (subed-jump-to-subtitle-text)) -(cl-defmethod subed--append-subtitle (&context (major-mode subed-srt-mode) &optional id start stop text) +(cl-defmethod subed--append-subtitle (&context (major-mode subed-srt-mode) &optional id start stop text comment) "Insert new subtitle after the subtitle at point. ID, START default to 0. diff --git a/subed/subed-tsv.el b/subed/subed-tsv.el index b9f5295d61..33ee1784f3 100644 --- a/subed/subed-tsv.el +++ b/subed/subed-tsv.el @@ -348,12 +348,13 @@ Use the format-specific function for MAJOR-MODE." (when (looking-at subed-tsv--regexp-timestamp) (replace-match (subed-msecs-to-timestamp msecs)))))) -(cl-defmethod subed--make-subtitle (&context (major-mode subed-tsv-mode) &optional id start stop text) +(cl-defmethod subed--make-subtitle (&context (major-mode subed-tsv-mode) &optional id start stop text comment) "Generate new subtitle string. ID, START default to 0. STOP defaults to (+ START `subed-subtitle-spacing') TEXT defaults to an empty string. +COMMENT is ignored. A newline is appended to TEXT, meaning you'll get two trailing newlines if TEXT is nil or empty. @@ -361,30 +362,32 @@ Use the format-specific function for MAJOR-MODE." (format "%s\t%s\t%s\n" (subed-msecs-to-timestamp (or start 0)) (subed-msecs-to-timestamp (or stop (+ (or start 0) - subed-default-subtitle-length))) + subed-default-subtitle-length))) (replace-regexp-in-string "\n" " " (or text "")))) -(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-tsv-mode) &optional id start stop text) +(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-tsv-mode) &optional id start stop text comment) "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. +COMMENT is ignored. 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) - (insert (subed-make-subtitle id start stop text)) + (insert (subed-make-subtitle id start stop text comment)) (forward-line -1) (subed-jump-to-subtitle-text)) -(cl-defmethod subed--append-subtitle (&context (major-mode subed-tsv-mode) &optional id start stop text) +(cl-defmethod subed--append-subtitle (&context (major-mode subed-tsv-mode) &optional id start stop text comment) "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. +COMMENT is ignored. Move point to the text of the inserted subtitle. Return new point. @@ -393,7 +396,7 @@ Use the format-specific function for MAJOR-MODE." ;; Point is on last subtitle or buffer is empty (subed-jump-to-subtitle-end) (unless (bolp) (insert "\n"))) - (insert (subed-make-subtitle id start stop text)) + (insert (subed-make-subtitle id start stop text comment)) (forward-line -1) (subed-jump-to-subtitle-text)) diff --git a/subed/subed-vtt.el b/subed/subed-vtt.el index 6dd8842775..6d77ad2693 100644 --- a/subed/subed-vtt.el +++ b/subed/subed-vtt.el @@ -102,6 +102,18 @@ 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)) + (prev-end (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) @@ -223,7 +235,19 @@ format-specific function for MAJOR-MODE." ;;; Manipulation -(cl-defmethod subed--make-subtitle (&context (major-mode subed-vtt-mode) &optional id start stop text) +(defun subed-vtt--format-comment (comment) + "Return COMMENT formatted for insertion. +If COMMENT starts with NOTE, keep it as is. If not, add a NOTE header to it. +Make sure COMMENT ends with a blank line." + (cond ((null comment) "") + ((string-match "\\`NOTE" + (concat comment + (if (string-match "\n\n\\'" comment) + "" "\n\n")))) + ((string-match "\n" comment) (concat "NOTE\n" comment "\n\n")) + (t (concat "NOTE " comment)))) + +(cl-defmethod subed--make-subtitle (&context (major-mode subed-vtt-mode) &optional id start stop text comment) "Generate new subtitle string. ID, START default to 0. @@ -233,13 +257,14 @@ 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. Use the format-specific function for MAJOR-MODE." - (format "%s --> %s\n%s\n" + (format "%s%s --> %s\n%s\n" + (subed-vtt--format-comment comment) (subed-msecs-to-timestamp (or start 0)) (subed-msecs-to-timestamp (or stop (+ (or start 0) subed-default-subtitle-length))) (or text ""))) -(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-vtt-mode) &optional id start stop text) +(cl-defmethod subed--prepend-subtitle (&context (major-mode subed-vtt-mode) &optional id start stop text comment) "Insert new subtitle before the subtitle at point. ID and START default to 0. @@ -249,13 +274,13 @@ 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) - (insert (subed-make-subtitle id start stop text)) + (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)) -(cl-defmethod subed--append-subtitle (&context (major-mode subed-vtt-mode) &optional id start stop text) +(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. ID, START default to 0. @@ -274,7 +299,7 @@ point. Use the format-specific function for MAJOR-MODE." (save-excursion (insert ?\n))) ;; Move to end of separator (goto-char (match-end 0))) - (insert (subed-make-subtitle id start stop text)) + (insert (subed-make-subtitle id start stop text comment)) (unless (eolp) ;; Complete separator with another newline unless we inserted at the end (insert ?\n)) @@ -319,13 +344,14 @@ Use the format-specific function for MAJOR-MODE." (while (looking-at "\\`\n+") (replace-match "")) - ;; Replace separators between subtitles with double newlines + ;; Replace blank separators between subtitles with double newlines (goto-char (point-min)) (while (subed-forward-subtitle-id) (let ((prev-sub-end (save-excursion (when (subed-backward-subtitle-end) (point))))) (when (and prev-sub-end - (not (string= (buffer-substring prev-sub-end (point)) "\n\n"))) + (not (string= (buffer-substring prev-sub-end (point)) "\n\n")) + (string-match "\\`\n+\\'" (buffer-substring prev-sub-end (point)))) (delete-region prev-sub-end (point)) (insert "\n\n")))) diff --git a/subed/subed.el b/subed/subed.el index e5f340109f..b5831de850 100644 --- a/subed/subed.el +++ b/subed/subed.el @@ -1,6 +1,6 @@ ;;; subed.el --- A major mode for editing subtitles -*- lexical-binding: t; -*- -;; Version: 1.0.21 +;; Version: 1.0.22 ;; Maintainer: Sacha Chua <sa...@sachachua.com> ;; Author: Random User ;; Keywords: convenience, files, hypermedia, multimedia diff --git a/tests/test-subed-common.el b/tests/test-subed-common.el index 3cc3db05a7..a256ef2ad5 100644 --- a/tests/test-subed-common.el +++ b/tests/test-subed-common.el @@ -3131,9 +3131,9 @@ This is another. (with-temp-srt-buffer (insert mock-srt-data) (expect (subed-subtitle-list) :to-equal - '((1 61000 65123 "Foo.") - (2 122234 130345 "Bar.") - (3 183450 195500 "Baz."))))) + '((1 61000 65123 "Foo." nil) + (2 122234 130345 "Bar." nil) + (3 183450 195500 "Baz." nil))))) (it "returns a subset when bounds are specified." (with-temp-srt-buffer (insert mock-srt-data) @@ -3141,9 +3141,8 @@ This is another. (backward-char 1) (expect (subed-subtitle-list (point-min) (point)) :to-equal - '((1 61000 65123 "Foo.") - (2 122234 130345 "Bar."))))) - ) + '((1 61000 65123 "Foo." nil) + (2 122234 130345 "Bar." nil)))))) (describe "Sorting" (it "detects sorted lists." (expect (subed--sorted-p '((1 1000 2000 "Test") diff --git a/tests/test-subed-vtt.el b/tests/test-subed-vtt.el index b846360a6e..8b00cd6887 100644 --- a/tests/test-subed-vtt.el +++ b/tests/test-subed-vtt.el @@ -1083,10 +1083,23 @@ Baz. (goto-char (point-min)) (forward-line 2) (while (re-search-forward "\n\n" nil t) - (replace-match "\n \n \t \t\t \n\n \t\n")) + (replace-match "\n\n\n\n\n\n")) (expect (buffer-string) :not :to-equal mock-vtt-data) (subed-sanitize) (expect (buffer-string) :to-equal mock-vtt-data))) + (it "retains comments" + (with-temp-vtt-buffer + (insert (concat "WEBVTT\n\n" + "00:01:01.000 --> 00:01:05.123\n" + "Foo.\n\nNOTE This is a test\n\n" + "00:02:02.234 --> 00:02:10.345\n" + "Bar.\n\n" + "NOTE\nAnother comment\n\n" + "00:03:03.45 --> 00:03:15.5\n" + "Baz.\n")) + (subed-sanitize) + (expect (buffer-string) :to-match "NOTE This is a test") + (expect (buffer-string) :to-match "Another comment"))) (it "ensures double newline between subtitles if text of previous subtitle is empty." (with-temp-vtt-buffer (insert mock-vtt-data)