Local SRT timing check

by scanfileonline

HTML

<main class="timing-check">
    <p class="eyebrow">A small subtitle-workflow example</p>
      <h1>Local SRT timing check</h1>
        <p>Check timestamp syntax, duration, order and overlaps before working with subtitles. Overlap warnings can be intentional.</p>
          <form id="timing-form">
              <label for="srt-input">SRT text</label>
                  <textarea id="srt-input" rows="12" maxlength="1000000" spellcheck="false" placeholder="1&#10;00:00:01,000 --&gt; 00:00:03,000&#10;Your caption."></textarea>
                      <div class="actions"><button type="submit">Check timing</button><button type="button" id="load-example">Load example</button></div>
                          <label class="file-label" for="srt-file">Or open a UTF-8 SRT file (up to 1 MB)</label>
                              <input id="srt-file" type="file" accept=".srt,text/plain">
                                </form>
                                  <section aria-labelledby="result-title"><h2 id="result-title">Timing notes</h2><p id="timing-status" role="status" aria-live="polite">Paste text or load the example to begin.</p><ul id="timing-issues"></ul></section>
                                    <p class="fine-print">This example uses local JavaScript and contains no upload request. Hosting-site privacy rules still apply. It checks timing lines; subtitle meaning, translation quality and complete format conformance are outside its scope. SRT files can use other encodings; the file picker here supports UTF-8.</p>
                                      <footer><p>Original example shared by <a href="https://scanfile.online/" target="_blank" rel="noopener">Scanfile · online file translation</a>. Prepared with AI assistance; example code is MIT licensed.</p><p>Format reference: <a href="https://www.loc.gov/preservation/digital/formats/fdd/fdd000569.shtml" target="_blank" rel="noopener">Library of Congress: SubRip subtitles</a>.</p></footer>
                     ...

CSS

*{box-sizing:border-box}body{margin:0;background:#f2f5f6;color:#173238;font:16px/1.6 system-ui,sans-serif}.timing-check{max-width:800px;margin:32px auto;padding:28px;background:#fff;border:1px solid #dae4e4;border-radius:16px}.eyebrow{color:#476968;font-size:.85rem;font-weight:650;letter-spacing:.03em}h1{line-height:1.15;margin:8px 0 18px;font-size:clamp(1.8rem,5vw,2.5rem)}h2{font-size:1.1rem}label{display:block;font-weight:600}textarea{display:block;width:100%;margin:8px 0 14px;padding:14px;border:1px solid #8baca9;border-radius:8px;background:#fbfdfd;color:#173238;font:14px/1.6 ui-monospace,monospace;resize:vertical}.actions{display:flex;flex-wrap:wrap;gap:10px}button{border:1px solid #285951;border-radius:7px;padding:10px 16px;background:#285951;color:white;font:inherit;cursor:pointer}button[type=button]{background:white;color:#285951}button:focus-visible,textarea:focus-visible,input:focus-visible,a:focus-visible{outline:3px solid #c97623;outline-offset:3px}.file-label{margin-top:20px;font-size:.9rem}input[type=file]{max-width:100%;margin-top:8px}section{margin-top:26px;padding-top:12px;border-top:1px solid #dae4e4}ul{padding-left:24px}li{margin:8px 0}.error{color:#983c32}.warning{color:#74531b}.fine-print,footer{color:#50696b;font-size:.85rem}footer{margin-top:24px;padding-top:10px;border-top:1px solid #dae4e4}a{color:#205f58}@media(max-width:600px){.timing-check{margin:12px;padding:18px;border-radius:10px}}

JavaScript

/* Original AI-assisted example shared by Scanfile. MIT License. */
(function () {
  'use strict';
    const MAX_CHARS = 1000000;
      const time = '(\\d{2,}):([0-5]\\d):([0-5]\\d),(\\d{3})';
        const timing = new RegExp('^(?:\\d+\\s+)?' + time + '\\s*-->\\s*' + time + '\\s*$');
          function inspect(source) {
              if (typeof source !== 'string') throw new TypeError('Expected subtitle text.');
                  if (source.length > MAX_CHARS) return { count: 0, issues: [{ severity: 'error', line: 0, code: 'limit', message: 'This example supports up to 1,000,000 text characters.' }] };
                      const issues = [];
                          let count = 0, previous = null;
                              const lines = source.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').split('\n');
                                  const millis = (m, i) => ((Number(m[i]) * 60 + Number(m[i + 1])) * 60 + Number(m[i + 2])) * 1000 + Number(m[i + 3]);
                                      lines.forEach((original, index) => {
                                            const line = original.trim();
                                                  if (!/^(?:\d+\s+)?\d{2,}:\d{2}:\d{2}/.test(line)) return;
                                                        count++;
                                                              const add = (severity, code, message) => issues.push({ severity, code, line: index + 1, message });
                                                                    const m = timing.exec(line);
                                                                          if (!m) { add('error', 'syntax', 'Use HH:MM:SS,mmm --> HH:MM:SS,mmm; minutes and seconds must be 00–59.'); return; }
                                                                                const start = millis(m, 1), end = millis(m, 5);
                                                                                      if...