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

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

[nongnu] elpa/subed 29d8b84 337/389: Initial implementation of subed-vtt


From: ELPA Syncer
Subject: [nongnu] elpa/subed 29d8b84 337/389: Initial implementation of subed-vtt.el
Date: Fri, 3 Dec 2021 11:00:53 -0500 (EST)

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

    Initial implementation of subed-vtt.el
---
 README.org            |   4 +-
 subed/subed-common.el |   3 +-
 subed/subed-config.el |  14 +-
 subed/subed-vtt.el    | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 562 insertions(+), 4 deletions(-)

diff --git a/README.org b/README.org
index 41e03a4..6ab747b 100644
--- a/README.org
+++ b/README.org
@@ -1,7 +1,7 @@
 * subed
 subed is an Emacs major mode for editing subtitles while playing the
-corresponding video with [[https://mpv.io/][mpv]].  At the moment, the only 
supported format is
-SubRip ( ~.srt~).
+corresponding video with [[https://mpv.io/][mpv]].  At the moment, the only 
supported formats are
+SubRip ( ~.srt~) and WebVTT ( ~.vtt~ ).
 
 [[file:https://raw.githubusercontent.com/rndusr/subed/master/screenshot.jpg]]
 
diff --git a/subed/subed-common.el b/subed/subed-common.el
index 3f6d998..6f866cb 100644
--- a/subed/subed-common.el
+++ b/subed/subed-common.el
@@ -135,7 +135,8 @@ Before BODY is run, point is placed on the subtitle's ID."
       ;; Check if point moved across subtitle boundaries.
       (let ((new-sub-id (subed-subtitle-id)))
         (when (and new-sub-id subed--current-subtitle-id
-                   (not (= new-sub-id subed--current-subtitle-id)))
+                   (not (funcall (if (stringp subed--current-subtitle-id) 
'string= 'equal)
+                                 new-sub-id subed--current-subtitle-id)))
           ;; Store new ID and fire signal.
           (setq subed--current-subtitle-id new-sub-id)
           (run-hooks 'subed-subtitle-motion-hook))))))
diff --git a/subed/subed-config.el b/subed/subed-config.el
index 7083ea3..dc3120f 100644
--- a/subed/subed-config.el
+++ b/subed/subed-config.el
@@ -36,7 +36,8 @@
 (defvar-local subed--subtitle-format nil
   "Short form of the name of the subtitle format in the current buffer (e.g. 
\"srt\").")
 
-(defvar subed--init-alist '(("srt" . subed-srt--init))
+(defvar subed--init-alist '(("srt" . subed-srt--init)
+                            ("vtt" . subed-vtt--init))
   "Alist that maps file extensions to format-specific init functions.")
 
 ;; This variable is set in subed.el to avoid compiler warnings because it uses
@@ -64,6 +65,17 @@
   '((t (:inherit 'default)))
   "Text of the subtitle")
 
+(defface subed-vtt-time-face
+  '((t (:inherit 'font-lock-string-face)))
+  "Start and stop times of subtitles")
+
+(defface subed-vtt-time-separator-face
+  '((t (:inherit 'font-lock-comment-face)))
+  "Separator between the start and stop time (\" --> \")")
+
+(defface subed-vtt-text-face
+  '((t (:inherit 'default)))
+  "Text of the subtitle")
 
 ;; Variables
 
diff --git a/subed/subed-vtt.el b/subed/subed-vtt.el
new file mode 100644
index 0000000..c736a7c
--- /dev/null
+++ b/subed/subed-vtt.el
@@ -0,0 +1,545 @@
+;;; subed-vtt.el --- WebVTT implementation for subed  -*- lexical-binding: t; 
-*-
+
+;;; License:
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; This is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING.  If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+
+;;; Commentary:
+
+;; WebVTT implementation for subed-mode.
+;; Since WebVTT doesn't use IDs, we'll use the starting timestamp.
+
+;;; Code:
+
+(require 'subed-config)
+(require 'subed-debug)
+(require 'subed-common)
+
+;;; Syntax highlighting
+
+(defconst subed-vtt-font-lock-keywords
+  (list
+   '("[0-9]+:[0-9]+:[0-9]+\\.[0-9]+" . 'subed-vtt-time-face)
+   '(",[0-9]+ \\(-->\\) [0-9]+:" 1 'subed-vtt-time-separator-face t)
+   '("^.*$" . 'subed-vtt-text-face))
+  "Highlighting expressions for `subed-mode'.")
+
+
+;;; Parsing
+
+(defconst subed-vtt--regexp-timestamp 
"\\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\)\\.\\([0-9]+\\)")
+(defconst subed-vtt--regexp-separator "\\(?:[[:blank:]]*\n\\)+[[:blank:]]*\n")
+
+(defun subed-vtt--timestamp-to-msecs (time-string)
+  "Find HH:MM:SS.MS pattern in TIME-STRING and convert it to milliseconds.
+Return nil if TIME-STRING doesn't match the pattern."
+  (save-match-data
+    (when (string-match subed-vtt--regexp-timestamp time-string)
+      (let ((hours (string-to-number (match-string 1 time-string)))
+            (mins  (string-to-number (match-string 2 time-string)))
+            (secs  (string-to-number (match-string 3 time-string)))
+            (msecs (string-to-number (subed--right-pad (match-string 4 
time-string) 3 ?0))))
+        (+ (* (truncate hours) 3600000)
+           (* (truncate mins) 60000)
+           (* (truncate secs) 1000)
+           (truncate msecs))))))
+
+(defun subed-vtt--msecs-to-timestamp (msecs)
+  "Convert MSECS to string in the format HH:MM:SS,MS."
+  ;; We need to wrap format-seconds in save-match-data because it does regexp
+  ;; stuff and we need to preserve our own match-data.
+  (concat (save-match-data (format-seconds "%02h:%02m:%02s" (/ msecs 1000)))
+          "." (format "%03d" (mod msecs 1000))))
+
+(defun subed-vtt--subtitle-id ()
+  "Return the ID of the subtitle at point or nil if there is no ID."
+  (save-excursion
+    (when (subed-vtt--jump-to-subtitle-id)
+      (when (looking-at subed-vtt--regexp-timestamp)
+        (match-string 0)))))
+
+(defun subed-vtt--subtitle-id-max ()
+  "Return the ID of the last subtitle or nil if there are no subtitles."
+  (save-excursion
+    (goto-char (point-max))
+    (subed-vtt--subtitle-id)))
+
+(defun subed-vtt--subtitle-id-at-msecs (msecs)
+  "Return the ID of the subtitle at MSECS milliseconds.
+Return nil if there is no subtitle at MSECS."
+  (save-match-data
+    (save-excursion
+      (goto-char (point-min))
+      (let* ((secs       (/ msecs 1000))
+             (only-hours (truncate (/ secs 3600)))
+             (only-mins  (truncate (/ (- secs (* only-hours 3600)) 60))))
+        ;; Move to first subtitle in the relevant hour
+        (when (re-search-forward (format "\\(%s\\|\\`\\)%02d:" 
subed-vtt--regexp-separator only-hours) nil t)
+          (beginning-of-line)
+          ;; Move to first subtitle in the relevant hour and minute
+          (re-search-forward (format "\\(\n\n\\|\\`\\)%02d:%02d" only-hours 
only-mins) nil t)))
+      ;; Move to first subtitle that starts at or after MSECS
+      (catch 'subtitle-id
+        (while (<= (or (subed-vtt--subtitle-msecs-start) -1) msecs)
+          ;; If stop time is >= MSECS, we found a match
+          (let ((cur-sub-end (subed-vtt--subtitle-msecs-stop)))
+            (when (and cur-sub-end (>= cur-sub-end msecs))
+              (throw 'subtitle-id (subed-vtt--subtitle-id))))
+          (unless (subed-vtt--forward-subtitle-id)
+            (throw 'subtitle-id nil)))))))
+
+(defun subed-vtt--subtitle-msecs-start (&optional sub-id)
+  "Subtitle start time in milliseconds or nil if it can't be found.
+If SUB-ID is not given, use subtitle on point."
+  (let ((timestamp (save-excursion
+                     (when (subed-vtt--jump-to-subtitle-time-start sub-id)
+                       (when (looking-at subed-vtt--regexp-timestamp)
+                         (match-string 0))))))
+    (when timestamp
+      (subed-vtt--timestamp-to-msecs timestamp))))
+
+(defun subed-vtt--subtitle-msecs-stop (&optional sub-id)
+  "Subtitle stop time in milliseconds or nil if it can't be found.
+If SUB-ID is not given, use subtitle on point."
+  (let ((timestamp (save-excursion
+                     (when (subed-vtt--jump-to-subtitle-time-stop sub-id)
+                       (when (looking-at subed-vtt--regexp-timestamp)
+                         (match-string 0))))))
+    (when timestamp
+      (subed-vtt--timestamp-to-msecs timestamp))))
+
+(defun subed-vtt--subtitle-text (&optional sub-id)
+  "Return subtitle's text or an empty string.
+If SUB-ID is not given, use subtitle on point."
+  (or (save-excursion
+        (let ((beg (subed-vtt--jump-to-subtitle-text sub-id))
+              (end (subed-vtt--jump-to-subtitle-end sub-id)))
+          (when (and beg end)
+            (buffer-substring beg end))))
+      ""))
+
+(defun subed-vtt--subtitle-relative-point ()
+  "Point relative to subtitle's ID or nil if ID can't be found."
+  (let ((start-point (save-excursion
+                       (when (subed-vtt--jump-to-subtitle-id)
+                         (point)))))
+    (when start-point
+      (- (point) start-point))))
+
+;;; Traversing
+
+(defun subed-vtt--jump-to-subtitle-id (&optional sub-id)
+  "Move to the ID of a subtitle and return point.
+If SUB-ID is not given, focus the current subtitle's ID.
+Return point or nil if no subtitle ID could be found.
+WebVTT doesn't use IDs, so we use the starting timestamp instead"
+  (interactive)
+  (save-match-data
+    (if sub-id
+        ;; Look for a line that contains only the ID, preceded by one or more
+        ;; blank lines or the beginning of the buffer.
+        (let* ((orig-point (point))
+               (regex (concat "\\(" subed-srt--regexp-separator "\\|\\`\\)\\(" 
(regexp-quote (string-to-number sub-id)) "\\)"))
+               (match-found (progn (goto-char (point-min))
+                                   (re-search-forward regex nil t))))
+          (if match-found
+              (goto-char (match-beginning 2))
+            (goto-char orig-point)))
+      ;; Find one or more blank lines.
+      (re-search-forward "\\([[:blank:]]*\n\\)+" nil t)
+      ;; Find two or more blank lines or the beginning of the buffer, followed
+      ;; by line starting with a timestamp.
+      (let* ((regex (concat  "\\(" subed-srt--regexp-separator "\\|\\`\\)\\(" 
subed-vtt--regexp-timestamp "\\)"))
+             (match-found (re-search-backward regex nil t)))
+        (when match-found
+          (goto-char (match-beginning 2)))))
+    ;; Make extra sure we're on a timestamp, return nil if we're not
+    (when (looking-at "^\\([0-9]+:[0-9]+:[0-9]+\\.[0-9]+\\)")
+      (point))))
+
+(defun subed-vtt--jump-to-subtitle-id-at-msecs (msecs)
+  "Move point to the ID of the subtitle that is playing at MSECS.
+Return point or nil if point is still on the same subtitle.
+See also `subed-vtt--subtitle-id-at-msecs'."
+  (let ((current-sub-id (subed-vtt--subtitle-id))
+        (target-sub-id (subed-vtt--subtitle-id-at-msecs msecs)))
+    (when (and target-sub-id current-sub-id (not (= target-sub-id 
current-sub-id)))
+      (subed-vtt--jump-to-subtitle-id target-sub-id))))
+
+(defun subed-vtt--jump-to-subtitle-text-at-msecs (msecs)
+  "Move point to the text of the subtitle that is playing at MSECS.
+Return point or nil if point is still on the same subtitle.
+See also `subed-vtt--subtitle-id-at-msecs'."
+  (when (subed-vtt--jump-to-subtitle-id-at-msecs msecs)
+    (subed-vtt--jump-to-subtitle-text)))
+
+(defun subed-vtt--jump-to-subtitle-time-start (&optional sub-id)
+  "Move point to subtitle's start time.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if no start time could be found."
+  (interactive)
+  (save-match-data
+    (when (subed-vtt--jump-to-subtitle-id sub-id)
+      (when (looking-at subed-vtt--regexp-timestamp)
+        (point)))))
+
+(defun subed-vtt--jump-to-subtitle-time-stop (&optional sub-id)
+  "Move point to subtitle's stop time.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if no stop time could be found."
+  (interactive)
+  (save-match-data
+    (when (subed-vtt--jump-to-subtitle-id sub-id)
+      (re-search-forward " *--> *" (point-at-eol) t)
+      (when (looking-at subed-vtt--regexp-timestamp)
+        (point)))))
+
+(defun subed-vtt--jump-to-subtitle-text (&optional sub-id)
+  "Move point on the first character of subtitle's text.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if a the subtitle's text can't be found."
+  (interactive)
+  (when (subed-vtt--jump-to-subtitle-id sub-id)
+    (forward-line 1)
+    (point)))
+
+(defun subed-vtt--jump-to-subtitle-end (&optional sub-id)
+  "Move point after the last character of the subtitle's text.
+If SUB-ID is not given, use subtitle on point.
+Return point or nil if point did not change or if no subtitle end
+can be found."
+  (interactive)
+  (save-match-data
+    (let ((orig-point (point)))
+      (subed-vtt--jump-to-subtitle-text sub-id)
+      ;; Look for next separator or end of buffer.  We can't use
+      ;; `subed-vtt--regexp-separator' here because if subtitle text is empty,
+      ;; it may be the only empty line in the separator, i.e. there's only one
+      ;; "\n".
+      (let ((regex (concat 
"\\([[:blank:]]*\n+[0-9]+\n\\|\\([[:blank:]]*\n*\\)\\'\\)")))
+        (when (re-search-forward regex nil t)
+          (goto-char (match-beginning 0))))
+      (unless (= (point) orig-point)
+        (point)))))
+
+(defun subed-vtt--forward-subtitle-id ()
+  "Move point to next subtitle's ID.
+Return point or nil if there is no next subtitle."
+  (interactive)
+  (save-match-data
+    (when (re-search-forward (concat subed-vtt--regexp-separator 
subed-vtt--regexp-timestamp) nil t)
+      (subed-vtt--jump-to-subtitle-id))))
+
+(defun subed-vtt--backward-subtitle-id ()
+  "Move point to previous subtitle's ID.
+Return point or nil if there is no previous subtitle."
+  (interactive)
+  (let ((orig-point (point)))
+    (when (subed-vtt--jump-to-subtitle-id)
+      (if (re-search-backward (concat "\\(" subed-vtt--regexp-separator 
"\\|\\`[[:space:]]*\\)" "\\([0-9]+\\)\n") nil t)
+          (progn
+            (goto-char (match-beginning 2))
+            (point))
+        (goto-char orig-point)
+        nil))))
+
+(defun subed-vtt--forward-subtitle-text ()
+  "Move point to next subtitle's text.
+Return point or nil if there is no next subtitle."
+  (interactive)
+  (when (subed-vtt--forward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-text)))
+
+(defun subed-vtt--backward-subtitle-text ()
+  "Move point to previous subtitle's text.
+Return point or nil if there is no previous subtitle."
+  (interactive)
+  (when (subed-vtt--backward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-text)))
+
+(defun subed-vtt--forward-subtitle-end ()
+  "Move point to end of next subtitle.
+Return point or nil if there is no next subtitle."
+  (interactive)
+  (when (subed-vtt--forward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-end)))
+
+(defun subed-vtt--backward-subtitle-end ()
+  "Move point to end of previous subtitle.
+Return point or nil if there is no previous subtitle."
+  (interactive)
+  (when (subed-vtt--backward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-end)))
+
+(defun subed-vtt--forward-subtitle-time-start ()
+  "Move point to next subtitle's start time."
+  (interactive)
+  (when (subed-vtt--forward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-time-start)))
+
+(defun subed-vtt--backward-subtitle-time-start ()
+  "Move point to previous subtitle's start time."
+  (interactive)
+  (when (subed-vtt--backward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-time-start)))
+
+(defun subed-vtt--forward-subtitle-time-stop ()
+  "Move point to next subtitle's stop time."
+  (interactive)
+  (when (subed-vtt--forward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-time-stop)))
+
+(defun subed-vtt--backward-subtitle-time-stop ()
+  "Move point to previous subtitle's stop time."
+  (interactive)
+  (when (subed-vtt--backward-subtitle-id)
+    (subed-vtt--jump-to-subtitle-time-stop)))
+
+
+;;; Manipulation
+
+(defun subed-vtt--set-subtitle-time-start (msecs &optional sub-id)
+  "Set subtitle start time to MSECS milliseconds.
+
+If SUB-ID is not given, set the start of the current subtitle.
+
+Return the new subtitle start time in milliseconds."
+  (save-excursion
+    (when (or (not sub-id)
+              (and sub-id (subed-vtt--jump-to-subtitle-id sub-id)))
+      (subed-vtt--jump-to-subtitle-time-start)
+      (when (looking-at subed-vtt--regexp-timestamp)
+        (replace-match (subed-vtt--msecs-to-timestamp msecs))))))
+
+(defun subed-vtt--set-subtitle-time-stop (msecs &optional sub-id)
+  "Set subtitle stop time to MSECS milliseconds.
+
+If SUB-ID is not given, set the stop of the current subtitle.
+
+Return the new subtitle stop time in milliseconds."
+  (save-excursion
+    (when (or (not sub-id)
+              (and sub-id (subed-vtt--jump-to-subtitle-id sub-id)))
+      (subed-vtt--jump-to-subtitle-time-stop)
+      (when (looking-at subed-vtt--regexp-timestamp)
+        (replace-match (subed-vtt--msecs-to-timestamp msecs))))))
+
+(defun subed-vtt--make-subtitle (&optional id start stop text)
+  "Generate new subtitle string.
+
+ID, START default to 0.
+STOP defaults to (+ START `subed-subtitle-spacing')
+TEXT defaults to an empty string.
+
+A newline is appended to TEXT, meaning you'll get two trailing
+newlines if TEXT is nil or empty."
+  (interactive "P")
+  (format "%s --> %s\n%s\n"
+          (subed-vtt--msecs-to-timestamp (or start 0))
+          (subed-vtt--msecs-to-timestamp (or stop (+ (or start 0)
+                                                     
subed-default-subtitle-length)))
+          (or text "")))
+
+(defun subed-vtt--prepend-subtitle (&optional id start stop text)
+  "Insert new subtitle before the subtitle at point.
+
+ID and START default to 0.
+STOP defaults to (+ START `subed-subtitle-spacing')
+TEXT defaults to an empty string.
+
+Move point to the text of the inserted subtitle.
+Return new point."
+  (interactive "P")
+  (subed-vtt--jump-to-subtitle-id)
+  (insert (subed-vtt--make-subtitle id start stop text))
+  (save-match-data
+    (when (looking-at "\\([[:space:]]*\\|^\\)[0-9]+$")
+      (insert "\n")))
+  (forward-line -2)
+  (subed-vtt--jump-to-subtitle-text))
+
+(defun subed-vtt--append-subtitle (&optional id start stop text)
+  "Insert new subtitle after the subtitle at point.
+
+ID, START default to 0.
+STOP defaults to (+ START `subed-subtitle-spacing')
+TEXT defaults to an empty string.
+
+Move point to the text of the inserted subtitle.
+Return new point."
+  (interactive "P")
+  (unless (subed-vtt--forward-subtitle-id)
+    ;; Point is on last subtitle or buffer is empty
+    (subed-vtt--jump-to-subtitle-end)
+    ;; Moved point to end of last subtitle; ensure separator exists
+    (while (not (looking-at "\\(\\`\\|[[:blank:]]*\n[[:blank:]]*\n\\)"))
+      (save-excursion (insert ?\n)))
+    ;; Move to end of separator
+    (goto-char (match-end 0)))
+  (insert (subed-vtt--make-subtitle id start stop text))
+  ;; Complete separator with another newline unless we inserted at the end
+  (save-match-data
+    (when (looking-at (concat "\\([[:space:]]*\\|^\\)" 
subed-vtt--regexp-timestamp))
+      (insert ?\n)))
+  (forward-line -2)
+  (subed-vtt--jump-to-subtitle-text))
+
+(defun subed-vtt--kill-subtitle ()
+  "Remove subtitle at point."
+  (interactive)
+  (let ((beg (save-excursion (subed-vtt--jump-to-subtitle-id)
+                             (point)))
+        (end (save-excursion (subed-vtt--jump-to-subtitle-id)
+                             (when (subed-vtt--forward-subtitle-id)
+                               (point)))))
+    (if (not end)
+        ;; Removing the last subtitle because forward-subtitle-id returned nil
+        (setq beg (save-excursion (goto-char beg)
+                                  (subed-vtt--backward-subtitle-end)
+                                  (1+ (point)))
+              end (save-excursion (goto-char (point-max)))))
+    (delete-region beg end))
+  (subed-vtt--regenerate-ids-soon))
+
+
+;;; Maintenance
+
+(defun subed-vtt--regenerate-ids ()
+  "Not applicable to WebVTT."
+  (interactive))
+
+(defvar-local subed-vtt--regenerate-ids-soon-timer nil)
+(defun subed-vtt--regenerate-ids-soon ()
+  "Not applicable to WebVTT."
+  (interactive))
+
+(defun subed-vtt--sanitize ()
+  "Remove surplus newlines and whitespace."
+  (interactive)
+  (atomic-change-group
+    (save-match-data
+      (subed-save-excursion
+       ;; Remove trailing whitespace from each line
+       (delete-trailing-whitespace (point-min) (point-max))
+
+       ;; Remove leading spaces and tabs from each line
+       (goto-char (point-min))
+       (while (re-search-forward "^[[:blank:]]+" nil t)
+         (replace-match ""))
+
+       ;; Remove leading newlines
+       (goto-char (point-min))
+       (while (looking-at "\\`\n+")
+         (replace-match ""))
+
+       ;; Replace separators between subtitles with double newlines
+       (goto-char (point-min))
+       (while (subed-vtt--forward-subtitle-id)
+         (let ((prev-sub-end (save-excursion (when 
(subed-vtt--backward-subtitle-end)
+                                               (point)))))
+           (when (and prev-sub-end
+                      (not (string= (buffer-substring prev-sub-end (point)) 
"\n\n")))
+             (delete-region prev-sub-end (point))
+             (insert "\n\n"))))
+
+       ;; Two trailing newline if last subtitle text is empty, one trailing
+       ;; newline otherwise; do nothing in empty buffer (no graphical
+       ;; characters)
+       (goto-char (point-min))
+       (when (re-search-forward "[[:graph:]]" nil t)
+         (goto-char (point-max))
+         (subed-vtt--jump-to-subtitle-end)
+         (unless (looking-at "\n\\'")
+           (delete-region (point) (point-max))
+           (insert "\n")))
+
+       ;; One space before and after " --> "
+       (goto-char (point-min))
+       (while (re-search-forward (format "^%s" subed-vtt--regexp-timestamp) 
nil t)
+         (when (looking-at "[[:blank:]]*-->[[:blank:]]*")
+           (unless (= (length (match-string 0)) 5)
+             (replace-match " --> "))))))))
+
+(defun subed-vtt--validate ()
+  "Move point to the first invalid subtitle and report an error."
+  (interactive)
+  (when (> (buffer-size) 0)
+    (atomic-change-group
+      (save-match-data
+        (let ((orig-point (point)))
+          (goto-char (point-min))
+          (while (and (re-search-forward (format "\\(%s[[^\\']]\\|\\`\\)" 
subed-vtt--regexp-separator) nil t)
+                      (looking-at "[[:alnum:]]"))
+            ;; This regex is stricter than `subed-vtt--regexp-timestamp'
+            (unless (looking-at 
"^[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\.[0-9]\\{3\\}")
+              (error "Found invalid start time: %S"  (substring (or 
(thing-at-point 'line :no-properties) "\n") 0 -1)))
+            (when (re-search-forward "[[:blank:]]" (point-at-eol) t)
+              (goto-char (match-beginning 0)))
+            (unless (looking-at " --> ")
+              (error "Found invalid separator between start and stop time: %S"
+                     (substring (or (thing-at-point 'line :no-properties) 
"\n") 0 -1)))
+            (condition-case nil
+                (forward-char 5)
+              (error nil))
+            (unless (looking-at 
"[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\},[0-9]\\{3\\}$")
+              (error "Found invalid stop time: %S" (substring (or 
(thing-at-point 'line :no-properties) "\n") 0 -1))))
+          (goto-char orig-point))))))
+
+(defun subed-vtt--sort ()
+  "Sanitize, then sort subtitles by start time and re-number them."
+  (interactive)
+  (atomic-change-group
+    (subed-vtt--sanitize)
+    (subed-vtt--validate)
+    (subed-save-excursion
+     (goto-char (point-min))
+     (sort-subr nil
+                ;; nextrecfun (move to next record/subtitle or to end-of-buffer
+                ;; if there are no more records)
+                (lambda () (unless (subed-vtt--forward-subtitle-id)
+                             (goto-char (point-max))))
+                ;; endrecfun (move to end of current record/subtitle)
+                #'subed-vtt--jump-to-subtitle-end
+                ;; startkeyfun (return sort value of current record/subtitle)
+                #'subed-vtt--subtitle-msecs-start))
+    (subed-vtt--regenerate-ids)))
+
+(defun subed-vtt--init ()
+  "This function is called when subed-mode is entered for a SRT file."
+  (setq-local subed--subtitle-format "vtt")
+  (setq-local font-lock-defaults '(subed-vtt-font-lock-keywords))
+  ;; Support for fill-paragraph (M-q)
+  (let ((timestamps-regexp (concat subed-vtt--regexp-timestamp
+                                   " *--> *"
+                                   subed-vtt--regexp-timestamp)))
+    (setq-local paragraph-separate (concat "^\\("
+                                           (mapconcat 'identity 
`("[[:blank:]]*"
+                                                                  
"[[:digit:]]+"
+                                                                  
,timestamps-regexp) "\\|")
+                                           "\\)$"))
+    (setq-local paragraph-start (concat "\\("
+                                        ;; Mulitple speakers in the same
+                                        ;; subtitle are often distinguished 
with
+                                        ;; a "-" at the start of the line.
+                                        (mapconcat 'identity '("^-"
+                                                               
"[[:graph:]]*$") "\\|")
+                                        "\\)"))))
+
+(provide 'subed-vtt)
+;;; subed-vtt.el ends here



reply via email to

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