[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
- [nongnu] elpa/subed 3baf5c1 296/389: subed-mpv--client-filter: Store process mark in variable, (continued)
- [nongnu] elpa/subed 3baf5c1 296/389: subed-mpv--client-filter: Store process mark in variable, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 08d5033 301/389: Remove unused variable subed-mode--enabled-p, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 070384b 302/389: subed--set-subtitle-loop: Don't croak on empty file, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 89df304 304/389: subed-srt--sanitize: Don't insert newline in empty buffer, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed b0a4ff0 307/389: Fix parentheses, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 8007c12 308/389: Make debugging window a bit smaller, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed ec38a0b 317/389: Add function: subed-mpv-add-subtitles, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 7764909 324/389: Add some (require ...)s to prevent compiler warnings, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed a084f5d 326/389: Improve comment, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed cd3af86 328/389: Silence checkdoc, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 29d8b84 337/389: Initial implementation of subed-vtt.el,
ELPA Syncer <=
- [nongnu] elpa/subed 6db0005 338/389: New function subed-mpv-play-video-from-url, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed d3df6f2 347/389: Fix "test" target in Makefile, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 5b8c23a 348/389: New commands: subed-merge-with-next, subed-merge-with-previous, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed a7b25c8 341/389: subed-vtt: Fix ID->timestamp and stop generating IDs, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 9796a77 349/389: Merge branch 'sacha', ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed c4e3fec 352/389: Add error handling for subed-split-subtitle, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 918ba6f 370/389: Prevent showing the "Match data clobbered" message, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 808ef92 118/389: Fix comment, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 97bb125 183/389: Add default keybindings for copying player position, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 7665826 248/389: Add tests for copying player position to start/stop time, ELPA Syncer, 2021/12/03