inital commit
This commit is contained in:
15 files changed
+711
No files matched your search
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"valid": [
|
||||
{
|
||||
"name": "multiline and inclusive single-page range",
|
||||
"input": "1\n1 --> 4\nHello\nAgain\n\n2\n5 --> 5\nBye\n",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 4,
|
||||
"text": "Hello\nAgain"
|
||||
},
|
||||
{
|
||||
"start": 5,
|
||||
"end": 5,
|
||||
"text": "Bye"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "BOM CRLF and Japanese",
|
||||
"input": "\ufeff1\r\n1 --> 12\r\n\u3053\u3093\u306b\u3061\u306f\uff01\r\n",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 12,
|
||||
"text": "\u3053\u3093\u306b\u3061\u306f\uff01"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Arabic and literal markup",
|
||||
"input": "1\n1 --> 3\n\u0645\u0631\u062d\u0628\u0627 <b>123</b>",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 3,
|
||||
"text": "\u0645\u0631\u062d\u0628\u0627 <b>123</b>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "non-consecutive numbers sorted by page",
|
||||
"input": "8\n20-->30\nLater\n \t\n3\n1 --> 8\n Earlier \n",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 8,
|
||||
"text": " Earlier "
|
||||
},
|
||||
{
|
||||
"start": 20,
|
||||
"end": 30,
|
||||
"text": "Later"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "leading blank lines and CR",
|
||||
"input": "\n\n1\r1 --> 1\rOnly\r",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 1,
|
||||
"text": "Only"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "multiple whitespace separators",
|
||||
"input": "1\n1 --> 4\nHello\n \n\t\n\n2\n5 --> 8\nAgain\n",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 4,
|
||||
"text": "Hello"
|
||||
},
|
||||
{
|
||||
"start": 5,
|
||||
"end": 8,
|
||||
"text": "Again"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "emoji and combining marks",
|
||||
"input": "1\n1 --> 3\n\ud83d\ude00 \u062d\u0648\u0649\u0670 \u0639\u0650\u0644\u0645\u0627\u064b\n",
|
||||
"cues": [
|
||||
{
|
||||
"start": 1,
|
||||
"end": 3,
|
||||
"text": "\ud83d\ude00 \u062d\u0648\u0649\u0670 \u0639\u0650\u0644\u0645\u0627\u064b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "largest supported page",
|
||||
"input": "1\n9007199254740991 --> 9007199254740991\nLast\n",
|
||||
"cues": [
|
||||
{
|
||||
"start": 9007199254740991,
|
||||
"end": 9007199254740991,
|
||||
"text": "Last"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"invalid": [
|
||||
{
|
||||
"name": "empty",
|
||||
"input": "\ufeff\r\n"
|
||||
},
|
||||
{
|
||||
"name": "ordinary SRT",
|
||||
"input": "1\n00:00:00,000 --> 00:00:01,000\nHello"
|
||||
},
|
||||
{
|
||||
"name": "missing sequence",
|
||||
"input": "1 --> 4\nHello"
|
||||
},
|
||||
{
|
||||
"name": "zero sequence",
|
||||
"input": "0\n1 --> 4\nHello"
|
||||
},
|
||||
{
|
||||
"name": "zero page",
|
||||
"input": "1\n0 --> 4\nHello"
|
||||
},
|
||||
{
|
||||
"name": "negative page",
|
||||
"input": "1\n-1 --> 4\nHello"
|
||||
},
|
||||
{
|
||||
"name": "fractional page",
|
||||
"input": "1\n1.5 --> 4\nHello"
|
||||
},
|
||||
{
|
||||
"name": "reversed range",
|
||||
"input": "1\n4 --> 1\nHello"
|
||||
},
|
||||
{
|
||||
"name": "overlapping inclusive boundary",
|
||||
"input": "1\n1 --> 4\nHello\n\n2\n4 --> 8\nAgain"
|
||||
},
|
||||
{
|
||||
"name": "unsafe integer",
|
||||
"input": "1\n1 --> 9007199254740992\nHello"
|
||||
},
|
||||
{
|
||||
"name": "missing text",
|
||||
"input": "1\n1 --> 4\n"
|
||||
},
|
||||
{
|
||||
"name": "control character",
|
||||
"input": "1\n1 --> 4\nHello\u0000"
|
||||
},
|
||||
{
|
||||
"name": "past last page",
|
||||
"input": "1\n1 --> 4\nHello",
|
||||
"maxPage": 3
|
||||
},
|
||||
{
|
||||
"name": "too many cues",
|
||||
"input": "1\n1 --> 4\nHello\n\n2\n5 --> 8\nAgain",
|
||||
"maxCues": 1
|
||||
},
|
||||
{
|
||||
"name": "unsafe sequence number",
|
||||
"input": "9007199254740992\n1 --> 3\nHello"
|
||||
},
|
||||
{
|
||||
"name": "timing settings are not page numbers",
|
||||
"input": "1\n1 --> 3 align:start\nHello"
|
||||
},
|
||||
{
|
||||
"name": "unpaired high surrogate",
|
||||
"input": "1\n1 --> 3\n\ud800"
|
||||
},
|
||||
{
|
||||
"name": "unpaired low surrogate",
|
||||
"input": "1\n1 --> 3\n\udfff"
|
||||
},
|
||||
{
|
||||
"name": "unsafe configured page limit",
|
||||
"input": "1\n1 --> 3\nHello",
|
||||
"maxPage": 9007199254740992
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const {parse, stringify, FsrtError, MAX_FILE_BYTES} = require('../fsrt.js');
|
||||
const cases = require('./cases.json');
|
||||
|
||||
for (const item of cases.valid) test(item.name, () => {
|
||||
const cues = parse(item.input);
|
||||
assert.deepEqual(cues, item.cues);
|
||||
assert.deepEqual(parse(stringify(cues)), cues);
|
||||
});
|
||||
for (const item of cases.invalid) test(`reject ${item.name}`, () => {
|
||||
const options = {};
|
||||
if (item.maxPage) options.maxPage = item.maxPage;
|
||||
if (item.maxCues) options.maxCues = item.maxCues;
|
||||
assert.throws(() => parse(item.input, options), FsrtError);
|
||||
});
|
||||
test('all examples round-trip without changing their text', () => {
|
||||
for (const file of fs.readdirSync(path.join(__dirname, '../examples'))) {
|
||||
const text = fs.readFileSync(path.join(__dirname, '../examples', file), 'utf8');
|
||||
assert.deepEqual(parse(stringify(parse(text))), parse(text));
|
||||
}
|
||||
});
|
||||
test('reject oversized files and invalid exported ranges or blank lines', () => {
|
||||
assert.throws(() => parse('a'.repeat(MAX_FILE_BYTES + 1)), FsrtError);
|
||||
for (const cues of [[], [{start: 0, end: 1, text: 'Hi'}], [{start: 1, end: 2, text: 'Hi\n\nBye'}]])
|
||||
assert.throws(() => stringify(cues), FsrtError);
|
||||
});
|
||||
|
||||
test('UTF-8 byte limits and malformed Unicode apply to exports too', () => {
|
||||
assert.throws(() => parse('1\n1 --> 1\n' + 'あ'.repeat(Math.ceil(MAX_FILE_BYTES / 3))), FsrtError);
|
||||
assert.throws(() => stringify([{start: 1, end: 1, text: '\ud800'}]), FsrtError);
|
||||
assert.throws(() => stringify([{start: 1, end: 1, text: '\udfff'}]), FsrtError);
|
||||
assert.throws(() => parse('1\n1 --> 1\nHello', {maxPage: Number.MAX_SAFE_INTEGER + 1}), FsrtError);
|
||||
});
|
||||
|
||||
test('the same file exposes its API directly in a browser', () => {
|
||||
const browser = {TextEncoder};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../fsrt.js'), 'utf8'), browser);
|
||||
const input = '1\n1 --> 3\nこんにちは! مرحبا 😀\n';
|
||||
const cues = browser.FlipnoteSubtitle.parse(input);
|
||||
assert.equal(cues[0].start, 1);
|
||||
assert.equal(cues[0].text, 'こんにちは! مرحبا 😀');
|
||||
assert.equal(browser.FlipnoteSubtitle.stringify(cues), input);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from fsrt import FsrtError, MAX_FILE_BYTES, parse, stringify
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CASES = json.loads((ROOT / 'tests/cases.json').read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
class FsrtTests(unittest.TestCase):
|
||||
def test_valid_cases_round_trip(self):
|
||||
for item in CASES['valid']:
|
||||
with self.subTest(item['name']):
|
||||
cues = parse(item['input'])
|
||||
self.assertEqual(cues, item['cues'])
|
||||
self.assertEqual(parse(stringify(cues)), cues)
|
||||
|
||||
def test_invalid_cases(self):
|
||||
for item in CASES['invalid']:
|
||||
with self.subTest(item['name']):
|
||||
options = {key: item[source] for source, key in [('maxPage', 'max_page'), ('maxCues', 'max_cues')] if source in item}
|
||||
with self.assertRaises(FsrtError):
|
||||
parse(item['input'], **options)
|
||||
|
||||
def test_examples(self):
|
||||
for file in (ROOT / 'examples').glob('*.fsrt'):
|
||||
cues = parse(file.read_text(encoding='utf-8'))
|
||||
self.assertEqual(parse(stringify(cues)), cues)
|
||||
|
||||
def test_invalid_export_and_oversized_files(self):
|
||||
with self.assertRaises(FsrtError):
|
||||
parse('a' * (MAX_FILE_BYTES + 1))
|
||||
for cues in [[], [{'start': 0, 'end': 1, 'text': 'Hi'}], [{'start': 1, 'end': 2, 'text': 'Hi\n\nBye'}]]:
|
||||
with self.assertRaises(FsrtError):
|
||||
stringify(cues)
|
||||
|
||||
def test_utf8_byte_limits_and_malformed_unicode_apply_to_exports(self):
|
||||
with self.assertRaises(FsrtError):
|
||||
parse('1\n1 --> 1\n' + 'あ' * (MAX_FILE_BYTES // 3 + 1))
|
||||
for text in ('\ud800', '\udfff'):
|
||||
with self.subTest(text=repr(text)), self.assertRaises(FsrtError):
|
||||
stringify([{'start': 1, 'end': 1, 'text': text}])
|
||||
with self.assertRaises(FsrtError):
|
||||
parse('1\n1 --> 1\nHello', max_page=2**53)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user
