independent-subtitle-parser 0.0.0-stage → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/CONTRIBUTING.md +81 -0
  3. package/LICENSE +21 -0
  4. package/README.md +194 -2
  5. package/THIRD_PARTY_NOTICES.md +60 -0
  6. package/dist/context.d.ts +22 -0
  7. package/dist/context.js +70 -0
  8. package/dist/decode.d.ts +3 -0
  9. package/dist/decode.js +51 -0
  10. package/dist/detect.d.ts +3 -0
  11. package/dist/detect.js +38 -0
  12. package/dist/entities.d.ts +1 -0
  13. package/dist/entities.js +2235 -0
  14. package/dist/formats/ass.d.ts +3 -0
  15. package/dist/formats/ass.js +298 -0
  16. package/dist/formats/sami.d.ts +3 -0
  17. package/dist/formats/sami.js +594 -0
  18. package/dist/formats/timed.d.ts +2 -0
  19. package/dist/formats/timed.js +149 -0
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.js +67 -0
  22. package/dist/text.d.ts +9 -0
  23. package/dist/text.js +109 -0
  24. package/dist/time.d.ts +5 -0
  25. package/dist/time.js +23 -0
  26. package/dist/types.d.ts +86 -0
  27. package/dist/types.js +1 -0
  28. package/docs/API.md +159 -0
  29. package/docs/CI.md +55 -0
  30. package/docs/CORPUS-016.md +112 -0
  31. package/docs/CORPUS-017.md +148 -0
  32. package/docs/CORPUS.md +163 -0
  33. package/docs/DESIGN.md +94 -0
  34. package/docs/DEVELOPMENT.md +104 -0
  35. package/docs/DIAGNOSTICS.md +65 -0
  36. package/docs/IMPROVEMENTS.md +91 -0
  37. package/docs/QUALITY.md +53 -0
  38. package/docs/RELEASE.md +109 -0
  39. package/docs/REPORTING.md +73 -0
  40. package/docs/SOURCES.md +70 -0
  41. package/docs/USAGE.md +118 -0
  42. package/examples/browser.html +36 -0
  43. package/examples/browser.mjs +107 -0
  44. package/examples/node.mjs +41 -0
  45. package/examples/parse-worker.mjs +12 -0
  46. package/examples/serve.mjs +24 -0
  47. package/examples/smoke.html +37 -0
  48. package/examples/worker-client.mjs +28 -0
  49. package/examples/workflow.mjs +35 -0
  50. package/package.json +48 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ ## 0.1.7 — legacy SAMI commands and large real ASS/SSA validation, unpublished
4
+
5
+ - Connect the public GitHub source repository, issue/PR routes and npm repository
6
+ metadata. Prepare the public 0.1.7 manifest and registry/tarball installation
7
+ instructions; actual npm publication remains pending separate approval.
8
+ - Prepare public-facing README, contribution/reporting guides and GitHub issue/PR
9
+ templates. Include contribution/reporting guides in the package. Add a separate
10
+ release package check for the public manifest.
11
+ - Retain observed `SpanID`, `ShowImg` and `DisplayImg` authoring commands as opaque
12
+ `sami-legacy-command` metadata; exclude complete commands from reading text so
13
+ they cannot create phantom captions or prevent NBSP clears. Warn once with
14
+ `SAMI_LEGACY_COMMAND_UNAPPLIED`. No command effects are executed.
15
+ - Recover incomplete commands at the next `<`; preserve their literal text and
16
+ diagnose malformed syntax. Quoted `>` and unquoted apostrophes are covered.
17
+ - Add seven hash-pinned local evaluation files, including 7.6 MB and 5.9 MB ASS
18
+ animation-heavy originals and three long-form SSA v4 files. Independent
19
+ expectations cover every event's time/raw fields and a declared reading subset.
20
+ - Score three previously unscored legacy SAMI files under a narrow nonspoken
21
+ command profile. Demo layout and interactive playback remain unscored.
22
+ - See [scope, comparisons and reproduction](docs/CORPUS-017.md). No npm release
23
+ or product integration has been performed.
24
+
25
+ ## 0.1.6 — ASS/SSA/SAMI corpus validation, unpublished
26
+
27
+ - Recover unpadded ASS minute/second fields with `ASS_TIME_WIDTH_RECOVERED`;
28
+ keep centisecond precision strict and preserve original event fields.
29
+ - Read legacy SAMI `SAMI_Type` as well as `SAMIType` into track kind.
30
+ - Warn once per document for unapplied SAMI Source ID speaker persistence;
31
+ retain its paragraph text and attributes without claiming layout support.
32
+ - Add 18 pinned, licensed upstream ASS/SSA/SAMI originals and independent
33
+ source-derived oracles. Evaluate 19 additional local-only originals without
34
+ redistributing transcripts of uncertain license. Record unscored legacy
35
+ player commands separately from passing cases.
36
+ - Expand the mutation corpus and current-runtime correctness, package and
37
+ performance checks. Keep `private: true`; no adapters or publication.
38
+
39
+ ## 0.1.5 — publication preparation, unpublished
40
+
41
+ - Confirm the package name and apply the owner-selected MIT license to original
42
+ code. Keep `private: true` until a separately authorized publication.
43
+ - Check actual archive bytes against source and installed files, verify npm
44
+ integrity, enforce the package allowlist and require repeatable tarball bytes.
45
+ - Bound browser track options to 100 per page and shorten displayed labels;
46
+ all tracks remain selectable and complete results remain in JSON.
47
+ - Add pinned, licensed libass and Microsoft WMP samples with independent
48
+ expectations, plus a reproducible external-writer integration comparison.
49
+ - Expand fuzz seeds and clarify the remaining corpus and platform gaps.
50
+ - Parser runtime and public API remain unchanged from 0.1.4. No adapters,
51
+ product changes or npm publication.
52
+
53
+ ## 0.1.4 — consumer usability iteration, unpublished
54
+
55
+ - Reorganize README around installed-package use; move detailed contract to API
56
+ reference and add diagnostic, usage and development guides.
57
+ - Include runnable Node and browser file examples in the tarball. Browser parsing
58
+ runs in a disposable Worker with cancel/deadline/stale-result handling, explicit
59
+ encoding/format choices, track selection, labeled previews and complete JSON.
60
+ - Keep decode and parse diagnostics distinct, unknown cue ends unchanged, and
61
+ text output inert. No file upload, storage, charset guessing or rendering.
62
+ - Retain the existing public API and TypeScript interfaces; workflow helpers are
63
+ consumer recipes, not package exports. Parser source is unchanged from 0.1.3.
64
+ - Correct stale documentation about the already completed product comparison.
65
+ - Preserve historical quality evidence; no product adapters or npm publication.
66
+
67
+ ## 0.1.3 — corpus and robustness iteration, unpublished
68
+
69
+ - Fix WebVTT missing-separator recovery assigning header metadata as a cue ID.
70
+ - Recover SRT single-dash arrows and one terminal period after a complete comma
71
+ end timestamp, with dedicated warnings and exact source retention. These occur
72
+ in Blender's published Portuguese and Indonesian subtitles.
73
+ - Diagnose preserved unterminated WebVTT markup instead of returning clean status.
74
+ - Correct windows-1252 C1 characters on affected Node/ICU decoders using the
75
+ WHATWG mapping; aliases resolve through TextDecoder. Other encodings remain
76
+ runtime-dependent. No charset detection was introduced.
77
+ - Avoid the SAMI markup scanner for plain paragraphs and gaps while preserving
78
+ HTML ASCII whitespace and non-ASCII spaces.
79
+ - Add pinned WPT and licensed Blender corpus, six OSS comparison targets,
80
+ isolated performance/memory harnesses and reproducible sustained fuzzing.
81
+ - Test Node 20 and 24 plus Chromium. Evidence and remaining limits are recorded
82
+ in the local `evidence/quality-013-final/REPORT.md` and `docs/CORPUS.md`.
83
+ - No adapters, product changes, runtime dependencies or publication.
84
+
85
+ ## 0.1.2 — local quality iteration, unpublished
86
+
87
+ - Fix valid WebVTT class names containing Unicode spaces and voice annotations
88
+ containing a literal `<`. Preserve the original tag in raw text and fields.
89
+ - Add 19 focused text regressions and generated arithmetic, newline, Unicode
90
+ decode and malformed-content invariant checks.
91
+ - Define evidence-based public npm release gates and defer product adapters.
92
+ - No public API shape changes. Existing 0.1.0/0.1.1 and product-comparison evidence
93
+ remains unchanged. Full WebVTT semantic trees and ASS rendering remain absent.
94
+
95
+ ## 0.1.1 — unpublished
96
+
97
+ See `docs/IMPROVEMENTS.md` and the frozen `evidence/improvements-1/REPORT.md` in
98
+ the local source workspace for the original improvement record.
@@ -0,0 +1,81 @@
1
+ # Contributing
2
+
3
+ Contributions can improve parsing, independent test expectations, documentation
4
+ or consumer examples. Keep changes focused on the documented extraction profile.
5
+ Please explain the input that fails and the behavior your change establishes.
6
+
7
+ The canonical repository is
8
+ [saramkim/independent-subtitle-parser](https://github.com/saramkim/independent-subtitle-parser).
9
+ Use [Issues](https://github.com/saramkim/independent-subtitle-parser/issues) for
10
+ reports and proposals. Fork the repository, make a focused branch, then open a
11
+ [pull request](https://github.com/saramkim/independent-subtitle-parser/pulls)
12
+ against `main`. See [Reporting bugs](docs/REPORTING.md). No response time is promised.
13
+
14
+ ## Set up a source checkout
15
+
16
+ Use Node.js 20+ and npm. Package validation also uses `tar` from PATH.
17
+ Runtime dependencies remain empty; TypeScript is a development dependency.
18
+
19
+ ```sh
20
+ npm ci --ignore-scripts
21
+ npm run build
22
+ npm test
23
+ ```
24
+
25
+ For a focused change, build and run the relevant test file first:
26
+
27
+ ```sh
28
+ npm run build
29
+ node --test test/sami.test.mjs
30
+ ```
31
+
32
+ Before proposing a finished patch, run `npm run validate`. It builds the package,
33
+ runs the suite, installs/checks the real tarball, runs the Node example and records
34
+ a benchmark. Evidence is written to a fresh ignored directory. See the
35
+ [development guide](docs/DEVELOPMENT.md) for custom output paths and PowerShell.
36
+ Do not label an unexecuted platform/browser check as passed. For documentation
37
+ changes, validate changed examples and package links; unrelated long fuzz
38
+ campaigns need not be rerun when their runtime inputs are unchanged.
39
+
40
+ ## Establish the expected result independently
41
+
42
+ - Cite a public format specification or a clearly described compatibility policy.
43
+ For real files, record provenance, encoding, revision and SHA-256.
44
+ - Add the smallest useful reproduction. Preserve the original failing case and
45
+ show that the proposed fix changes it for the stated reason.
46
+ - Do not change an expected value merely because this parser or another library
47
+ returns something else. Explain disagreements and distinguish parsing from
48
+ rendering, raw preservation from text projection, and recovery from conformance.
49
+ - Test important effects on times, line breaks, raw/source text, tracks, fields,
50
+ diagnostics and resource limits. Include malformed variants when relevant.
51
+
52
+ Tests are in `test/`; runtime code is in `src/`; examples are host applications
53
+ in `examples/`. The public API is the package root. Keep the core independent of
54
+ UI, DOM, filesystem, storage and extension APIs. Full ASS rendering, streaming
55
+ and serialization are outside the current scope. Discuss substantial API or
56
+ dependency changes with maintainers before investing in a large patch.
57
+
58
+ ## Fixtures, privacy and licensing
59
+
60
+ Prefer a small synthetic subtitle that you wrote. Do not commit private captions,
61
+ full copyrighted transcripts, credentials, local account paths or personal data.
62
+ An open-source repository's code license does not necessarily license subtitles,
63
+ fonts or images stored beside its code. Third-party fixtures need a verified
64
+ redistribution basis, attribution and provenance; retain their original licenses.
65
+ If rights are unclear, use a synthetic reproduction or record public provenance
66
+ for optional local evaluation. See [corpus methodology](docs/CORPUS.md).
67
+
68
+ Preserve fixture bytes, BOM and line endings. `.gitattributes` intentionally
69
+ disables automatic newline conversion. Do not bulk-format corpus files.
70
+ Keep generated `dist/`, tarballs, dependencies, caches and local `evidence/` out
71
+ of commits. Original code contributions are provided under the project's
72
+ [MIT license](LICENSE); retain applicable third-party notices.
73
+
74
+ ## Submit a focused change
75
+
76
+ Describe the problem, independent expected behavior, final change and checks
77
+ actually run. Mention API/diagnostic changes and known limitations. Update the
78
+ changelog and user-facing documentation when behavior changes. Use the prepared
79
+ PR template when opening a pull request. Publishing a package or
80
+ changing release credentials is a separate maintainer action, not a contribution
81
+ requirement. Be respectful and keep disagreements tied to reproducible evidence.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 independent-subtitle-parser contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,195 @@
1
- # Temporary Holding Version
1
+ # Independent Subtitle Parser
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/saramkim/independent-subtitle-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/saramkim/independent-subtitle-parser/actions/workflows/ci.yml)
4
+
5
+ Parse **SRT, WebVTT, SAMI/SMI and ASS/SSA** with one TypeScript API in browsers
6
+ and Node.js. Extract timed text and language tracks while keeping the source,
7
+ raw markup and diagnostics available for inspection.
8
+
9
+ - **One data model:** millisecond timestamps, readable text, tracks and metadata.
10
+ - **Explicit recovery:** malformed input, omitted data and unsupported presentation
11
+ produce diagnostics instead of silently disappearing.
12
+ - **Portable core:** no runtime dependencies, DOM, storage, network access or renderer.
13
+
14
+ Use it for subtitle import, inspection, search and text-based playback workflows.
15
+ For styled ASS playback, use a dedicated renderer alongside the preserved source.
16
+
17
+ ## Install
18
+
19
+ Registry installation (requires a published version):
20
+
21
+ ```sh
22
+ npm install independent-subtitle-parser
23
+ ```
24
+
25
+ For an unpublished release candidate, install the supplied tarball instead:
26
+
27
+ ```sh
28
+ npm install ./independent-subtitle-parser-0.1.7.tgz
29
+ ```
30
+
31
+ To build that tarball from a source checkout:
32
+
33
+ ```sh
34
+ git clone https://github.com/saramkim/independent-subtitle-parser.git
35
+ cd independent-subtitle-parser
36
+ npm ci --ignore-scripts
37
+ npm pack
38
+ ```
39
+
40
+ Node.js **20+**; ESM with TypeScript declarations. CommonJS callers use dynamic
41
+ `import()`; synchronous `require()` is not a supported export. Browser consumers
42
+ need ES2022, and `TextDecoder` when decoding bytes. Import the package root through
43
+ your application's module/bundler setup; the examples also run without a bundler.
44
+
45
+ ## Quick start
46
+
47
+ ```ts
48
+ import { parse } from 'independent-subtitle-parser';
49
+
50
+ const source = '1\n00:00:01,250 --> 00:00:03,000\n안녕하세요\nHello';
51
+ const result = parse(source);
52
+
53
+ if (result.status === 'fatal') {
54
+ console.error(result.diagnostics); // no cues, tracks or metadata returned
55
+ } else {
56
+ console.log(result.cues[0]?.start); // 1250 (milliseconds)
57
+ console.log(result.cues[0]?.end); // 3000
58
+ console.log(result.cues[0]?.text); // 안녕하세요\nHello
59
+ console.log(result.diagnostics); // inspect partial results too
60
+ }
61
+ ```
62
+
63
+ `parse` detects the format from content. If the format is already known, pass
64
+ `parse(source, { format: 'sami' })`; `.smi` files use the `sami` option.
65
+ `detectFormat(source)` exposes the detection result without parsing cues.
66
+
67
+ ## Supported formats
68
+
69
+ | Format | Extracted and preserved | Important limits |
70
+ | --- | --- | --- |
71
+ | SRT | Times, IDs, multiline text and raw markup; diagnosed recovery of common malformed syntax | No universal SRT standard or styling renderer; ambiguous damage can remain unrecovered |
72
+ | WebVTT | Times, IDs, cue settings, text/voice/language markup and NOTE/STYLE/REGION metadata | No browser layout/CSS engine; HLS timestamp mapping is retained but not applied |
73
+ | SAMI/SMI | SYNC times, class/language tracks, clear events, BR/PRE and legacy authoring-command metadata | Final end can be unknown; no full HTML/CSS layout, Source panel or command execution |
74
+ | ASS/SSA | Dialogue, centisecond times, event fields, styles/comments, raw overrides and a reading-text projection | No fonts, positioning, drawings or karaoke rendering; declared Timer scaling is not applied |
75
+
76
+ See the [complete support profile](docs/API.md) for recovery rules and unsupported
77
+ syntax. Source order, overlaps and duplicates are retained. The library does not
78
+ sort cues, infer languages, merge tracks or choose a playback track for you.
79
+
80
+ ## Work with results
81
+
82
+ | Value | Meaning |
83
+ | --- | --- |
84
+ | `status: 'ok'` | No warning/error in the supported extraction profile; not a full conformance certificate |
85
+ | `status: 'partial'` | Usable data may coexist with recovery, omitted data or unsupported presentation; inspect diagnostics |
86
+ | `status: 'fatal'` | No cues/tracks/metadata; diagnostics and source remain available |
87
+ | `end: null` | No reliable end is known; your application must choose an explicit playback policy |
88
+
89
+ `text` is a reading projection. `rawText` retains cue markup with LF-normalized
90
+ line endings. `sourceText` is the exact string passed to `parse`. Keep original
91
+ bytes separately for byte-perfect archival. Render output as text, never raw
92
+ `innerHTML`. A warning about unapplied styling does not necessarily mean that a
93
+ caption was lost. The [diagnostic guide](docs/DIAGNOSTICS.md) explains decisions.
94
+
95
+ For multilingual subtitles, inspect `result.tracks`, let the caller choose a
96
+ track ID, then filter `result.cues` by `cue.trackId`.
97
+ [Track-selection recipes](docs/USAGE.md) cover missing language metadata and
98
+ unknown end times.
99
+
100
+ ## Decode file bytes
101
+
102
+ Decoding is separate from parsing. `decode` defaults to UTF-8 or a UTF-8/UTF-16
103
+ BOM; it does **not** guess legacy encodings. Supply a known encoding such as
104
+ `euc-kr`. Invalid byte sequences fail by default.
105
+
106
+ ```ts
107
+ import { decode, parse } from 'independent-subtitle-parser';
108
+
109
+ function readSubtitle(bytes: Uint8Array, encoding?: string) {
110
+ const decoded = decode(bytes, { encoding });
111
+ const parsed = decoded.text === null ? null : parse(decoded.text);
112
+ return { decoded, parsed }; // retain diagnostics from both stages
113
+ }
114
+
115
+ // Browser: readSubtitle(new Uint8Array(await file.arrayBuffer()))
116
+ // Node: readSubtitle(await readFile(path)) from node:fs/promises
117
+ // Legacy encoding: readSubtitle(bytes, 'euc-kr')
118
+ ```
119
+
120
+ Check file size **before** reading it. Default decoding accepts up to 16 MiB;
121
+ parsing has separate string, line, cue and diagnostic budgets. Replacement
122
+ decoding (`fatal: false`) is an explicit lossy choice: retain its warning even
123
+ when parsing succeeds. Invalid API options throw `TypeError` or `RangeError`;
124
+ file-content problems return diagnostics. Catch host I/O errors separately.
125
+
126
+ The API is synchronous and reads a whole document. Large results and JSON export
127
+ consume memory. For untrusted files or responsive UI, use lower
128
+ [resource budgets](docs/API.md) and a host-managed Worker/process with a deadline.
129
+
130
+ ## Try a real file
131
+
132
+ The package includes runnable examples. From an application with the package
133
+ installed:
134
+
135
+ ```sh
136
+ node node_modules/independent-subtitle-parser/examples/node.mjs ./movie.srt
137
+ node node_modules/independent-subtitle-parser/examples/node.mjs ./movie.smi euc-kr sami
138
+ node node_modules/independent-subtitle-parser/examples/serve.mjs
139
+ ```
140
+
141
+ The Node example writes the complete result as JSON. The server prints a local
142
+ URL for the browser example, which provides file/encoding/format selection,
143
+ track previews, diagnostics, JSON download and a cancellable module Worker.
144
+ File contents are not uploaded or stored. Preview limits do not truncate the
145
+ retained parse result. Serve over HTTP; `file:` does not reliably load modules.
146
+
147
+ In a source checkout, run `npm run build`, then `node examples/node.mjs` or
148
+ `node examples/serve.mjs`. See [consumer recipes](docs/USAGE.md) for CommonJS,
149
+ bundlers, Worker lifetime and CLI exit codes.
150
+
151
+ ## Validation and compatibility
152
+
153
+ [Hosted CI](docs/CI.md) passed **412 tests in each of nine jobs**: Node 20, 22 and
154
+ 24 on Windows, Ubuntu and macOS. Package checks install the actual tarball and
155
+ check imports, TypeScript declarations, examples and archive byte identity.
156
+ Local Chromium 156 also passed 16 consumer scenarios on Windows. The Node CI
157
+ matrix does not certify native browser behavior: the current runtime has not
158
+ been revalidated in Firefox/Safari or in browsers on the other operating systems.
159
+
160
+ The latest corpus expansion checked 23,889 events in seven real ASS/SSA files
161
+ for timing, raw text and fields; reading text was scored on a restricted 5,107
162
+ events. Three legacy SAMI files were checked separately. A ten-minute seeded
163
+ campaign completed 3,655,680 mutations and 456,960 arithmetic cases, with another
164
+ 896 mutations on full-size original files. These are bounded checks, not proof
165
+ of perfection, full rendering conformance or superiority over every parser.
166
+
167
+ Reproduce the standard checks in a source checkout with `npm run validate`.
168
+ [Corpus methodology](docs/CORPUS.md), [the latest evaluation](docs/CORPUS-017.md)
169
+ and [quality criteria](docs/QUALITY.md) explain sources, licenses, comparisons,
170
+ unscored cases and reproduction commands. Historical local evidence archives
171
+ are not included in Git or npm.
172
+
173
+ ## Contribute and report bugs
174
+
175
+ Small reproductions, corrections to format interpretation, documentation and
176
+ tests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for setup and expected
177
+ evidence, and [Reporting bugs](docs/REPORTING.md) for the report checklist.
178
+ Use the [public source repository](https://github.com/saramkim/independent-subtitle-parser),
179
+ [bug report form](https://github.com/saramkim/independent-subtitle-parser/issues/new?template=bug_report.yml)
180
+ and [pull requests](https://github.com/saramkim/independent-subtitle-parser/pulls).
181
+
182
+ During 0.x, review the [changelog](CHANGELOG.md) when upgrading and pin the
183
+ version you have validated. New diagnostics and recovery behavior can affect
184
+ consumer decisions. Full rendering, serialization, streaming and automatic
185
+ charset guessing remain outside this package's current scope.
186
+
187
+ ## License and documentation
188
+
189
+ Original package code is licensed under [MIT](LICENSE). Third-party reference
190
+ data retains its own licenses; see [third-party notices](THIRD_PARTY_NOTICES.md).
191
+
192
+ - [API reference](docs/API.md) and [diagnostic guide](docs/DIAGNOSTICS.md)
193
+ - [Usage recipes](docs/USAGE.md) and [development guide](docs/DEVELOPMENT.md)
194
+ - [Design](docs/DESIGN.md), [public format sources](docs/SOURCES.md) and [changelog](CHANGELOG.md)
195
+ - [Release checklist for maintainers](docs/RELEASE.md)
@@ -0,0 +1,60 @@
1
+ # WHATWG HTML named character reference data
2
+
3
+ Original parser code is under the MIT license in `LICENSE`. This does not
4
+ relicense third-party data. Additional development-only fixtures in
5
+ `corpus/release-015/` are from libass (ISC; local `libass/COPYING`) and Microsoft
6
+ Windows-classic-samples (MIT; local `microsoft/LICENSE`), with exact revisions,
7
+ URLs and hashes in that directory's manifest. These fixtures are not in the
8
+ npm tarball. The external sample/writer provenance and coverage limits are
9
+ documented in `docs/RELEASE.md`.
10
+
11
+ Development-only `corpus/corpus-016/` adds unchanged authored samples from
12
+ Arnavion/libjass (Apache-2.0), subtitle-rs/subtitler (Apache-2.0), and
13
+ SubtitleEdit/subtitleedit (MIT). Each upstream license is retained alongside
14
+ its samples; `manifest.json` pins revisions, original URLs and SHA-256 hashes.
15
+ No upstream parser implementation, font or image is incorporated. These samples
16
+ and licenses are excluded from the npm tarball. Additional downloaded originals
17
+ with uncertain transcript redistribution rights remain in ignored local caches;
18
+ their metadata alone is in `research/corpus-016/local-manifest.json`.
19
+
20
+ Development-only corpus: WPT files retain their BSD-3-Clause notice in
21
+ `corpus/wpt/LICENSE.md`. *Tears of Steel* subtitles are attributed to
22
+ **(CC) Blender Foundation | mango.blender.org** and volunteer translators under
23
+ CC BY 3.0 (https://creativecommons.org/licenses/by/3.0/), with unchanged original
24
+ files and provenance documented in `docs/CORPUS.md`. Corpus and comparison
25
+ dependencies are not shipped in the npm tarball. No third-party parser code is
26
+ incorporated into the runtime. The small windows-1252 mapping in `decode.ts` is
27
+ standard character data from https://encoding.spec.whatwg.org/index-windows-1252.txt
28
+ under WHATWG's BSD terms reproduced below.
29
+
30
+ `entities.reference.json` was retrieved on 2026-10-23 from
31
+ https://html.spec.whatwg.org/entities.json . `src/entities.ts` is a mechanical
32
+ projection of the names and Unicode values. No parser code
33
+ was copied. `scripts/generate-entities.mjs` reproduces that projection.
34
+
35
+ Copyright © WHATWG (Apple, Google, Mozilla, Microsoft).
36
+ The HTML standard portions incorporated into source code use the BSD 3-Clause
37
+ License, per https://whatwg.org/ipr-policy#copyright-license-for-whatwg-publications .
38
+
39
+ Redistribution and use in source and binary forms, with or without
40
+ modification, are permitted provided that the following conditions are met:
41
+
42
+ 1. Redistributions of source code must retain the above copyright notice, this
43
+ list of conditions and the following disclaimer.
44
+ 2. Redistributions in binary form must reproduce the above copyright notice,
45
+ this list of conditions and the following disclaimer in the documentation
46
+ and/or other materials provided with the distribution.
47
+ 3. Neither the name of the copyright holder nor the names of its contributors
48
+ may be used to endorse or promote products derived from this software without
49
+ specific prior written permission.
50
+
51
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
52
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
53
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
54
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
55
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
56
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
57
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
58
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
59
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
60
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,22 @@
1
+ import type { Cue, Limits, MetadataBlock, ParseResult, Severity, SubtitleFormat, Track } from './types.js';
2
+ export declare const DEFAULT_LIMITS: Readonly<Limits>;
3
+ export declare class ParseAbort extends Error {
4
+ }
5
+ export declare class ParseContext {
6
+ readonly format: SubtitleFormat;
7
+ readonly limits: Limits;
8
+ readonly cues: Cue[];
9
+ readonly tracks: Track[];
10
+ readonly metadata: MetadataBlock[];
11
+ private problem;
12
+ private truncated;
13
+ private readonly infoCodes;
14
+ private readonly diagnosticBuckets;
15
+ private diagnosticCount;
16
+ private diagnosticOrder;
17
+ constructor(format: SubtitleFormat, limits: Limits);
18
+ diagnostic(code: string, severity: Severity, message: string, line?: number): void;
19
+ fatal(code: string, message: string, line?: number): never;
20
+ addCue(cue: Cue): boolean;
21
+ result(sourceText: string, fatal?: boolean): ParseResult;
22
+ }
@@ -0,0 +1,70 @@
1
+ export const DEFAULT_LIMITS = Object.freeze({
2
+ maxInputLength: 8 * 1024 * 1024, maxLines: 250_000, maxLineLength: 1024 * 1024,
3
+ maxCues: 100_000, maxDiagnostics: 1_000,
4
+ });
5
+ export class ParseAbort extends Error {
6
+ }
7
+ const severityRank = { info: 0, warning: 1, error: 2, fatal: 3 };
8
+ export class ParseContext {
9
+ format;
10
+ limits;
11
+ cues = [];
12
+ tracks = [];
13
+ metadata = [];
14
+ problem = false;
15
+ truncated = false;
16
+ infoCodes = new Set();
17
+ diagnosticBuckets = [[], [], [], []];
18
+ diagnosticCount = 0;
19
+ diagnosticOrder = 0;
20
+ constructor(format, limits) {
21
+ this.format = format;
22
+ this.limits = limits;
23
+ }
24
+ diagnostic(code, severity, message, line) {
25
+ if (severity === 'info') {
26
+ if (this.infoCodes.has(code))
27
+ return;
28
+ this.infoCodes.add(code);
29
+ }
30
+ if (severity === 'warning' || severity === 'error' || severity === 'fatal')
31
+ this.problem = true;
32
+ const rank = severityRank[severity];
33
+ if (this.diagnosticCount >= this.limits.maxDiagnostics) {
34
+ this.truncated = true;
35
+ this.problem = true;
36
+ // Four fixed buckets keep overflow work O(1), even for large budgets.
37
+ const lowest = this.diagnosticBuckets.findIndex(bucket => bucket.length > 0);
38
+ if (rank <= lowest && severity !== 'fatal')
39
+ return;
40
+ // Keep earlier diagnostics at equal severity; replace the newest member
41
+ // of a lower severity when an actionable cause arrives.
42
+ this.diagnosticBuckets[lowest].pop();
43
+ }
44
+ else
45
+ this.diagnosticCount++;
46
+ const diagnostic = { code, severity, message, ...(line === undefined ? {} : { line }) };
47
+ this.diagnosticBuckets[rank].push({ diagnostic, order: this.diagnosticOrder++ });
48
+ }
49
+ fatal(code, message, line) {
50
+ this.diagnostic(code, 'fatal', message, line);
51
+ throw new ParseAbort(message);
52
+ }
53
+ addCue(cue) {
54
+ if (!Number.isSafeInteger(cue.start) || cue.start < 0 ||
55
+ (cue.end !== null && (!Number.isSafeInteger(cue.end) || cue.end <= cue.start))) {
56
+ this.diagnostic('INVALID_INTERVAL', 'error', 'Cue requires safe nonnegative integer milliseconds and end greater than start.', cue.source.line);
57
+ return false;
58
+ }
59
+ if (this.cues.length >= this.limits.maxCues)
60
+ this.fatal('CUE_LIMIT', 'Cue count exceeds maxCues.', cue.source.line);
61
+ this.cues.push(cue);
62
+ return true;
63
+ }
64
+ result(sourceText, fatal = false) {
65
+ const diagnostics = this.diagnosticBuckets.flat().sort((a, b) => a.order - b.order).map(entry => entry.diagnostic);
66
+ return { format: this.format, status: fatal ? 'fatal' : this.problem ? 'partial' : 'ok',
67
+ cues: fatal ? [] : this.cues, tracks: fatal ? [] : this.tracks,
68
+ metadata: fatal ? [] : this.metadata, diagnostics, diagnosticsTruncated: this.truncated, sourceText };
69
+ }
70
+ }
@@ -0,0 +1,3 @@
1
+ import type { DecodeOptions, DecodeResult } from './types.js';
2
+ /** Explicit byte decoding, independent of subtitle format and file/network I/O. */
3
+ export declare function decode(bytes: Uint8Array, options?: DecodeOptions): DecodeResult;
package/dist/decode.js ADDED
@@ -0,0 +1,51 @@
1
+ // Intrinsic getters inspect internal typed-array slots across realms. Object#toString
2
+ // and an object's public byteLength can both be overridden by callers.
3
+ const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
4
+ const typedArrayName = Object.getOwnPropertyDescriptor(typedArrayPrototype, Symbol.toStringTag).get;
5
+ const typedArrayByteLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, 'byteLength').get;
6
+ // WHATWG windows-1252 index entries 0..31 (bytes 0x80..0x9f).
7
+ // Some Node/ICU versions incorrectly return Latin-1 controls for these bytes.
8
+ // A conforming decoder's five literal C1 controls map to themselves here.
9
+ const WINDOWS_1252_C1 = '€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008dŽ\u008f\u0090‘’“”•–—˜™š›œ\u009džŸ';
10
+ /** Explicit byte decoding, independent of subtitle format and file/network I/O. */
11
+ export function decode(bytes, options = {}) {
12
+ if (!ArrayBuffer.isView(bytes) || typedArrayName.call(bytes) !== 'Uint8Array')
13
+ throw new TypeError('decode expects Uint8Array.');
14
+ const maxBytes = options.maxBytes ?? 16 * 1024 * 1024;
15
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
16
+ throw new RangeError('maxBytes must be a positive safe integer.');
17
+ if (options.fatal !== undefined && typeof options.fatal !== 'boolean')
18
+ throw new TypeError('fatal must be boolean.');
19
+ const diagnostics = [];
20
+ const fail = (code, message, encoding = null, bom = null) => ({ text: null, encoding, bom, diagnostics: [...diagnostics, { code, severity: 'fatal', message }] });
21
+ if (typedArrayByteLength.call(bytes) > maxBytes)
22
+ return fail('BYTE_LIMIT', 'Byte input exceeds maxBytes.');
23
+ // UTF-32 must not be mistaken for UTF-16LE, and is unsupported by TextDecoder.
24
+ if ((bytes[0] === 0xff && bytes[1] === 0xfe && bytes[2] === 0 && bytes[3] === 0) ||
25
+ (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 0xfe && bytes[3] === 0xff))
26
+ return fail('UNSUPPORTED_BOM', 'UTF-32 input is unsupported.');
27
+ const bom = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 'utf-8'
28
+ : bytes[0] === 0xff && bytes[1] === 0xfe ? 'utf-16le' : bytes[0] === 0xfe && bytes[1] === 0xff ? 'utf-16be' : null;
29
+ let decoder;
30
+ try {
31
+ decoder = new TextDecoder(options.encoding ?? bom ?? 'utf-8', { fatal: true });
32
+ }
33
+ catch {
34
+ return fail('UNSUPPORTED_ENCODING', 'Encoding label is unsupported by this runtime.', null, bom);
35
+ }
36
+ const encoding = decoder.encoding;
37
+ if (bom && encoding !== bom)
38
+ return fail('ENCODING_BOM_CONFLICT', 'Explicit encoding conflicts with the byte order mark.', encoding, bom);
39
+ try {
40
+ let text = decoder.decode(bytes);
41
+ if (encoding === 'windows-1252')
42
+ text = text.replace(/[\u0080-\u009f]/g, char => WINDOWS_1252_C1[char.charCodeAt(0) - 0x80]);
43
+ return { text, encoding, bom, diagnostics };
44
+ }
45
+ catch {
46
+ if (options.fatal !== false)
47
+ return fail('INVALID_ENCODING', 'Bytes are invalid for the selected encoding. Choose the correct encoding explicitly.', encoding, bom);
48
+ diagnostics.push({ code: 'DECODE_REPLACEMENT', severity: 'warning', message: 'Invalid byte sequences were replaced with U+FFFD at explicit caller request.' });
49
+ return { text: new TextDecoder(encoding).decode(bytes), encoding, bom, diagnostics };
50
+ }
51
+ }
@@ -0,0 +1,3 @@
1
+ import type { Detection } from './types.js';
2
+ /** Content-only heuristic, examining at most 64 Ki UTF-16 code units. No filename guessing. */
3
+ export declare function detectFormat(text: string): Detection;
package/dist/detect.js ADDED
@@ -0,0 +1,38 @@
1
+ /** Content-only heuristic, examining at most 64 Ki UTF-16 code units. No filename guessing. */
2
+ export function detectFormat(text) {
3
+ if (typeof text !== 'string')
4
+ throw new TypeError('detectFormat expects a string.');
5
+ const sample = text.slice(0, 65_536).replace(/^\uFEFF/, '');
6
+ if (/^WEBVTT(?:[ \t\r\n]|$)/.test(sample))
7
+ return { format: 'vtt', confidence: 'high', candidates: ['vtt'] };
8
+ let offset = 0;
9
+ while (offset < sample.length) {
10
+ while (offset < sample.length && /\s/.test(sample[offset]))
11
+ offset++;
12
+ if (!sample.startsWith('<!--', offset))
13
+ break;
14
+ const end = sample.indexOf('-->', offset + 4);
15
+ if (end < 0)
16
+ break;
17
+ offset = end + 3;
18
+ }
19
+ const leading = sample.slice(offset);
20
+ if (/^<sami(?:\s|>)/i.test(leading))
21
+ return { format: 'sami', confidence: 'high', candidates: ['sami'] };
22
+ if (/^\[(?:Script Info|Events|V4\+? Styles)\]/i.test(leading)) {
23
+ const ssa = /(?:^|[\r\n])[ \t]*(?:ScriptType[ \t]*:[ \t]*v4\.00[ \t]*$|\[V4 Styles\])/im.test(sample);
24
+ const ass = /(?:^|[\r\n])[ \t]*(?:ScriptType[ \t]*:[ \t]*v4\.00\+|\[V4\+ Styles\])/im.test(sample);
25
+ if (ssa && ass)
26
+ return { format: null, confidence: 'ambiguous', candidates: ['ass', 'ssa'] };
27
+ return { format: ssa ? 'ssa' : 'ass', confidence: ass || ssa ? 'high' : 'low', candidates: [ssa ? 'ssa' : 'ass'] };
28
+ }
29
+ if (/(?:^|[\r\n])[ \t]*\d+:\d{2}:\d{2},\d{3}[ \t]+-->/.test(sample))
30
+ return { format: 'srt', confidence: 'high', candidates: ['srt'] };
31
+ if (/(?:^|[\r\n])[ \t]*\d+:\d{2}:\d{2},\d{3}[ \t]+->/.test(sample))
32
+ return { format: 'srt', confidence: 'low', candidates: ['srt'] };
33
+ if (/(?:^|[\r\n])[ \t]*(?:\d+:)?\d{2}:\d{2}\.\d{3}[ \t]+-->/.test(sample))
34
+ return { format: null, confidence: 'ambiguous', candidates: ['srt', 'vtt'] };
35
+ if (/<sync\b[^<>]*\bstart\s*=/i.test(sample))
36
+ return { format: 'sami', confidence: 'low', candidates: ['sami'] };
37
+ return { format: null, confidence: 'none', candidates: [] };
38
+ }
@@ -0,0 +1 @@
1
+ export declare const HTML_ENTITIES: Readonly<Record<string, string>>;