├── .github └── workflows │ └── build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md ├── Makefile ├── README.md ├── ass_tag_parser ├── __init__.py ├── ass_composer.py ├── ass_parser.py ├── ass_struct.py ├── common.py ├── draw_composer.py ├── draw_parser.py ├── draw_struct.py ├── errors.py ├── io.py ├── py.typed └── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_ass_composing.py │ ├── test_ass_parsing.py │ ├── test_documentation.py │ ├── test_draw_composing.py │ ├── test_draw_parsing.py │ └── test_module_exports.py ├── poetry.lock └── pyproject.toml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest] 12 | python-version: [3.9] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python3 -m pip install poetry 22 | poetry install 23 | - name: Run style check 24 | run: poetry run pre-commit run -a 25 | - name: Run tests 26 | run: poetry run pytest 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/pycqa/isort 4 | rev: 5.10.1 5 | hooks: 6 | - id: isort 7 | additional_dependencies: [toml] 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/pre-commit/mirrors-mypy 13 | rev: v0.942 14 | hooks: 15 | - id: mypy 16 | - repo: https://github.com/pycqa/pylint 17 | rev: v2.13.7 18 | hooks: 19 | - id: pylint 20 | additional_dependencies: [toml] 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2018 Marcin Kurczewski 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | release: 2 | rm -rf dist 3 | python3 setup.py sdist 4 | twine upload dist/* 5 | 6 | test: 7 | pytest --cov=ass_tag_parser --cov-report term-missing 8 | 9 | lint: 10 | pre-commit run -a 11 | 12 | clean: 13 | rm .coverage 14 | rm -rf .mypy_cache 15 | rm -rf .pytest_cache 16 | 17 | .PHONY: release test lint clean 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ass_tag_parser 2 | ============== 3 | 4 | [![Build](https://github.com/bubblesub/ass_tag_parser/actions/workflows/build.yml/badge.svg)](https://github.com/bubblesub/ass_tag_parser/actions/workflows/build.yml) 5 | 6 | A Python library for serialization and deserialization of ASS subtitle file 7 | format tags markup. 8 | 9 | Not to confuse with parsing `.ass` files that can be manipulated with 10 | [`ass_parser`](https://github.com/bubblesub/ass_parser). 11 | 12 | 13 | **Example**: 14 | 15 | ```python3 16 | from ass_tag_parser import parse_ass 17 | 18 | 19 | result = parse_ass( 20 | r"{\an5\pos(175,460)\fnUtopia with Oldstyle figures\fs90\bord0\blur3" 21 | r"\1c&H131313&\t(0,1000,2,\1c&H131340&)\t(1000,2000,\1c&H1015B2&" 22 | r"\blur1.4)}Attack No. 1{NOTE:アタックNo.1}" 23 | ) 24 | print(result) 25 | print(result[2].meta) 26 | ``` 27 | 28 | **Result**: 29 | 30 | ```python3 console 31 | [ 32 | AssTagListOpening(), 33 | AssTagAlignment(alignment=5, legacy=False), 34 | AssTagPosition(x=175.0, y=460.0), 35 | AssTagFontName(name="Utopia with Oldstyle figures"), 36 | AssTagFontSize(size=90.0), 37 | AssTagBorder(size=0.0), 38 | AssTagBlurEdgesGauss(weight=3.0), 39 | AssTagColor(red=19, green=19, blue=19, target=1, short=False), 40 | AssTagAnimation( 41 | tags=[AssTagColor(red=64, green=19, blue=19, target=1, short=False)], 42 | time1=0.0, 43 | time2=1000.0, 44 | acceleration=2.0, 45 | ), 46 | AssTagAnimation( 47 | tags=[ 48 | AssTagColor(red=178, green=21, blue=16, target=1, short=False), 49 | AssTagBlurEdgesGauss(weight=1.4), 50 | ], 51 | time1=1000.0, 52 | time2=2000.0, 53 | acceleration=None, 54 | ), 55 | AssTagListEnding(), 56 | AssText(text="Attack No. 1"), 57 | AssTagListOpening(), 58 | AssTagComment(text="NOTE:アタックNo.1"), 59 | AssTagListEnding(), 60 | ] 61 | Meta(start=5, end=18, text="\\pos(175,460)") 62 | ``` 63 | 64 | Starting from version 2.2, drawing commands are parsed automatically. 65 | 66 | --- 67 | 68 | ### Serializing the tree back 69 | 70 | ASS tree: `compose_ass`. Note that you don't need to supply `AssTagListOpening` 71 | nor `AssTagListEnding` tags in the input item list – this function inserts them 72 | automatically. 73 | 74 | Draw commands: `compose_draw_commands`. 75 | 76 | # Contributing 77 | 78 | ```sh 79 | # Clone the repository: 80 | git clone https://github.com/bubblesub/ass_tag_parser.git 81 | cd ass_tag_parser 82 | 83 | # Install to a local venv: 84 | poetry install 85 | 86 | # Install pre-commit hooks: 87 | poetry run pre-commit install 88 | 89 | # Enter the venv: 90 | poetry shell 91 | ``` 92 | 93 | This project uses [poetry](https://python-poetry.org/) for packaging, 94 | install instructions at [poetry#installation](https://python-poetry.org/docs/#installation) 95 | -------------------------------------------------------------------------------- /ass_tag_parser/__init__.py: -------------------------------------------------------------------------------- 1 | from .ass_composer import compose_ass 2 | from .ass_parser import ass_to_plaintext, parse_ass 3 | from .ass_struct import * 4 | from .draw_composer import compose_draw_commands 5 | from .draw_parser import parse_draw_commands 6 | from .draw_struct import * 7 | from .errors import * 8 | 9 | __all__ = [ 10 | "AssDrawCmd", 11 | "AssDrawCmdBezier", 12 | "AssDrawCmdCloseSpline", 13 | "AssDrawCmdExtendSpline", 14 | "AssDrawCmdLine", 15 | "AssDrawCmdMove", 16 | "AssDrawCmdSpline", 17 | "AssDrawPoint", 18 | "AssItem", 19 | "AssTag", 20 | "AssTagAlignment", 21 | "AssTagAlpha", 22 | "AssTagAnimation", 23 | "AssTagBaselineOffset", 24 | "AssTagBlurEdges", 25 | "AssTagBlurEdgesGauss", 26 | "AssTagBold", 27 | "AssTagBorder", 28 | "AssTagClipRectangle", 29 | "AssTagClipVector", 30 | "AssTagColor", 31 | "AssTagComment", 32 | "AssTagDraw", 33 | "AssTagFade", 34 | "AssTagFadeComplex", 35 | "AssTagFontEncoding", 36 | "AssTagFontName", 37 | "AssTagFontSize", 38 | "AssTagFontXScale", 39 | "AssTagFontYScale", 40 | "AssTagItalic", 41 | "AssTagKaraoke", 42 | "AssTagLetterSpacing", 43 | "AssTagListEnding", 44 | "AssTagListOpening", 45 | "AssTagMove", 46 | "AssTagPosition", 47 | "AssTagResetStyle", 48 | "AssTagRotationOrigin", 49 | "AssTagShadow", 50 | "AssTagStrikeout", 51 | "AssTagUnderline", 52 | "AssTagWrapStyle", 53 | "AssTagXBorder", 54 | "AssTagXRotation", 55 | "AssTagXShadow", 56 | "AssTagXShear", 57 | "AssTagYBorder", 58 | "AssTagYRotation", 59 | "AssTagYShadow", 60 | "AssTagYShear", 61 | "AssTagZRotation", 62 | "AssText", 63 | "BadAssTagArgument", 64 | "BaseError", 65 | "ParseError", 66 | "UnexpectedCurlyBrace", 67 | "UnknownTag", 68 | "UnterminatedCurlyBrace", 69 | "compose_ass", 70 | "compose_draw_commands", 71 | "parse_ass", 72 | "parse_draw_commands", 73 | "ass_to_plaintext", 74 | ] 75 | -------------------------------------------------------------------------------- /ass_tag_parser/ass_composer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from ass_tag_parser.ass_struct import ( 4 | AssItem, 5 | AssTag, 6 | AssTagAlignment, 7 | AssTagAlpha, 8 | AssTagAnimation, 9 | AssTagBaselineOffset, 10 | AssTagBlurEdges, 11 | AssTagBlurEdgesGauss, 12 | AssTagBold, 13 | AssTagBorder, 14 | AssTagClipRectangle, 15 | AssTagClipVector, 16 | AssTagColor, 17 | AssTagComment, 18 | AssTagDraw, 19 | AssTagFade, 20 | AssTagFadeComplex, 21 | AssTagFontEncoding, 22 | AssTagFontName, 23 | AssTagFontSize, 24 | AssTagFontXScale, 25 | AssTagFontYScale, 26 | AssTagItalic, 27 | AssTagKaraoke, 28 | AssTagLetterSpacing, 29 | AssTagListEnding, 30 | AssTagListOpening, 31 | AssTagMove, 32 | AssTagPosition, 33 | AssTagResetStyle, 34 | AssTagRotationOrigin, 35 | AssTagShadow, 36 | AssTagStrikeout, 37 | AssTagUnderline, 38 | AssTagWrapStyle, 39 | AssTagXBorder, 40 | AssTagXRotation, 41 | AssTagXShadow, 42 | AssTagXShear, 43 | AssTagYBorder, 44 | AssTagYRotation, 45 | AssTagYShadow, 46 | AssTagYShear, 47 | AssTagZRotation, 48 | AssText, 49 | ) 50 | from ass_tag_parser.common import smart_bool, smart_float, smart_int, smart_str 51 | from ass_tag_parser.draw_composer import compose_draw_commands 52 | from ass_tag_parser.io import MyIO 53 | 54 | 55 | @dataclass 56 | class _ComposeContext: 57 | io: MyIO 58 | autoinsert: bool 59 | opened: bool = False 60 | 61 | 62 | def visitor(ctx: _ComposeContext, item: AssItem) -> None: 63 | # pylint: disable=too-many-statements,too-many-branches 64 | 65 | if isinstance(item, AssTag) and ctx.autoinsert and not ctx.opened: 66 | ctx.io.write("{") 67 | ctx.opened = True 68 | elif isinstance(item, AssText) and ctx.autoinsert and ctx.opened: 69 | ctx.io.write("}") 70 | ctx.opened = False 71 | 72 | if isinstance(item, AssTagListOpening): 73 | if not ctx.autoinsert: 74 | ctx.io.write("{") 75 | elif isinstance(item, AssTagListEnding): 76 | if not ctx.autoinsert: 77 | ctx.io.write("}") 78 | 79 | elif isinstance(item, AssTagAnimation): 80 | ctx.io.write("\\t(") 81 | if item.time1 is not None and item.time2 is not None: 82 | ctx.io.write( 83 | f"{smart_float(item.time1)},{smart_float(item.time2)}," 84 | ) 85 | if item.acceleration is not None: 86 | ctx.io.write(f"{smart_float(item.acceleration)},") 87 | for subitem in item.tags: 88 | visitor(ctx, subitem) 89 | ctx.io.write(")") 90 | 91 | elif isinstance(item, AssText): 92 | ctx.io.write(item.text) 93 | elif isinstance(item, AssTagComment): 94 | ctx.io.write(f"{item.text}") 95 | elif isinstance(item, AssTagBaselineOffset): 96 | ctx.io.write(f"\\pbo{smart_float(item.y)}") 97 | elif isinstance(item, AssTagDraw): 98 | ctx.io.write(f"\\p{smart_int(item.scale)}") 99 | ctx.io.write("}") 100 | ctx.io.write(compose_draw_commands(item.path)) 101 | ctx.io.write("{\\p0") 102 | elif isinstance(item, AssTagAlignment): 103 | if item.legacy: 104 | value = item.alignment 105 | if value in {4, 5, 6}: 106 | value += 1 107 | elif value in {7, 8, 9}: 108 | value += 2 109 | ctx.io.write(f"\\a{smart_int(value)}") 110 | else: 111 | ctx.io.write(f"\\an{smart_int(item.alignment)}") 112 | elif isinstance(item, AssTagFade): 113 | ctx.io.write("\\fad(") 114 | ctx.io.write(f"{smart_float(item.time1)},{smart_float(item.time2)}") 115 | ctx.io.write(")") 116 | elif isinstance(item, AssTagFadeComplex): 117 | ctx.io.write("\\fade(") 118 | ctx.io.write(f"{item.alpha1},{item.alpha2},{item.alpha3},") 119 | ctx.io.write(f"{smart_float(item.time1)},{smart_float(item.time2)},") 120 | ctx.io.write(f"{smart_float(item.time3)},{smart_float(item.time4)}") 121 | ctx.io.write(")") 122 | elif isinstance(item, AssTagMove): 123 | ctx.io.write("\\move(") 124 | ctx.io.write(f"{smart_float(item.x1)},{smart_float(item.y1)},") 125 | ctx.io.write(f"{smart_float(item.x2)},{smart_float(item.y2)}") 126 | if item.time1 is not None and item.time2 is not None: 127 | ctx.io.write( 128 | f",{smart_float(item.time1)},{smart_float(item.time2)}" 129 | ) 130 | ctx.io.write(")") 131 | elif isinstance(item, AssTagColor): 132 | ctx.io.write("\\c" if item.short else f"\\{item.target}c") 133 | if ( 134 | item.red is not None 135 | and item.green is not None 136 | and item.blue is not None 137 | ): 138 | ctx.io.write("&H") 139 | ctx.io.write(f"{item.blue:02X}") 140 | ctx.io.write(f"{item.green:02X}") 141 | ctx.io.write(f"{item.red:02X}") 142 | ctx.io.write("&") 143 | elif isinstance(item, AssTagAlpha): 144 | ctx.io.write("\\alpha" if item.target == 0 else f"\\{item.target}a") 145 | if item.value is not None: 146 | ctx.io.write("&H") 147 | ctx.io.write(f"{item.value:02X}") 148 | ctx.io.write("&") 149 | elif isinstance(item, AssTagResetStyle): 150 | ctx.io.write(f"\\r{smart_str(item.style)}") 151 | elif isinstance(item, AssTagBorder): 152 | ctx.io.write(f"\\bord{smart_float(item.size)}") 153 | elif isinstance(item, AssTagXBorder): 154 | ctx.io.write(f"\\xbord{smart_float(item.size)}") 155 | elif isinstance(item, AssTagYBorder): 156 | ctx.io.write(f"\\ybord{smart_float(item.size)}") 157 | elif isinstance(item, AssTagShadow): 158 | ctx.io.write(f"\\shad{smart_float(item.size)}") 159 | elif isinstance(item, AssTagXShadow): 160 | ctx.io.write(f"\\xshad{smart_float(item.size)}") 161 | elif isinstance(item, AssTagYShadow): 162 | ctx.io.write(f"\\yshad{smart_float(item.size)}") 163 | elif isinstance(item, AssTagXRotation): 164 | ctx.io.write(f"\\frx{smart_float(item.angle)}") 165 | elif isinstance(item, AssTagYRotation): 166 | ctx.io.write(f"\\fry{smart_float(item.angle)}") 167 | elif isinstance(item, AssTagZRotation): 168 | ctx.io.write( 169 | f"\\fr{smart_float(item.angle)}" 170 | if item.short 171 | else f"\\frz{smart_float(item.angle)}" 172 | ) 173 | elif isinstance(item, AssTagRotationOrigin): 174 | ctx.io.write(f"\\org({smart_float(item.x)},{smart_float(item.y)})") 175 | elif isinstance(item, AssTagPosition): 176 | ctx.io.write(f"\\pos({smart_float(item.x)},{smart_float(item.y)})") 177 | elif isinstance(item, AssTagXShear): 178 | ctx.io.write(f"\\fax{smart_float(item.value)}") 179 | elif isinstance(item, AssTagYShear): 180 | ctx.io.write(f"\\fay{smart_float(item.value)}") 181 | elif isinstance(item, AssTagFontName): 182 | ctx.io.write(f"\\fn{smart_str(item.name)}") 183 | elif isinstance(item, AssTagFontSize): 184 | ctx.io.write(f"\\fs{smart_float(item.size)}") 185 | elif isinstance(item, AssTagFontEncoding): 186 | ctx.io.write(f"\\fe{smart_int(item.encoding)}") 187 | elif isinstance(item, AssTagLetterSpacing): 188 | ctx.io.write(f"\\fsp{smart_float(item.spacing)}") 189 | elif isinstance(item, AssTagFontXScale): 190 | ctx.io.write(f"\\fscx{smart_float(item.scale)}") 191 | elif isinstance(item, AssTagFontYScale): 192 | ctx.io.write(f"\\fscy{smart_float(item.scale)}") 193 | elif isinstance(item, AssTagBlurEdges): 194 | ctx.io.write(f"\\be{smart_float(item.times)}") 195 | elif isinstance(item, AssTagBlurEdgesGauss): 196 | ctx.io.write(f"\\blur{smart_float(item.weight)}") 197 | elif isinstance(item, AssTagKaraoke): 198 | tag = {1: "\\k", 2: "\\K", 3: "\\kf", 4: "\\ko"}[item.karaoke_type] 199 | ctx.io.write(f"{tag}{smart_float(item.duration / 10)}") 200 | elif isinstance(item, AssTagItalic): 201 | ctx.io.write(f"\\i{smart_bool(item.enabled)}") 202 | elif isinstance(item, AssTagUnderline): 203 | ctx.io.write(f"\\u{smart_bool(item.enabled)}") 204 | elif isinstance(item, AssTagStrikeout): 205 | ctx.io.write(f"\\s{smart_bool(item.enabled)}") 206 | elif isinstance(item, AssTagWrapStyle): 207 | ctx.io.write(f"\\q{smart_int(item.style)}") 208 | elif isinstance(item, AssTagBold): 209 | ctx.io.write( 210 | "\\b" 211 | + ( 212 | str(item.weight) 213 | if item.weight is not None 214 | else smart_bool(item.enabled) 215 | ) 216 | ) 217 | elif isinstance(item, AssTagClipRectangle): 218 | ctx.io.write("\\iclip" if item.inverse else "\\clip") 219 | ctx.io.write(f"({smart_float(item.x1)},{smart_float(item.y1)},") 220 | ctx.io.write(f"{smart_float(item.x2)},{smart_float(item.y2)})") 221 | elif isinstance(item, AssTagClipVector): 222 | ctx.io.write("\\iclip" if item.inverse else "\\clip") 223 | ctx.io.write("(") 224 | if item.scale is not None: 225 | ctx.io.write(f"{item.scale},") 226 | ctx.io.write(compose_draw_commands(item.path)) 227 | ctx.io.write(")") 228 | else: 229 | raise NotImplementedError(f"not implemented ({type(item)})") 230 | 231 | 232 | def compose_ass(ass_line: list[AssItem], autoinsert: bool = True) -> str: 233 | ctx = _ComposeContext(io=MyIO(), autoinsert=autoinsert) 234 | 235 | for item in ass_line: 236 | visitor(ctx, item) 237 | 238 | if ctx.opened: 239 | ctx.io.write("}") 240 | return ctx.io.text 241 | -------------------------------------------------------------------------------- /ass_tag_parser/ass_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass 3 | from functools import cache 4 | from typing import Any, Iterable, Optional, Union 5 | 6 | from ass_tag_parser.ass_struct import ( 7 | AssItem, 8 | AssTag, 9 | AssTagAlignment, 10 | AssTagAlpha, 11 | AssTagAnimation, 12 | AssTagBaselineOffset, 13 | AssTagBlurEdges, 14 | AssTagBlurEdgesGauss, 15 | AssTagBold, 16 | AssTagBorder, 17 | AssTagClipRectangle, 18 | AssTagClipVector, 19 | AssTagColor, 20 | AssTagComment, 21 | AssTagDraw, 22 | AssTagFade, 23 | AssTagFadeComplex, 24 | AssTagFontEncoding, 25 | AssTagFontName, 26 | AssTagFontSize, 27 | AssTagFontXScale, 28 | AssTagFontYScale, 29 | AssTagItalic, 30 | AssTagKaraoke, 31 | AssTagLetterSpacing, 32 | AssTagListEnding, 33 | AssTagListOpening, 34 | AssTagMove, 35 | AssTagPosition, 36 | AssTagResetStyle, 37 | AssTagRotationOrigin, 38 | AssTagShadow, 39 | AssTagStrikeout, 40 | AssTagUnderline, 41 | AssTagWrapStyle, 42 | AssTagXBorder, 43 | AssTagXRotation, 44 | AssTagXShadow, 45 | AssTagXShear, 46 | AssTagYBorder, 47 | AssTagYRotation, 48 | AssTagYShadow, 49 | AssTagYShear, 50 | AssTagZRotation, 51 | AssText, 52 | ) 53 | from ass_tag_parser.common import Meta 54 | from ass_tag_parser.draw_parser import parse_draw_commands 55 | from ass_tag_parser.errors import ( 56 | BadAssTagArgument, 57 | ParseError, 58 | UnexpectedCurlyBrace, 59 | UnknownTag, 60 | UnterminatedCurlyBrace, 61 | ) 62 | from ass_tag_parser.io import MyIO 63 | 64 | 65 | @dataclass 66 | class _ParseContext: 67 | io: MyIO 68 | drawing_tag: Optional[AssTagDraw] = None 69 | 70 | 71 | def _single_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[str]]: 72 | arg = "" 73 | if ctx.io.peek(1) == "(": 74 | raise BadAssTagArgument( 75 | ctx.io.global_pos, f"{tag} doesn't take complex arguments" 76 | ) 77 | while not ctx.io.eof and ctx.io.peek(1) not in r"\()": 78 | arg += ctx.io.read(1) 79 | if not arg: 80 | return (None,) 81 | return (arg,) 82 | 83 | 84 | def _complex_args( 85 | ctx: _ParseContext, tag: str, valid_counts: set[int] 86 | ) -> tuple[tuple[str, int], ...]: 87 | # pylint: disable=too-many-branches 88 | if ctx.io.read(1) != "(": 89 | raise BadAssTagArgument(ctx.io.global_pos, "expected brace") 90 | 91 | brackets = 1 92 | args: list[tuple[str, int]] = [] 93 | arg = "" 94 | arg_start = ctx.io.global_pos 95 | while brackets > 0: 96 | char = ctx.io.read(1) 97 | if char == "(": 98 | brackets += 1 99 | arg += char 100 | elif char == ")": 101 | brackets -= 1 102 | if brackets > 0: 103 | arg += char 104 | else: 105 | args.append((arg, arg_start)) 106 | break 107 | elif char == ",": 108 | if brackets > 1: 109 | arg += char 110 | else: 111 | args.append((arg, arg_start)) 112 | arg = "" 113 | arg_start = ctx.io.global_pos 114 | elif char in "{}": 115 | raise UnexpectedCurlyBrace(ctx.io.global_pos) 116 | elif ctx.io.eof: 117 | raise BadAssTagArgument(ctx.io.global_pos, "unterminated brace") 118 | else: 119 | arg += char 120 | 121 | if len(args) not in valid_counts: 122 | raise BadAssTagArgument( 123 | ctx.io.global_pos, 124 | f"{tag} takes {' or '.join(map(str, sorted(valid_counts)))} " 125 | f"arguments (got {len(args)})", 126 | ) 127 | 128 | return tuple(args) 129 | 130 | 131 | def _bool_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[bool]]: 132 | (arg,) = _single_arg(ctx, tag) 133 | if not arg: 134 | return (None,) 135 | if arg == "0": 136 | return (False,) 137 | if arg == "1": 138 | return (True,) 139 | raise BadAssTagArgument(ctx.io.global_pos, f"{tag} requires a boolean") 140 | 141 | 142 | def _int_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[int]]: 143 | (arg,) = _single_arg(ctx, tag) 144 | if not arg: 145 | return (None,) 146 | try: 147 | return (int(arg),) 148 | except ValueError as exc: 149 | raise BadAssTagArgument( 150 | ctx.io.global_pos, f"{tag} requires an integer" 151 | ) from exc 152 | 153 | 154 | def _positive_int_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[int]]: 155 | (value,) = _int_arg(ctx, tag) 156 | if value is not None and value < 0: 157 | raise BadAssTagArgument( 158 | ctx.io.global_pos, f"{tag} takes only positive integers" 159 | ) 160 | return (value,) 161 | 162 | 163 | def _float_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[float]]: 164 | (arg,) = _single_arg(ctx, tag) 165 | if not arg: 166 | return (None,) 167 | try: 168 | return (float(arg),) 169 | except ValueError as exc: 170 | raise BadAssTagArgument( 171 | ctx.io.global_pos, f"{tag} requires a decimal" 172 | ) from exc 173 | 174 | 175 | def _positive_float_arg( 176 | ctx: _ParseContext, tag: str 177 | ) -> tuple[Optional[float]]: 178 | (value,) = _float_arg(ctx, tag) 179 | if value is not None and value < 0: 180 | raise BadAssTagArgument( 181 | ctx.io.global_pos, f"{tag} takes only positive decimals" 182 | ) 183 | return (value,) 184 | 185 | 186 | def _pos_args(ctx: _ParseContext, tag: str) -> tuple[float, float]: 187 | args = _complex_args(ctx, tag, {2}) 188 | 189 | try: 190 | coords = ( 191 | float(args[0][0]), 192 | float(args[1][0]), 193 | ) 194 | except ValueError as exc: 195 | raise BadAssTagArgument( 196 | ctx.io.global_pos, f"{tag} takes only decimal arguments" 197 | ) from exc 198 | 199 | return coords 200 | 201 | 202 | def _fade_simple_args(ctx: _ParseContext, tag: str) -> tuple[float, float]: 203 | args = list(_complex_args(ctx, tag, {2})) 204 | 205 | try: 206 | times = ( 207 | float(args[0][0]), 208 | float(args[1][0]), 209 | ) 210 | if any(time < 0 for time in times): 211 | raise BadAssTagArgument( 212 | ctx.io.global_pos, f"{tag} takes only positive times" 213 | ) 214 | except ValueError as exc: 215 | raise BadAssTagArgument( 216 | ctx.io.global_pos, f"{tag} requires decimal times" 217 | ) from exc 218 | 219 | return times 220 | 221 | 222 | def _fade_complex_args( 223 | ctx: _ParseContext, tag: str 224 | ) -> tuple[int, int, int, float, float, float, float]: 225 | args = list(_complex_args(ctx, tag, {7})) 226 | 227 | try: 228 | alpha_values = ( 229 | int(args[0][0]), 230 | int(args[1][0]), 231 | int(args[2][0]), 232 | ) 233 | if any(value < 0 for value in alpha_values): 234 | raise BadAssTagArgument( 235 | ctx.io.global_pos, f"{tag} takes only positive alpha values" 236 | ) 237 | except ValueError as exc: 238 | raise BadAssTagArgument( 239 | ctx.io.global_pos, f"{tag} requires integer alpha values" 240 | ) from exc 241 | 242 | try: 243 | times = ( 244 | float(args[3][0]), 245 | float(args[4][0]), 246 | float(args[5][0]), 247 | float(args[6][0]), 248 | ) 249 | if any(time < 0 for time in times): 250 | raise BadAssTagArgument( 251 | ctx.io.global_pos, f"{tag} takes only positive times" 252 | ) 253 | except ValueError as exc: 254 | raise BadAssTagArgument( 255 | ctx.io.global_pos, f"{tag} requires decimal times" 256 | ) from exc 257 | 258 | return alpha_values + times 259 | 260 | 261 | def _bold_arg( 262 | ctx: _ParseContext, tag: str 263 | ) -> tuple[Optional[bool], Optional[float]]: 264 | (weight,) = _positive_int_arg(ctx, tag) 265 | return ( 266 | None if weight is None else weight != 0, 267 | None if weight is None or weight in {0, 1} else weight, 268 | ) 269 | 270 | 271 | def _alignment_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[int], bool]: 272 | (value,) = _positive_int_arg(ctx, tag) 273 | legacy = tag == r"\a" 274 | if value is None: 275 | return (None, legacy) 276 | if legacy: 277 | if value in (1, 2, 3): 278 | pass 279 | elif value in (5, 6, 7): 280 | value -= 1 281 | elif value in (9, 10, 11): 282 | value -= 2 283 | else: 284 | raise BadAssTagArgument( 285 | ctx.io.global_pos, f"{tag} expects 1-3, 5-7 or 9-11" 286 | ) 287 | elif value not in range(1, 10): 288 | raise BadAssTagArgument(ctx.io.global_pos, f"{tag} expects 1-9") 289 | return (value, legacy) 290 | 291 | 292 | def _color_arg( 293 | ctx: _ParseContext, tag: str 294 | ) -> tuple[Optional[int], Optional[int], Optional[int], int, bool]: 295 | start = ctx.io.global_pos 296 | (arg,) = _single_arg(ctx, tag) 297 | io = MyIO(arg or "", start, ctx.io.global_text) 298 | 299 | short = tag == r"\c" 300 | target = {r"\1c": 1, r"\2c": 2, r"\3c": 3, r"\4c": 4, r"\c": 1}[tag] 301 | if io.eof: 302 | return (None, None, None, target, short) 303 | 304 | if io.read(1) != "&": 305 | raise BadAssTagArgument(io.global_pos, "expected ampersand") 306 | if io.read(1) != "H": 307 | raise BadAssTagArgument(io.global_pos, "expected uppercase H") 308 | 309 | rgb = [0, 0, 0] 310 | for i in range(3): 311 | try: 312 | rgb[i] = int(io.read(2), 16) 313 | except ValueError as exc: 314 | raise BadAssTagArgument( 315 | io.global_pos, "expected hexadecimal number" 316 | ) from exc 317 | 318 | if io.read(1) != "&": 319 | raise BadAssTagArgument(io.global_pos, "expected ampersand") 320 | 321 | if not io.eof: 322 | raise BadAssTagArgument(io.global_pos, "extra data") 323 | 324 | return (rgb[2], rgb[1], rgb[0], target, short) 325 | 326 | 327 | def _alpha_arg(ctx: _ParseContext, tag: str) -> tuple[Optional[int], int]: 328 | start = ctx.io.global_pos 329 | (arg,) = _single_arg(ctx, tag) 330 | io = MyIO(arg or "", start, ctx.io.global_text) 331 | 332 | target = {r"\1a": 1, r"\2a": 2, r"\3a": 3, r"\4a": 4, r"\alpha": 0}[tag] 333 | if io.peek(1) != "&": 334 | return (None, target) 335 | io.skip(1) 336 | 337 | if io.read(1) != "H": 338 | raise BadAssTagArgument(io.global_pos, "expected uppercase H") 339 | 340 | try: 341 | value = int(io.read(2), 16) 342 | except ValueError as exc: 343 | raise BadAssTagArgument( 344 | io.global_pos, "expected hexadecimal number" 345 | ) from exc 346 | 347 | if io.read(1) != "&": 348 | raise BadAssTagArgument(io.global_pos, "expected ampersand") 349 | 350 | if not io.eof: 351 | raise BadAssTagArgument(io.global_pos, "extra data") 352 | 353 | return (value, target) 354 | 355 | 356 | def _karaoke_arg(ctx: _ParseContext, tag: str) -> tuple[float, int]: 357 | karaoke_type = {"\\k": 1, "\\K": 2, "\\kf": 3, "\\ko": 4}[tag] 358 | (value,) = _positive_float_arg(ctx, tag) 359 | if value is None: 360 | raise BadAssTagArgument( 361 | ctx.io.global_pos, f"{tag} requires an argument" 362 | ) 363 | return (value * 10, karaoke_type) 364 | 365 | 366 | def _wrap_style_arg(ctx: _ParseContext, tag: str) -> Any: 367 | (value,) = _positive_int_arg(ctx, tag) 368 | if value not in range(4): 369 | raise BadAssTagArgument( 370 | ctx.io.global_pos, f"{tag} expects 0, 1, 2 or 3" 371 | ) 372 | return (value,) 373 | 374 | 375 | def _move_args( 376 | ctx: _ParseContext, tag: str 377 | ) -> tuple[float, float, float, float, Optional[float], Optional[float]]: 378 | args = list(_complex_args(ctx, tag, {4, 6})) 379 | 380 | try: 381 | coords = ( 382 | float(args[0][0]), 383 | float(args[1][0]), 384 | float(args[2][0]), 385 | float(args[3][0]), 386 | ) 387 | except ValueError as exc: 388 | raise BadAssTagArgument( 389 | ctx.io.global_pos, f"{tag} requires decimal coordinates" 390 | ) from exc 391 | 392 | speed: tuple[Optional[float], Optional[float]] = (None, None) 393 | if len(args) == 6: 394 | try: 395 | speed = (float(args[4][0]), float(args[5][0])) 396 | if any(time < 0 for time in speed): 397 | raise BadAssTagArgument( 398 | ctx.io.global_pos, f"{tag} takes only positive times" 399 | ) 400 | except ValueError as exc: 401 | raise BadAssTagArgument( 402 | ctx.io.global_pos, f"{tag} requires decimal times" 403 | ) from exc 404 | 405 | return coords + speed 406 | 407 | 408 | def _animation_args( 409 | ctx: _ParseContext, tag: str 410 | ) -> tuple[list[AssTag], Optional[float], Optional[float], Optional[float]]: 411 | acceleration: Union[None, str, float] 412 | time1: Union[None, str, float] 413 | time2: Union[None, str, float] 414 | tags: Union[None, str, list[AssTag]] 415 | 416 | args = _complex_args(ctx, tag, {1, 2, 3, 4}) 417 | 418 | if len(args) == 1: 419 | acceleration = None 420 | time1 = None 421 | time2 = None 422 | tags, tags_start = args[0] 423 | elif len(args) == 2: 424 | acceleration = args[0][0] 425 | time1 = None 426 | time2 = None 427 | tags, tags_start = args[1] 428 | elif len(args) == 3: 429 | acceleration = None 430 | time1 = args[0][0] 431 | time2 = args[1][0] 432 | tags, tags_start = args[2] 433 | else: 434 | time1 = args[0][0] 435 | time2 = args[1][0] 436 | acceleration = args[2][0] 437 | tags, tags_start = args[3] 438 | 439 | try: 440 | acceleration = None if acceleration is None else float(acceleration) 441 | except ValueError as exc: 442 | raise BadAssTagArgument( 443 | ctx.io.global_pos, f"{tag} requires decimal acceleration value" 444 | ) from exc 445 | if acceleration is not None and acceleration < 0: 446 | raise BadAssTagArgument( 447 | ctx.io.global_pos, f"{tag} takes only positive acceleration value" 448 | ) 449 | 450 | try: 451 | time1 = None if time1 is None else float(time1) 452 | time2 = None if time2 is None else float(time2) 453 | except ValueError as exc: 454 | raise BadAssTagArgument( 455 | ctx.io.global_pos, f"{tag} requires decimal times" 456 | ) from exc 457 | if (time1 is not None and time1 < 0) or (time2 is not None and time2 < 0): 458 | raise BadAssTagArgument( 459 | ctx.io.global_pos, f"{tag} takes only positive times" 460 | ) 461 | 462 | old_io = ctx.io 463 | ctx.io = MyIO(tags, tags_start, ctx.io.global_text) 464 | tags = list(_parse_ass_tags(ctx)) 465 | ctx.io = old_io 466 | 467 | return tags, time1, time2, acceleration 468 | 469 | 470 | _PARSING_MAP = [ 471 | (r"\bord", AssTagBorder, _positive_float_arg), 472 | (r"\xbord", AssTagXBorder, _positive_float_arg), 473 | (r"\ybord", AssTagYBorder, _positive_float_arg), 474 | (r"\shad", AssTagShadow, _positive_float_arg), 475 | (r"\xshad", AssTagXShadow, _positive_float_arg), 476 | (r"\yshad", AssTagYShadow, _positive_float_arg), 477 | (r"\fsp", AssTagLetterSpacing, _float_arg), 478 | (r"\fax", AssTagXShear, _float_arg), 479 | (r"\fay", AssTagYShear, _float_arg), 480 | (r"\pos", AssTagPosition, _pos_args), 481 | (r"\org", AssTagRotationOrigin, _pos_args), 482 | (r"\move", AssTagMove, _move_args), 483 | (r"\fade", AssTagFadeComplex, _fade_complex_args), 484 | (r"\fad", AssTagFade, _fade_simple_args), 485 | (r"\frx", AssTagXRotation, _float_arg), 486 | (r"\fry", AssTagYRotation, _float_arg), 487 | ( 488 | r"\frz", 489 | AssTagZRotation, 490 | lambda ctx, tag: tuple(list(_float_arg(ctx, tag)) + [False]), 491 | ), 492 | ( 493 | r"\fr", 494 | AssTagZRotation, 495 | lambda ctx, tag: tuple(list(_float_arg(ctx, tag)) + [True]), 496 | ), 497 | (r"\fn", AssTagFontName, _single_arg), 498 | (r"\fscx", AssTagFontXScale, _positive_float_arg), 499 | (r"\fscy", AssTagFontYScale, _positive_float_arg), 500 | (r"\fs", AssTagFontSize, _positive_float_arg), 501 | (r"\fe", AssTagFontEncoding, _positive_int_arg), 502 | (r"\blur", AssTagBlurEdgesGauss, _positive_float_arg), 503 | (r"\be", AssTagBlurEdges, _positive_float_arg), 504 | (r"\i", AssTagItalic, _bool_arg), 505 | (r"\u", AssTagUnderline, _bool_arg), 506 | (r"\s", AssTagStrikeout, _bool_arg), 507 | (r"\b", AssTagBold, _bold_arg), 508 | (r"\kf", AssTagKaraoke, _karaoke_arg), 509 | (r"\ko", AssTagKaraoke, _karaoke_arg), 510 | (r"\k", AssTagKaraoke, _karaoke_arg), 511 | (r"\K", AssTagKaraoke, _karaoke_arg), 512 | (r"\q", AssTagWrapStyle, _wrap_style_arg), 513 | (r"\r", AssTagResetStyle, _single_arg), 514 | (r"\alpha", AssTagAlpha, _alpha_arg), 515 | (r"\1a", AssTagAlpha, _alpha_arg), 516 | (r"\2a", AssTagAlpha, _alpha_arg), 517 | (r"\3a", AssTagAlpha, _alpha_arg), 518 | (r"\4a", AssTagAlpha, _alpha_arg), 519 | (r"\1c", AssTagColor, _color_arg), 520 | (r"\2c", AssTagColor, _color_arg), 521 | (r"\3c", AssTagColor, _color_arg), 522 | (r"\4c", AssTagColor, _color_arg), 523 | (r"\c", AssTagColor, _color_arg), 524 | (r"\an", AssTagAlignment, _alignment_arg), 525 | (r"\a", AssTagAlignment, _alignment_arg), 526 | (r"\pbo", AssTagBaselineOffset, _float_arg), 527 | (r"\p", AssTagDraw, _positive_int_arg), 528 | (r"\t", AssTagAnimation, _animation_args), 529 | ] 530 | 531 | 532 | def _parse_ass_tag(ctx: _ParseContext) -> AssTag: 533 | i = ctx.io.global_pos 534 | 535 | for prefix in [r"\clip", r"\iclip"]: 536 | if ctx.io.peek(len(prefix)) != prefix: 537 | continue 538 | ctx.io.skip(len(prefix)) 539 | inverse = prefix == r"\iclip" 540 | args = _complex_args(ctx, prefix, {1, 2, 4}) 541 | 542 | scale: Optional[int] 543 | if len(args) == 1: 544 | scale = None 545 | path = args[0][0] 546 | return AssTagClipVector( 547 | scale=scale, path=parse_draw_commands(path), inverse=inverse 548 | ) 549 | 550 | if len(args) == 2: 551 | scale_str = args[0][0] 552 | path = args[1][0] 553 | try: 554 | scale = int(scale_str) 555 | except ValueError as exc: 556 | raise BadAssTagArgument( 557 | ctx.io.global_pos, f"{prefix} scale must be integer" 558 | ) from exc 559 | if scale < 0: 560 | raise BadAssTagArgument( 561 | ctx.io.global_pos, 562 | f"{prefix} scale must be positive integer", 563 | ) 564 | return AssTagClipVector( 565 | scale=scale, path=parse_draw_commands(path), inverse=inverse 566 | ) 567 | 568 | if len(args) == 4: 569 | try: 570 | corners = [float(arg[0]) for arg in args] 571 | except ValueError as exc: 572 | raise BadAssTagArgument( 573 | ctx.io.global_pos, 574 | f"{prefix} takes only decimal coordinates", 575 | ) from exc 576 | return AssTagClipRectangle( 577 | corners[0], corners[1], corners[2], corners[3], inverse=inverse 578 | ) 579 | 580 | assert False 581 | 582 | for prefix, cls, arg_func in _PARSING_MAP: 583 | if ctx.io.peek(len(prefix)) == prefix: 584 | ctx.io.skip(len(prefix)) 585 | args = arg_func(ctx, prefix) 586 | ret: AssTag = cls(*args) 587 | ret.meta = Meta( 588 | i, ctx.io.global_pos, ctx.io.global_text[i : ctx.io.global_pos] 589 | ) 590 | 591 | if ( 592 | isinstance(ret, AssTagDraw) 593 | and ret.scale is not None 594 | and ret.scale > 0 595 | ): 596 | ctx.drawing_tag = ret 597 | 598 | return ret 599 | 600 | raise UnknownTag(ctx.io.global_pos) 601 | 602 | 603 | def _merge_comments(tags: list[AssTag]) -> list[AssTag]: 604 | if not tags: 605 | return [] 606 | 607 | ret = [tags.pop(0)] 608 | while tags: 609 | cur = tags.pop(0) 610 | 611 | if isinstance(ret[-1], AssTagComment) and isinstance( 612 | cur, AssTagComment 613 | ): 614 | prev = ret.pop() 615 | assert cur.meta 616 | assert prev.meta 617 | assert isinstance(prev, AssTagComment) 618 | block = AssTagComment(prev.text + cur.text) 619 | block.meta = Meta(prev.meta.start, cur.meta.end, block.text) 620 | ret.append(block) 621 | else: 622 | ret.append(cur) 623 | 624 | return ret 625 | 626 | 627 | def _parse_ass_tags(ctx: _ParseContext) -> Iterable[AssTag]: 628 | while not ctx.io.eof: 629 | if ctx.io.peek(1) == "\\": 630 | if ctx.io.peek(2) in {r"\N", r"\n", r"\h", r"\\"}: 631 | block = AssTagComment(ctx.io.read(2)) 632 | block.meta = Meta( 633 | ctx.io.global_pos, ctx.io.global_pos + 2, block.text 634 | ) 635 | yield block 636 | else: 637 | yield _parse_ass_tag(ctx) 638 | else: 639 | i = ctx.io.global_pos 640 | while not ctx.io.eof and ctx.io.peek(1) != "\\": 641 | ctx.io.skip(1) 642 | j = ctx.io.global_pos 643 | block = AssTagComment(ctx.io.global_text[i:j]) 644 | block.meta = Meta(i, j, block.text) 645 | yield block 646 | 647 | 648 | def _parse_ass(ctx: _ParseContext) -> Iterable[AssItem]: 649 | while not ctx.io.eof: 650 | i = ctx.io.pos 651 | if ctx.io.peek(1) == "{": 652 | 653 | ctx.io.skip(1) 654 | while True: 655 | if ctx.io.eof: 656 | raise UnterminatedCurlyBrace(ctx.io.global_pos) 657 | if ctx.io.peek(1) == "{": 658 | raise UnexpectedCurlyBrace(ctx.io.global_pos) 659 | if ctx.io.peek(1) == "}": 660 | ctx.io.skip(1) 661 | break 662 | ctx.io.skip(1) 663 | j = ctx.io.pos 664 | 665 | tag_list_opening = AssTagListOpening() 666 | tag_list_opening.meta = Meta(i, i + 1, "{") 667 | yield tag_list_opening 668 | 669 | old_io = ctx.io 670 | ctx.io = ctx.io.divide(i + 1, j - 1) 671 | yield from _merge_comments(list(_parse_ass_tags(ctx))) 672 | ctx.io = old_io 673 | 674 | tag_list_ending = AssTagListEnding() 675 | tag_list_ending.meta = Meta(j - 1, j, "}") 676 | yield tag_list_ending 677 | 678 | else: 679 | while not ctx.io.eof: 680 | if ctx.io.peek(1) == "{": 681 | break 682 | if ctx.io.peek(1) == "}": 683 | raise UnexpectedCurlyBrace(ctx.io.global_pos) 684 | ctx.io.skip(1) 685 | j = ctx.io.pos 686 | text = ctx.io.text[i:j] 687 | if ctx.drawing_tag is not None: 688 | ctx.drawing_tag.path = parse_draw_commands(text) 689 | ctx.drawing_tag = None 690 | else: 691 | ass_text = AssText(text) 692 | ass_text.meta = Meta(i, j, ctx.io.text[i:j]) 693 | yield ass_text 694 | 695 | 696 | def parse_ass(text: str) -> list[AssItem]: 697 | ctx = _ParseContext(io=MyIO(text)) 698 | return list(_parse_ass(ctx)) 699 | 700 | 701 | @cache 702 | def ass_to_plaintext(text: str) -> str: 703 | """Strip ASS tags from an ASS line. 704 | 705 | :param text: input ASS line 706 | :return: plain text 707 | """ 708 | try: 709 | ass_line = parse_ass(text) 710 | except ParseError: 711 | ret = str(re.sub("{[^}]*}", "", text)) 712 | else: 713 | ret = "" 714 | for item in ass_line: 715 | if isinstance(item, AssText): 716 | ret += item.text 717 | return ret.replace("\\h", " ").replace("\\n", " ").replace("\\N", "\n") 718 | -------------------------------------------------------------------------------- /ass_tag_parser/ass_struct.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | 4 | from ass_tag_parser.common import Meta 5 | from ass_tag_parser.draw_struct import AssDrawCmd 6 | 7 | 8 | class AssItem: 9 | meta: Optional[Meta] = None 10 | 11 | 12 | class AssTag(AssItem): 13 | pass 14 | 15 | 16 | @dataclass 17 | class AssTagComment(AssTag): 18 | text: str 19 | 20 | 21 | @dataclass 22 | class AssTagBold(AssTag): 23 | enabled: Optional[bool] = None 24 | weight: Optional[int] = None 25 | 26 | 27 | @dataclass 28 | class AssTagItalic(AssTag): 29 | enabled: Optional[bool] = None 30 | 31 | 32 | @dataclass 33 | class AssTagUnderline(AssTag): 34 | enabled: Optional[bool] = None 35 | 36 | 37 | @dataclass 38 | class AssTagStrikeout(AssTag): 39 | enabled: Optional[bool] = None 40 | 41 | 42 | @dataclass 43 | class AssTagBorder(AssTag): 44 | size: Optional[float] = None 45 | 46 | 47 | @dataclass 48 | class AssTagXBorder(AssTag): 49 | size: Optional[float] = None 50 | 51 | 52 | @dataclass 53 | class AssTagYBorder(AssTag): 54 | size: Optional[float] = None 55 | 56 | 57 | @dataclass 58 | class AssTagShadow(AssTag): 59 | size: Optional[float] = None 60 | 61 | 62 | @dataclass 63 | class AssTagXShadow(AssTag): 64 | size: Optional[float] = None 65 | 66 | 67 | @dataclass 68 | class AssTagYShadow(AssTag): 69 | size: Optional[float] = None 70 | 71 | 72 | @dataclass 73 | class AssTagBlurEdges(AssTag): 74 | times: Optional[float] = None 75 | 76 | 77 | @dataclass 78 | class AssTagBlurEdgesGauss(AssTag): 79 | weight: Optional[float] = None 80 | 81 | 82 | @dataclass 83 | class AssTagFontName(AssTag): 84 | name: Optional[str] = None 85 | 86 | 87 | @dataclass 88 | class AssTagFontEncoding(AssTag): 89 | encoding: Optional[int] = None 90 | 91 | 92 | @dataclass 93 | class AssTagFontSize(AssTag): 94 | size: Optional[float] = None 95 | 96 | 97 | @dataclass 98 | class AssTagFontXScale(AssTag): 99 | scale: Optional[float] = None 100 | 101 | 102 | @dataclass 103 | class AssTagFontYScale(AssTag): 104 | scale: Optional[float] = None 105 | 106 | 107 | @dataclass 108 | class AssTagLetterSpacing(AssTag): 109 | spacing: Optional[float] = None 110 | 111 | 112 | @dataclass 113 | class AssTagMove(AssTag): 114 | x1: float 115 | y1: float 116 | x2: float 117 | y2: float 118 | time1: Optional[float] = None 119 | time2: Optional[float] = None 120 | 121 | 122 | @dataclass 123 | class AssTagPosition(AssTag): 124 | x: Optional[float] = None 125 | y: Optional[float] = None 126 | 127 | 128 | @dataclass 129 | class AssTagRotationOrigin(AssTag): 130 | x: Optional[float] = None 131 | y: Optional[float] = None 132 | 133 | 134 | @dataclass 135 | class AssTagXRotation(AssTag): 136 | angle: Optional[float] = None 137 | 138 | 139 | @dataclass 140 | class AssTagYRotation(AssTag): 141 | angle: Optional[float] = None 142 | 143 | 144 | @dataclass 145 | class AssTagZRotation(AssTag): 146 | angle: Optional[float] = None 147 | short: bool = False 148 | 149 | 150 | @dataclass 151 | class AssTagAlignment(AssTag): 152 | alignment: Optional[int] = None 153 | legacy: bool = False 154 | 155 | 156 | @dataclass 157 | class AssTagResetStyle(AssTag): 158 | style: Optional[str] = None 159 | 160 | 161 | @dataclass 162 | class AssTagKaraoke(AssTag): 163 | duration: float 164 | karaoke_type: int 165 | 166 | 167 | @dataclass 168 | class AssTagColor(AssTag): 169 | red: Optional[int] 170 | green: Optional[int] 171 | blue: Optional[int] 172 | target: int 173 | short: bool = False 174 | 175 | 176 | @dataclass 177 | class AssTagAlpha(AssTag): 178 | value: Optional[int] 179 | target: int 180 | 181 | 182 | @dataclass 183 | class AssTagWrapStyle(AssTag): 184 | style: int 185 | 186 | 187 | @dataclass 188 | class AssTagFade(AssTag): 189 | time1: float 190 | time2: float 191 | 192 | 193 | @dataclass 194 | class AssTagFadeComplex(AssTag): 195 | alpha1: int 196 | alpha2: int 197 | alpha3: int 198 | time1: float 199 | time2: float 200 | time3: float 201 | time4: float 202 | 203 | 204 | @dataclass 205 | class AssTagXShear(AssTag): 206 | value: Optional[float] = None 207 | 208 | 209 | @dataclass 210 | class AssTagYShear(AssTag): 211 | value: Optional[float] = None 212 | 213 | 214 | @dataclass 215 | class AssTagAnimation(AssTag): 216 | tags: list[AssTag] 217 | time1: Optional[float] = None 218 | time2: Optional[float] = None 219 | acceleration: Optional[float] = None 220 | 221 | 222 | @dataclass 223 | class AssTagBaselineOffset(AssTag): 224 | y: float 225 | 226 | 227 | @dataclass 228 | class AssTagDraw(AssTag): 229 | scale: int 230 | path: list[AssDrawCmd] = field(default_factory=list) 231 | 232 | 233 | @dataclass 234 | class AssTagClipRectangle(AssTag): 235 | x1: float 236 | y1: float 237 | x2: float 238 | y2: float 239 | inverse: bool 240 | 241 | 242 | @dataclass 243 | class AssTagClipVector(AssTag): 244 | scale: Optional[int] 245 | path: list[AssDrawCmd] 246 | inverse: bool 247 | 248 | 249 | @dataclass 250 | class AssTagListOpening(AssItem): 251 | pass 252 | 253 | 254 | @dataclass 255 | class AssTagListEnding(AssItem): 256 | pass 257 | 258 | 259 | @dataclass 260 | class AssText(AssItem): 261 | text: str 262 | -------------------------------------------------------------------------------- /ass_tag_parser/common.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Union 3 | 4 | 5 | @dataclass 6 | class Meta: 7 | start: int 8 | end: int 9 | text: str 10 | 11 | 12 | def smart_float(value: Union[int, float, None]) -> str: 13 | if value is None: 14 | return "" 15 | return f"{float(value)}".rstrip("0").rstrip(".") 16 | 17 | 18 | def smart_int(value: Union[int, None]) -> str: 19 | if value is None: 20 | return "" 21 | return str(value) 22 | 23 | 24 | def smart_str(value: Union[str, None]) -> str: 25 | if value is None: 26 | return "" 27 | return value 28 | 29 | 30 | def smart_bool(value: Union[bool, int, None]) -> str: 31 | if value is None: 32 | return "" 33 | if value is True: 34 | return "1" 35 | if value is False: 36 | return "0" 37 | return "1" if value >= 1 else "0" 38 | -------------------------------------------------------------------------------- /ass_tag_parser/draw_composer.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ass_tag_parser.common import smart_float 4 | from ass_tag_parser.draw_struct import ( 5 | AssDrawCmd, 6 | AssDrawCmdBezier, 7 | AssDrawCmdCloseSpline, 8 | AssDrawCmdExtendSpline, 9 | AssDrawCmdLine, 10 | AssDrawCmdMove, 11 | AssDrawCmdSpline, 12 | ) 13 | from ass_tag_parser.errors import BaseError 14 | 15 | 16 | class Composer: 17 | def visit(self, draw_commands: list[AssDrawCmd]) -> str: 18 | ret = [] 19 | for cmd in draw_commands: 20 | assert isinstance(cmd, AssDrawCmd) 21 | 22 | visitor = getattr(self, "visit_" + cmd.__class__.__name__, None) 23 | if not visitor: 24 | raise NotImplementedError(f"not implemented ({cmd})") 25 | 26 | try: 27 | result = visitor(cmd) 28 | except (IndexError, KeyError, ValueError, TypeError) as exc: 29 | raise BaseError(exc) from exc 30 | 31 | result = [ 32 | smart_float(item) if isinstance(item, (int, float)) else item 33 | for item in result 34 | ] 35 | 36 | ret.append(" ".join(result)) 37 | 38 | return " ".join(ret) 39 | 40 | def visit_AssDrawCmdMove(self, cmd: AssDrawCmdMove) -> tuple[Any, ...]: 41 | return ("m" if cmd.close else "n", cmd.pos.x, cmd.pos.y) 42 | 43 | def visit_AssDrawCmdLine(self, cmd: AssDrawCmdLine) -> tuple[Any, ...]: 44 | return ("l", *sum([(point.x, point.y) for point in cmd.points], ())) 45 | 46 | def visit_AssDrawCmdBezier(self, cmd: AssDrawCmdBezier) -> tuple[Any, ...]: 47 | assert len(cmd.points) == 3 48 | return ( 49 | "b", 50 | cmd.points[0].x, 51 | cmd.points[0].y, 52 | cmd.points[1].x, 53 | cmd.points[1].y, 54 | cmd.points[2].x, 55 | cmd.points[2].y, 56 | ) 57 | 58 | def visit_AssDrawCmdSpline(self, cmd: AssDrawCmdSpline) -> tuple[Any, ...]: 59 | assert len(cmd.points) >= 3 60 | return ("s", *sum([(point.x, point.y) for point in cmd.points], ())) 61 | 62 | def visit_AssDrawCmdExtendSpline( 63 | self, cmd: AssDrawCmdExtendSpline 64 | ) -> tuple[Any, ...]: 65 | return ("p", *sum([(point.x, point.y) for point in cmd.points], ())) 66 | 67 | def visit_AssDrawCmdCloseSpline( 68 | self, _cmd: AssDrawCmdCloseSpline 69 | ) -> tuple[Any, ...]: 70 | return ("c",) 71 | 72 | 73 | def compose_draw_commands(commands: list[AssDrawCmd]) -> str: 74 | return Composer().visit(commands) 75 | -------------------------------------------------------------------------------- /ass_tag_parser/draw_parser.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Iterable, Optional, Union, cast 3 | 4 | from ass_tag_parser.common import Meta 5 | from ass_tag_parser.draw_struct import ( 6 | AssDrawCmd, 7 | AssDrawCmdBezier, 8 | AssDrawCmdCloseSpline, 9 | AssDrawCmdExtendSpline, 10 | AssDrawCmdLine, 11 | AssDrawCmdMove, 12 | AssDrawCmdSpline, 13 | AssDrawPoint, 14 | ) 15 | from ass_tag_parser.errors import ParseError 16 | from ass_tag_parser.io import MyIO 17 | 18 | 19 | @dataclass 20 | class _ParseContext: 21 | io: MyIO 22 | 23 | 24 | def _read_number(io: MyIO) -> Union[int, float]: 25 | ret = "" 26 | while io.peek(1).isspace(): 27 | io.skip(1) 28 | 29 | while True: 30 | char = io.peek(1) 31 | if not char or char not in ".-0123456789": 32 | if not ret: 33 | raise ParseError(io.global_pos, "expected number") 34 | break 35 | if char == "-" and ret: 36 | raise ParseError(io.global_pos, "unexpected dash") 37 | if char == "." and "." in ret: 38 | raise ParseError(io.global_pos, "unexpected dot") 39 | ret += char 40 | io.skip(1) 41 | 42 | return float(ret) if "." in ret else int(ret) 43 | 44 | 45 | def _read_point(io: MyIO) -> AssDrawPoint: 46 | return AssDrawPoint(_read_number(io), _read_number(io)) 47 | 48 | 49 | def _read_points( 50 | io: MyIO, min_count: int, max_count: Optional[int] = None 51 | ) -> Iterable[AssDrawPoint]: 52 | num = 0 53 | while num < min_count: 54 | yield _read_point(io) 55 | num += 1 56 | 57 | if max_count is not None: 58 | while num < max_count: 59 | yield _read_point(io) 60 | num += 1 61 | else: 62 | while not io.eof: 63 | while io.peek(1).isspace(): 64 | io.skip(1) 65 | if io.peek(1) in ".-0123456789": 66 | yield _read_point(io) 67 | else: 68 | break 69 | 70 | 71 | def _parse_draw_commands(ctx: _ParseContext) -> Iterable[AssDrawCmd]: 72 | while not ctx.io.eof: 73 | start_pos = ctx.io.global_pos 74 | cmd = ctx.io.read(1) 75 | 76 | ret: AssDrawCmd 77 | if cmd == "m": 78 | ret = AssDrawCmdMove(_read_point(ctx.io), close=True) 79 | elif cmd == "n": 80 | ret = AssDrawCmdMove(_read_point(ctx.io), close=False) 81 | elif cmd == "l": 82 | ret = AssDrawCmdLine(list(_read_points(ctx.io, min_count=1))) 83 | elif cmd == "b": 84 | ret = AssDrawCmdBezier( 85 | cast( 86 | tuple[AssDrawPoint, AssDrawPoint, AssDrawPoint], 87 | tuple(_read_points(ctx.io, min_count=3, max_count=3)), 88 | ) 89 | ) 90 | elif cmd == "s": 91 | ret = AssDrawCmdSpline( 92 | list(_read_points(ctx.io, min_count=3, max_count=None)) 93 | ) 94 | elif cmd == "p": 95 | ret = AssDrawCmdExtendSpline( 96 | list(_read_points(ctx.io, min_count=1)) 97 | ) 98 | elif cmd == "c": 99 | ret = AssDrawCmdCloseSpline() 100 | elif cmd.isspace(): 101 | continue 102 | else: 103 | raise ParseError(start_pos, "unknown draw command " + cmd) 104 | ret.meta = Meta( 105 | start_pos, 106 | ctx.io.global_pos, 107 | ctx.io.global_text[start_pos : ctx.io.global_pos], 108 | ) 109 | yield ret 110 | 111 | 112 | def parse_draw_commands(text: str) -> list[AssDrawCmd]: 113 | ctx = _ParseContext(io=MyIO(text)) 114 | return list(_parse_draw_commands(ctx)) 115 | -------------------------------------------------------------------------------- /ass_tag_parser/draw_struct.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from ass_tag_parser.common import Meta 5 | 6 | 7 | @dataclass 8 | class AssDrawPoint: 9 | x: float 10 | y: float 11 | 12 | 13 | class AssDrawCmd: 14 | meta: Optional[Meta] = None 15 | 16 | 17 | @dataclass 18 | class AssDrawCmdMove(AssDrawCmd): 19 | pos: AssDrawPoint 20 | close: bool 21 | 22 | 23 | @dataclass 24 | class AssDrawCmdLine(AssDrawCmd): 25 | points: list[AssDrawPoint] 26 | 27 | 28 | @dataclass 29 | class AssDrawCmdBezier(AssDrawCmd): 30 | points: tuple[AssDrawPoint, AssDrawPoint, AssDrawPoint] 31 | 32 | 33 | @dataclass 34 | class AssDrawCmdSpline(AssDrawCmd): 35 | points: list[AssDrawPoint] 36 | 37 | 38 | @dataclass 39 | class AssDrawCmdExtendSpline(AssDrawCmd): 40 | points: list[AssDrawPoint] 41 | 42 | 43 | @dataclass 44 | class AssDrawCmdCloseSpline(AssDrawCmd): 45 | pass 46 | -------------------------------------------------------------------------------- /ass_tag_parser/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class BaseError(Exception): 5 | pass 6 | 7 | 8 | class ParseError(BaseError): 9 | def __init__(self, pos: int, msg: Optional[str] = None) -> None: 10 | text = f"syntax error at pos {pos}" 11 | if msg: 12 | text += f": {msg}" 13 | super().__init__(text) 14 | 15 | 16 | class UnexpectedCurlyBrace(ParseError): 17 | def __init__(self, pos: int) -> None: 18 | super().__init__(pos, "unexpected curly brace") 19 | 20 | 21 | class UnknownTag(ParseError): 22 | def __init__(self, pos: int) -> None: 23 | super().__init__(pos, "unrecognized tag") 24 | 25 | 26 | class UnterminatedCurlyBrace(ParseError): 27 | def __init__(self, pos: int) -> None: 28 | super().__init__(pos, "unterminated curly brace") 29 | 30 | 31 | class BadAssTagArgument(ParseError): 32 | def __init__(self, pos: int, msg: Optional[str] = None) -> None: 33 | super().__init__(pos, "bad ass argument" if msg is None else msg) 34 | -------------------------------------------------------------------------------- /ass_tag_parser/io.py: -------------------------------------------------------------------------------- 1 | import io 2 | from typing import Optional 3 | 4 | 5 | class MyIO: 6 | def __init__( 7 | self, 8 | text: str = "", 9 | parent_pos: int = 0, 10 | global_text: Optional[str] = None, 11 | ) -> None: 12 | self._io = io.StringIO(text) 13 | self._text = text 14 | self._global_text = global_text or text 15 | self._size = len(text) 16 | self._parent_pos = parent_pos 17 | 18 | @property 19 | def eof(self) -> bool: 20 | return self.pos == self._size 21 | 22 | @property 23 | def pos(self) -> int: 24 | return self._io.tell() 25 | 26 | @property 27 | def global_pos(self) -> int: 28 | return self._parent_pos + self.pos 29 | 30 | @property 31 | def global_text(self) -> str: 32 | return self._global_text 33 | 34 | @property 35 | def text(self) -> str: 36 | pos = self._io.tell() 37 | self._io.seek(0) 38 | ret = self._io.read() 39 | self._io.seek(pos) 40 | return ret 41 | 42 | def read(self, num: int) -> str: 43 | return self._io.read(num) 44 | 45 | def write(self, text: str) -> None: 46 | self._io.write(text) 47 | 48 | def skip(self, num: int) -> None: 49 | self._io.seek(self.pos + num) 50 | 51 | def peek(self, num: int) -> str: 52 | old_pos = self._io.tell() 53 | ret = self._io.read(num) 54 | self._io.seek(old_pos) 55 | return ret 56 | 57 | def divide(self, start: int, end: int) -> "MyIO": 58 | return MyIO( 59 | self._text[start:end], self._parent_pos + start, self._global_text 60 | ) 61 | -------------------------------------------------------------------------------- /ass_tag_parser/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. 2 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bubblesub/ass_tag_parser/a758a3871b8904879a2588162335aa61863b370e/ass_tag_parser/tests/__init__.py -------------------------------------------------------------------------------- /ass_tag_parser/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(name="project_dir") 7 | def fixture_project_dir() -> Path: 8 | return Path(__file__).parent.parent 9 | 10 | 11 | @pytest.fixture(name="repo_dir") 12 | def fixture_repo_dir(project_dir: Path) -> Path: 13 | return project_dir.parent 14 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/test_ass_composing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ass_tag_parser import ( 4 | AssDrawCmdMove, 5 | AssDrawPoint, 6 | AssItem, 7 | AssTag, 8 | AssTagAlignment, 9 | AssTagAlpha, 10 | AssTagAnimation, 11 | AssTagBaselineOffset, 12 | AssTagBlurEdges, 13 | AssTagBlurEdgesGauss, 14 | AssTagBold, 15 | AssTagBorder, 16 | AssTagClipRectangle, 17 | AssTagClipVector, 18 | AssTagColor, 19 | AssTagComment, 20 | AssTagDraw, 21 | AssTagFade, 22 | AssTagFadeComplex, 23 | AssTagFontEncoding, 24 | AssTagFontName, 25 | AssTagFontSize, 26 | AssTagFontXScale, 27 | AssTagFontYScale, 28 | AssTagItalic, 29 | AssTagKaraoke, 30 | AssTagLetterSpacing, 31 | AssTagListEnding, 32 | AssTagListOpening, 33 | AssTagMove, 34 | AssTagPosition, 35 | AssTagResetStyle, 36 | AssTagRotationOrigin, 37 | AssTagShadow, 38 | AssTagStrikeout, 39 | AssTagUnderline, 40 | AssTagWrapStyle, 41 | AssTagXBorder, 42 | AssTagXRotation, 43 | AssTagXShadow, 44 | AssTagXShear, 45 | AssTagYBorder, 46 | AssTagYRotation, 47 | AssTagYShadow, 48 | AssTagYShear, 49 | AssTagZRotation, 50 | AssText, 51 | compose_ass, 52 | ) 53 | 54 | 55 | @pytest.mark.parametrize( 56 | "source_blocks,expected_line", 57 | [ 58 | ([], ""), 59 | ([AssText("test")], r"test"), 60 | ( 61 | [AssTagListOpening(), AssTagListEnding()], 62 | "", # autoinsert overrides {} 63 | ), 64 | ([AssTagComment("asdasd")], r"{asdasd}"), 65 | ( 66 | [ 67 | AssTagDraw( 68 | scale=2, 69 | path=[AssDrawCmdMove(AssDrawPoint(3, 4), close=True)], 70 | ) 71 | ], 72 | r"{\p2}m 3 4{\p0}", 73 | ), 74 | ( 75 | [ 76 | AssTagAlignment(5, legacy=False), 77 | AssTagAlignment(5, legacy=True), 78 | ], 79 | r"{\an5\a6}", 80 | ), 81 | ( 82 | [ 83 | AssTagListOpening(), 84 | AssTagAlignment(5, legacy=False), 85 | AssTagListEnding(), 86 | AssTagListOpening(), 87 | AssTagAlignment(5, legacy=False), 88 | AssTagListEnding(), 89 | ], 90 | r"{\an5\an5}", # autoinsert overrides {} 91 | ), 92 | ( 93 | [ 94 | AssText("abc def"), 95 | AssTagAlignment(5, legacy=False), 96 | AssText("ghi jkl"), 97 | AssTagAlignment(5, legacy=False), 98 | AssText("123 456"), 99 | ], 100 | r"abc def{\an5}ghi jkl{\an5}123 456", 101 | ), 102 | ( 103 | [ 104 | AssText("I am "), 105 | AssTagBold(enabled=True), 106 | AssText("not"), 107 | AssTagBold(enabled=False), 108 | AssText(" amused."), 109 | ], 110 | r"I am {\b1}not{\b0} amused.", 111 | ), 112 | ( 113 | [ 114 | AssTagBold(weight=100), 115 | AssText("How "), 116 | AssTagBold(weight=300), 117 | AssText("bold "), 118 | AssTagBold(weight=500), 119 | AssText("can "), 120 | AssTagBold(weight=700), 121 | AssText("you "), 122 | AssTagBold(weight=900), 123 | AssText("get?"), 124 | ], 125 | r"{\b100}How {\b300}bold {\b500}can {\b700}you {\b900}get?", 126 | ), 127 | ( 128 | [ 129 | AssText(r"-Hey\N"), 130 | AssTagResetStyle(style="Alternate"), 131 | AssText(r"-Huh?\N"), 132 | AssTagResetStyle(style=None), 133 | AssText("-Who are you?"), 134 | ], 135 | r"-Hey\N{\rAlternate}-Huh?\N{\r}-Who are you?", 136 | ), 137 | ( 138 | [ 139 | AssTagColor(0, 0, 255, 1), 140 | AssTagAnimation(tags=[AssTagColor(255, 0, 0, 1)]), 141 | AssText("Hello!"), 142 | ], 143 | r"{\1c&HFF0000&\t(\1c&H0000FF&)}Hello!", 144 | ), 145 | ( 146 | [ 147 | AssTagAlignment(5, legacy=False), 148 | AssTagAnimation( 149 | [AssTagZRotation(angle=3600)], 150 | time1=0, 151 | time2=5000, 152 | acceleration=None, 153 | ), 154 | AssText("Wheee"), 155 | ], 156 | r"{\an5\t(0,5000,\frz3600)}Wheee", 157 | ), 158 | ( 159 | [ 160 | AssTagAlignment(5, legacy=False), 161 | AssTagAnimation( 162 | [AssTagZRotation(angle=3600)], 163 | time1=0, 164 | time2=5000, 165 | acceleration=0.5, 166 | ), 167 | AssText("Wheee"), 168 | ], 169 | r"{\an5\t(0,5000,0.5,\frz3600)}Wheee", 170 | ), 171 | ( 172 | [ 173 | AssTagAlignment(5, legacy=False), 174 | AssTagFontXScale(0), 175 | AssTagFontYScale(0), 176 | AssTagAnimation( 177 | [AssTagFontXScale(100), AssTagFontYScale(100)], 178 | time1=0, 179 | time2=500, 180 | acceleration=None, 181 | ), 182 | AssText("Boo!"), 183 | ], 184 | r"{\an5\fscx0\fscy0\t(0,500,\fscx100\fscy100)}Boo!", 185 | ), 186 | ( 187 | [AssTagComment("comment"), AssTagBold(enabled=True)], 188 | r"{comment\b1}", 189 | ), 190 | ( 191 | [AssTagBold(enabled=True), AssTagComment(text="comment")], 192 | r"{\b1comment}", 193 | ), 194 | ( 195 | [AssTagAlpha(0xFF, 2), AssTagComment("comment")], 196 | r"{\2a&HFF&comment}", 197 | ), 198 | ([AssTagBlurEdges(times=2), AssTagComment(".2")], r"{\be2.2}"), 199 | ([AssTagFontSize(size=5), AssTagComment(text=".4")], r"{\fs5.4}"), 200 | ([AssTagKaraoke(duration=505, karaoke_type=1)], r"{\k50.5}"), 201 | ([AssTagKaraoke(duration=505, karaoke_type=2)], r"{\K50.5}"), 202 | ([AssTagKaraoke(duration=505, karaoke_type=3)], r"{\kf50.5}"), 203 | ([AssTagKaraoke(duration=505, karaoke_type=4)], r"{\ko50.5}"), 204 | ( 205 | [AssTagKaraoke(duration=500, karaoke_type=1), AssTagComment(".5")], 206 | r"{\k50.5}", 207 | ), 208 | ( 209 | [AssTagKaraoke(duration=500, karaoke_type=2), AssTagComment(".5")], 210 | r"{\K50.5}", 211 | ), 212 | ( 213 | [AssTagKaraoke(duration=500, karaoke_type=3), AssTagComment(".5")], 214 | r"{\kf50.5}", 215 | ), 216 | ( 217 | [AssTagKaraoke(duration=500, karaoke_type=4), AssTagComment(".5")], 218 | r"{\ko50.5}", 219 | ), 220 | ], 221 | ) 222 | def test_composing_valid_ass_line( 223 | source_blocks: list[AssItem], expected_line: str 224 | ) -> None: 225 | assert expected_line == compose_ass(source_blocks) 226 | 227 | 228 | @pytest.mark.parametrize( 229 | "source_tag,expected_line", 230 | [ 231 | (AssTagItalic(enabled=True), r"{\i1}"), 232 | (AssTagItalic(enabled=False), r"{\i0}"), 233 | (AssTagItalic(enabled=None), r"{\i}"), 234 | (AssTagBold(weight=300), r"{\b300}"), 235 | (AssTagBold(enabled=True), r"{\b1}"), 236 | (AssTagBold(enabled=False), r"{\b0}"), 237 | (AssTagBold(enabled=None), r"{\b}"), 238 | (AssTagUnderline(enabled=True), r"{\u1}"), 239 | (AssTagUnderline(enabled=False), r"{\u0}"), 240 | (AssTagUnderline(enabled=None), r"{\u}"), 241 | (AssTagStrikeout(enabled=True), r"{\s1}"), 242 | (AssTagStrikeout(enabled=False), r"{\s0}"), 243 | (AssTagStrikeout(enabled=None), r"{\s}"), 244 | (AssTagBorder(size=0), r"{\bord0}"), 245 | (AssTagXBorder(size=0), r"{\xbord0}"), 246 | (AssTagYBorder(size=0), r"{\ybord0}"), 247 | (AssTagBorder(size=1.0), r"{\bord1}"), 248 | (AssTagXBorder(size=1.0), r"{\xbord1}"), 249 | (AssTagYBorder(size=1.0), r"{\ybord1}"), 250 | (AssTagBorder(size=4.4), r"{\bord4.4}"), 251 | (AssTagXBorder(size=4.4), r"{\xbord4.4}"), 252 | (AssTagYBorder(size=4.4), r"{\ybord4.4}"), 253 | (AssTagBorder(size=None), r"{\bord}"), 254 | (AssTagXBorder(size=None), r"{\xbord}"), 255 | (AssTagYBorder(size=None), r"{\ybord}"), 256 | (AssTagShadow(size=0), r"{\shad0}"), 257 | (AssTagXShadow(size=0), r"{\xshad0}"), 258 | (AssTagYShadow(size=0), r"{\yshad0}"), 259 | (AssTagShadow(size=1.0), r"{\shad1}"), 260 | (AssTagXShadow(size=1.0), r"{\xshad1}"), 261 | (AssTagYShadow(size=1.0), r"{\yshad1}"), 262 | (AssTagShadow(size=4.4), r"{\shad4.4}"), 263 | (AssTagXShadow(size=4.4), r"{\xshad4.4}"), 264 | (AssTagYShadow(size=4.4), r"{\yshad4.4}"), 265 | (AssTagShadow(size=None), r"{\shad}"), 266 | (AssTagXShadow(size=None), r"{\xshad}"), 267 | (AssTagYShadow(size=None), r"{\yshad}"), 268 | (AssTagBlurEdges(times=2), r"{\be2}"), 269 | (AssTagBlurEdges(times=None), r"{\be}"), 270 | (AssTagBlurEdgesGauss(weight=1.0), r"{\blur1}"), 271 | (AssTagBlurEdgesGauss(weight=4.4), r"{\blur4.4}"), 272 | (AssTagBlurEdgesGauss(weight=None), r"{\blur}"), 273 | (AssTagFontName(name=None), r"{\fn}"), 274 | (AssTagFontName(name="Arial"), r"{\fnArial}"), 275 | (AssTagFontName(name="Comic Sans"), r"{\fnComic Sans}"), 276 | (AssTagFontEncoding(encoding=5), r"{\fe5}"), 277 | (AssTagFontEncoding(encoding=None), r"{\fe}"), 278 | (AssTagFontSize(size=15), r"{\fs15}"), 279 | (AssTagFontSize(size=None), r"{\fs}"), 280 | (AssTagFontXScale(scale=1.0), r"{\fscx1}"), 281 | (AssTagFontYScale(scale=1.0), r"{\fscy1}"), 282 | (AssTagFontXScale(scale=5.5), r"{\fscx5.5}"), 283 | (AssTagFontYScale(scale=5.5), r"{\fscy5.5}"), 284 | (AssTagFontXScale(scale=None), r"{\fscx}"), 285 | (AssTagFontYScale(scale=None), r"{\fscy}"), 286 | (AssTagLetterSpacing(spacing=1), r"{\fsp1}"), 287 | (AssTagLetterSpacing(spacing=5.5), r"{\fsp5.5}"), 288 | (AssTagLetterSpacing(spacing=-5.5), r"{\fsp-5.5}"), 289 | (AssTagLetterSpacing(spacing=None), r"{\fsp}"), 290 | (AssTagXRotation(angle=1.0), r"{\frx1}"), 291 | (AssTagXRotation(angle=5.5), r"{\frx5.5}"), 292 | (AssTagXRotation(angle=-5.5), r"{\frx-5.5}"), 293 | (AssTagXRotation(angle=None), r"{\frx}"), 294 | (AssTagYRotation(angle=1.0), r"{\fry1}"), 295 | (AssTagYRotation(angle=5.5), r"{\fry5.5}"), 296 | (AssTagYRotation(angle=-5.5), r"{\fry-5.5}"), 297 | (AssTagYRotation(angle=None), r"{\fry}"), 298 | (AssTagZRotation(angle=1.0), r"{\frz1}"), 299 | (AssTagZRotation(angle=1.0, short=True), r"{\fr1}"), 300 | (AssTagZRotation(angle=5.5), r"{\frz5.5}"), 301 | (AssTagZRotation(angle=-5.5), r"{\frz-5.5}"), 302 | (AssTagZRotation(angle=None), r"{\frz}"), 303 | (AssTagRotationOrigin(x=1, y=2), r"{\org(1,2)}"), 304 | (AssTagRotationOrigin(x=-1, y=-2), r"{\org(-1,-2)}"), 305 | (AssTagRotationOrigin(x=1.0, y=2.0), r"{\org(1,2)}"), 306 | (AssTagRotationOrigin(x=1.1, y=2.2), r"{\org(1.1,2.2)}"), 307 | (AssTagXShear(value=1.0), r"{\fax1}"), 308 | (AssTagYShear(value=1.0), r"{\fay1}"), 309 | (AssTagXShear(value=-1.5), r"{\fax-1.5}"), 310 | (AssTagYShear(value=-1.5), r"{\fay-1.5}"), 311 | (AssTagXShear(value=None), r"{\fax}"), 312 | (AssTagYShear(value=None), r"{\fay}"), 313 | (AssTagColor(0x56, 0x34, 0x12, 1, short=True), r"{\c&H123456&}"), 314 | (AssTagColor(0x56, 0x34, 0x12, 1), r"{\1c&H123456&}"), 315 | (AssTagColor(0x56, 0x34, 0x12, 2), r"{\2c&H123456&}"), 316 | (AssTagColor(0x56, 0x34, 0x12, 3), r"{\3c&H123456&}"), 317 | (AssTagColor(0x56, 0x34, 0x12, 4), r"{\4c&H123456&}"), 318 | (AssTagColor(None, None, None, 1, short=True), r"{\c}"), 319 | (AssTagColor(None, None, None, 1), r"{\1c}"), 320 | (AssTagColor(None, None, None, 2), r"{\2c}"), 321 | (AssTagColor(None, None, None, 3), r"{\3c}"), 322 | (AssTagColor(None, None, None, 4), r"{\4c}"), 323 | (AssTagAlpha(0x12, 0), r"{\alpha&H12&}"), 324 | (AssTagAlpha(0x12, 1), r"{\1a&H12&}"), 325 | (AssTagAlpha(0x12, 2), r"{\2a&H12&}"), 326 | (AssTagAlpha(0x12, 3), r"{\3a&H12&}"), 327 | (AssTagAlpha(0x12, 4), r"{\4a&H12&}"), 328 | (AssTagAlpha(None, 0), r"{\alpha}"), 329 | (AssTagAlpha(None, 1), r"{\1a}"), 330 | (AssTagAlpha(None, 2), r"{\2a}"), 331 | (AssTagAlpha(None, 3), r"{\3a}"), 332 | (AssTagAlpha(None, 4), r"{\4a}"), 333 | (AssTagKaraoke(duration=500, karaoke_type=1), r"{\k50}"), 334 | (AssTagKaraoke(duration=500, karaoke_type=2), r"{\K50}"), 335 | (AssTagKaraoke(duration=500, karaoke_type=3), r"{\kf50}"), 336 | (AssTagKaraoke(duration=500, karaoke_type=4), r"{\ko50}"), 337 | (AssTagAlignment(5, legacy=False), r"{\an5}"), 338 | (AssTagAlignment(None, legacy=False), r"{\an}"), 339 | (AssTagAlignment(None, legacy=True), r"{\a}"), 340 | (AssTagAlignment(1, legacy=True), r"{\a1}"), 341 | (AssTagAlignment(2, legacy=True), r"{\a2}"), 342 | (AssTagAlignment(3, legacy=True), r"{\a3}"), 343 | (AssTagAlignment(4, legacy=True), r"{\a5}"), 344 | (AssTagAlignment(5, legacy=True), r"{\a6}"), 345 | (AssTagAlignment(6, legacy=True), r"{\a7}"), 346 | (AssTagAlignment(7, legacy=True), r"{\a9}"), 347 | (AssTagAlignment(8, legacy=True), r"{\a10}"), 348 | (AssTagAlignment(9, legacy=True), r"{\a11}"), 349 | (AssTagWrapStyle(style=0), r"{\q0}"), 350 | (AssTagWrapStyle(style=1), r"{\q1}"), 351 | (AssTagWrapStyle(style=2), r"{\q2}"), 352 | (AssTagWrapStyle(style=3), r"{\q3}"), 353 | (AssTagResetStyle(style=None), r"{\r}"), 354 | (AssTagResetStyle(style="Some style"), r"{\rSome style}"), 355 | (AssTagDraw(scale=1, path=[]), r"{\p1}{\p0}"), 356 | (AssTagBaselineOffset(y=1.0), r"{\pbo1}"), 357 | (AssTagBaselineOffset(y=1.1), r"{\pbo1.1}"), 358 | (AssTagBaselineOffset(y=-50), r"{\pbo-50}"), 359 | (AssTagPosition(x=1, y=2), r"{\pos(1,2)}"), 360 | (AssTagPosition(x=1.0, y=2.0), r"{\pos(1,2)}"), 361 | (AssTagPosition(x=1.1, y=2.2), r"{\pos(1.1,2.2)}"), 362 | ( 363 | AssTagMove(x1=1, y1=2, x2=3, y2=4, time1=None, time2=None), 364 | r"{\move(1,2,3,4)}", 365 | ), 366 | ( 367 | AssTagMove(x1=1.0, y1=2.0, x2=3.0, y2=4.0, time1=5.0, time2=6.0), 368 | r"{\move(1,2,3,4,5,6)}", 369 | ), 370 | ( 371 | AssTagMove(x1=1.1, y1=2.2, x2=3.3, y2=4.4, time1=5.5, time2=6.6), 372 | r"{\move(1.1,2.2,3.3,4.4,5.5,6.6)}", 373 | ), 374 | ( 375 | AssTagMove(x1=1, y1=2, x2=3, y2=4, time1=100, time2=300), 376 | r"{\move(1,2,3,4,100,300)}", 377 | ), 378 | (AssTagFade(time1=100, time2=200), r"{\fad(100,200)}"), 379 | (AssTagFade(time1=1.0, time2=2.0), r"{\fad(1,2)}"), 380 | (AssTagFade(time1=1.1, time2=2.2), r"{\fad(1.1,2.2)}"), 381 | ( 382 | AssTagFadeComplex( 383 | alpha1=1, 384 | alpha2=2, 385 | alpha3=3, 386 | time1=4.0, 387 | time2=5.0, 388 | time3=6.0, 389 | time4=7.0, 390 | ), 391 | r"{\fade(1,2,3,4,5,6,7)}", 392 | ), 393 | ( 394 | AssTagFadeComplex( 395 | alpha1=1, 396 | alpha2=2, 397 | alpha3=3, 398 | time1=4.4, 399 | time2=5.5, 400 | time3=6.6, 401 | time4=7.7, 402 | ), 403 | r"{\fade(1,2,3,4.4,5.5,6.6,7.7)}", 404 | ), 405 | ( 406 | AssTagFadeComplex( 407 | alpha1=1, 408 | alpha2=2, 409 | alpha3=3, 410 | time1=4, 411 | time2=5, 412 | time3=6, 413 | time4=7, 414 | ), 415 | r"{\fade(1,2,3,4,5,6,7)}", 416 | ), 417 | ( 418 | AssTagAnimation([], time1=1.0, time2=2.0, acceleration=3.0), 419 | r"{\t(1,2,3,)}", 420 | ), 421 | ( 422 | AssTagAnimation( 423 | [AssTagBlurEdges(5), AssTagFontSize(40)], 424 | time1=1.1, 425 | time2=2.2, 426 | acceleration=3.3, 427 | ), 428 | r"{\t(1.1,2.2,3.3,\be5\fs40)}", 429 | ), 430 | (AssTagAnimation([], acceleration=1.0), r"{\t(1,)}"), 431 | ( 432 | AssTagAnimation( 433 | [AssTagBlurEdges(5), AssTagFontSize(40)], acceleration=1.2 434 | ), 435 | r"{\t(1.2,\be5\fs40)}", 436 | ), 437 | ( 438 | AssTagAnimation( 439 | [AssTagBlurEdges(5), AssTagFontSize(40)], time1=50, time2=100 440 | ), 441 | r"{\t(50,100,\be5\fs40)}", 442 | ), 443 | ( 444 | AssTagAnimation([AssTagBlurEdges(5), AssTagFontSize(40)]), 445 | r"{\t(\be5\fs40)}", 446 | ), 447 | ( 448 | AssTagClipRectangle(x1=1.0, y1=2.0, x2=3.0, y2=4.0, inverse=False), 449 | r"{\clip(1,2,3,4)}", 450 | ), 451 | ( 452 | AssTagClipRectangle(x1=1.0, y1=2.0, x2=3.0, y2=4.0, inverse=True), 453 | r"{\iclip(1,2,3,4)}", 454 | ), 455 | ( 456 | AssTagClipRectangle(x1=1.1, y1=2.2, x2=3.3, y2=4.4, inverse=False), 457 | r"{\clip(1.1,2.2,3.3,4.4)}", 458 | ), 459 | ( 460 | AssTagClipRectangle(x1=1.1, y1=2.2, x2=3.3, y2=4.4, inverse=True), 461 | r"{\iclip(1.1,2.2,3.3,4.4)}", 462 | ), 463 | ( 464 | AssTagClipRectangle(x1=1, y1=2, x2=3, y2=4, inverse=False), 465 | r"{\clip(1,2,3,4)}", 466 | ), 467 | ( 468 | AssTagClipRectangle(x1=1, y1=2, x2=3, y2=4, inverse=True), 469 | r"{\iclip(1,2,3,4)}", 470 | ), 471 | ( 472 | AssTagClipVector( 473 | scale=1, 474 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 475 | inverse=False, 476 | ), 477 | r"{\clip(1,m 50 0)}", 478 | ), 479 | ( 480 | AssTagClipVector( 481 | scale=1, 482 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 483 | inverse=True, 484 | ), 485 | r"{\iclip(1,m 50 0)}", 486 | ), 487 | ( 488 | AssTagClipVector( 489 | scale=None, 490 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 491 | inverse=False, 492 | ), 493 | r"{\clip(m 50 0)}", 494 | ), 495 | ( 496 | AssTagClipVector( 497 | scale=None, 498 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 499 | inverse=True, 500 | ), 501 | r"{\iclip(m 50 0)}", 502 | ), 503 | ], 504 | ) 505 | def test_composing_valid_single_tag( 506 | source_tag: AssTag, expected_line: str 507 | ) -> None: 508 | assert expected_line == compose_ass([source_tag]) 509 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/test_ass_parsing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ass_tag_parser import ( 4 | AssDrawCmdMove, 5 | AssDrawPoint, 6 | AssItem, 7 | AssTag, 8 | AssTagAlignment, 9 | AssTagAlpha, 10 | AssTagAnimation, 11 | AssTagBaselineOffset, 12 | AssTagBlurEdges, 13 | AssTagBlurEdgesGauss, 14 | AssTagBold, 15 | AssTagBorder, 16 | AssTagClipRectangle, 17 | AssTagClipVector, 18 | AssTagColor, 19 | AssTagComment, 20 | AssTagDraw, 21 | AssTagFade, 22 | AssTagFadeComplex, 23 | AssTagFontEncoding, 24 | AssTagFontName, 25 | AssTagFontSize, 26 | AssTagFontXScale, 27 | AssTagFontYScale, 28 | AssTagItalic, 29 | AssTagKaraoke, 30 | AssTagLetterSpacing, 31 | AssTagListEnding, 32 | AssTagListOpening, 33 | AssTagMove, 34 | AssTagPosition, 35 | AssTagResetStyle, 36 | AssTagRotationOrigin, 37 | AssTagShadow, 38 | AssTagStrikeout, 39 | AssTagUnderline, 40 | AssTagWrapStyle, 41 | AssTagXBorder, 42 | AssTagXRotation, 43 | AssTagXShadow, 44 | AssTagXShear, 45 | AssTagYBorder, 46 | AssTagYRotation, 47 | AssTagYShadow, 48 | AssTagYShear, 49 | AssTagZRotation, 50 | AssText, 51 | ParseError, 52 | parse_ass, 53 | ) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "source_line,expected_items", 58 | [ 59 | (r"", []), 60 | (r"\b1", [AssText(r"\b1")]), 61 | (r"{}", [AssTagListOpening(), AssTagListEnding()]), 62 | (r"test", [AssText("test")]), 63 | (r"(test)", [AssText("(test)")]), 64 | ( 65 | r"{(test)}", 66 | [AssTagListOpening(), AssTagComment("(test)"), AssTagListEnding()], 67 | ), 68 | ( 69 | r"{\b1()}", 70 | [ 71 | AssTagListOpening(), 72 | AssTagBold(True), 73 | AssTagComment("()"), 74 | AssTagListEnding(), 75 | ], 76 | ), 77 | ( 78 | r"{\b1(}", 79 | [ 80 | AssTagListOpening(), 81 | AssTagBold(True), 82 | AssTagComment("("), 83 | AssTagListEnding(), 84 | ], 85 | ), 86 | ( 87 | r"{\b1)}", 88 | [ 89 | AssTagListOpening(), 90 | AssTagBold(True), 91 | AssTagComment(")"), 92 | AssTagListEnding(), 93 | ], 94 | ), 95 | ( 96 | r"{\t(test)}", 97 | [ 98 | AssTagListOpening(), 99 | AssTagAnimation([AssTagComment("test")]), 100 | AssTagListEnding(), 101 | ], 102 | ), 103 | ( 104 | r"{\t(test()test)}", 105 | [ 106 | AssTagListOpening(), 107 | AssTagAnimation([AssTagComment("test()test")]), 108 | AssTagListEnding(), 109 | ], 110 | ), 111 | ( 112 | r"{\t(\t(test))}", 113 | [ 114 | AssTagListOpening(), 115 | AssTagAnimation([AssTagAnimation([AssTagComment("test")])]), 116 | AssTagListEnding(), 117 | ], 118 | ), 119 | ( 120 | "{garbage}", 121 | [ 122 | AssTagListOpening(), 123 | AssTagComment("garbage"), 124 | AssTagListEnding(), 125 | ], 126 | ), 127 | ( 128 | r"{asd\Nasd}", 129 | [ 130 | AssTagListOpening(), 131 | AssTagComment(r"asd\Nasd"), 132 | AssTagListEnding(), 133 | ], 134 | ), 135 | ( 136 | r"{asd\Nasd\nasd\hasd\\asd}", 137 | [ 138 | AssTagListOpening(), 139 | AssTagComment(r"asd\Nasd\nasd\hasd\\asd"), 140 | AssTagListEnding(), 141 | ], 142 | ), 143 | ( 144 | r"{\p2}m 3 4{\p0}", 145 | [ 146 | AssTagListOpening(), 147 | AssTagDraw( 148 | scale=2, 149 | path=[AssDrawCmdMove(AssDrawPoint(3, 4), close=True)], 150 | ), 151 | AssTagListEnding(), 152 | AssTagListOpening(), 153 | AssTagDraw(scale=0), 154 | AssTagListEnding(), 155 | ], 156 | ), 157 | ( 158 | r"{\an5\an5}", 159 | [ 160 | AssTagListOpening(), 161 | AssTagAlignment(5, False), 162 | AssTagAlignment(5, False), 163 | AssTagListEnding(), 164 | ], 165 | ), 166 | ( 167 | r"{\an5}{\an5}", 168 | [ 169 | AssTagListOpening(), 170 | AssTagAlignment(5, False), 171 | AssTagListEnding(), 172 | AssTagListOpening(), 173 | AssTagAlignment(5, False), 174 | AssTagListEnding(), 175 | ], 176 | ), 177 | ( 178 | r"abc def{\an5}ghi jkl{\an5}123 456", 179 | [ 180 | AssText("abc def"), 181 | AssTagListOpening(), 182 | AssTagAlignment(5, False), 183 | AssTagListEnding(), 184 | AssText("ghi jkl"), 185 | AssTagListOpening(), 186 | AssTagAlignment(5, False), 187 | AssTagListEnding(), 188 | AssText("123 456"), 189 | ], 190 | ), 191 | ( 192 | r"I am {\b1}not{\b0} amused.", 193 | [ 194 | AssText("I am "), 195 | AssTagListOpening(), 196 | AssTagBold(True), 197 | AssTagListEnding(), 198 | AssText("not"), 199 | AssTagListOpening(), 200 | AssTagBold(False), 201 | AssTagListEnding(), 202 | AssText(" amused."), 203 | ], 204 | ), 205 | ( 206 | r"{\b100}How {\b300}bold {\b500}can {\b700}you {\b900}get?", 207 | [ 208 | AssTagListOpening(), 209 | AssTagBold(True, 100), 210 | AssTagListEnding(), 211 | AssText("How "), 212 | AssTagListOpening(), 213 | AssTagBold(True, 300), 214 | AssTagListEnding(), 215 | AssText("bold "), 216 | AssTagListOpening(), 217 | AssTagBold(True, 500), 218 | AssTagListEnding(), 219 | AssText("can "), 220 | AssTagListOpening(), 221 | AssTagBold(True, 700), 222 | AssTagListEnding(), 223 | AssText("you "), 224 | AssTagListOpening(), 225 | AssTagBold(True, 900), 226 | AssTagListEnding(), 227 | AssText("get?"), 228 | ], 229 | ), 230 | ( 231 | r"-Hey\N{\rAlternate}-Huh?\N{\r}-Who are you?", 232 | [ 233 | AssText(r"-Hey\N"), 234 | AssTagListOpening(), 235 | AssTagResetStyle("Alternate"), 236 | AssTagListEnding(), 237 | AssText(r"-Huh?\N"), 238 | AssTagListOpening(), 239 | AssTagResetStyle(None), 240 | AssTagListEnding(), 241 | AssText("-Who are you?"), 242 | ], 243 | ), 244 | ( 245 | r"{\1c&HFF0000&\t(\1c&H0000FF&)}Hello!", 246 | [ 247 | AssTagListOpening(), 248 | AssTagColor(0, 0, 255, 1), 249 | AssTagAnimation(tags=[AssTagColor(255, 0, 0, 1)]), 250 | AssTagListEnding(), 251 | AssText("Hello!"), 252 | ], 253 | ), 254 | ( 255 | r"{\an5\t(0,5000,\frz3600)}Wheee", 256 | [ 257 | AssTagListOpening(), 258 | AssTagAlignment(5), 259 | AssTagAnimation( 260 | tags=[AssTagZRotation(3600)], time1=0, time2=5000 261 | ), 262 | AssTagListEnding(), 263 | AssText("Wheee"), 264 | ], 265 | ), 266 | ( 267 | r"{\an5\t(0,5000,0.5,\frz3600)}Wheee", 268 | [ 269 | AssTagListOpening(), 270 | AssTagAlignment(5), 271 | AssTagAnimation( 272 | acceleration=0.5, 273 | time1=0, 274 | time2=5000, 275 | tags=[AssTagZRotation(3600)], 276 | ), 277 | AssTagListEnding(), 278 | AssText("Wheee"), 279 | ], 280 | ), 281 | ( 282 | r"{\an5\fscx0\fscy0\t(0,500,\fscx100\fscy100)}Boo!", 283 | [ 284 | AssTagListOpening(), 285 | AssTagAlignment(5), 286 | AssTagFontXScale(0), 287 | AssTagFontYScale(0), 288 | AssTagAnimation( 289 | tags=[AssTagFontXScale(100), AssTagFontYScale(100)], 290 | time1=0, 291 | time2=500, 292 | acceleration=None, 293 | ), 294 | AssTagListEnding(), 295 | AssText("Boo!"), 296 | ], 297 | ), 298 | ( 299 | r"{comment\b1}", 300 | [ 301 | AssTagListOpening(), 302 | AssTagComment("comment"), 303 | AssTagBold(True, None), 304 | AssTagListEnding(), 305 | ], 306 | ), 307 | ], 308 | ) 309 | def test_parsing_valid_ass_line( 310 | source_line: str, expected_items: list[AssItem] 311 | ) -> None: 312 | assert expected_items == parse_ass(source_line) 313 | 314 | 315 | @pytest.mark.parametrize( 316 | "source_line,expected_tag", 317 | [ 318 | (r"{\i1}", AssTagItalic(enabled=True)), 319 | (r"{\i0}", AssTagItalic(enabled=False)), 320 | (r"{\i}", AssTagItalic(enabled=None)), 321 | (r"{\b300}", AssTagBold(enabled=True, weight=300)), 322 | (r"{\b1}", AssTagBold(enabled=True, weight=None)), 323 | (r"{\b0}", AssTagBold(enabled=False, weight=None)), 324 | (r"{\b}", AssTagBold(enabled=None, weight=None)), 325 | (r"{\u1}", AssTagUnderline(enabled=True)), 326 | (r"{\u0}", AssTagUnderline(enabled=False)), 327 | (r"{\u}", AssTagUnderline(enabled=None)), 328 | (r"{\s1}", AssTagStrikeout(enabled=True)), 329 | (r"{\s0}", AssTagStrikeout(enabled=False)), 330 | (r"{\s}", AssTagStrikeout(enabled=None)), 331 | (r"{\bord0}", AssTagBorder(size=0)), 332 | (r"{\xbord0}", AssTagXBorder(size=0)), 333 | (r"{\ybord0}", AssTagYBorder(size=0)), 334 | (r"{\bord4.4}", AssTagBorder(size=4.4)), 335 | (r"{\xbord4.4}", AssTagXBorder(size=4.4)), 336 | (r"{\ybord4.4}", AssTagYBorder(size=4.4)), 337 | (r"{\bord}", AssTagBorder(size=None)), 338 | (r"{\xbord}", AssTagXBorder(size=None)), 339 | (r"{\ybord}", AssTagYBorder(size=None)), 340 | (r"{\shad0}", AssTagShadow(size=0)), 341 | (r"{\xshad0}", AssTagXShadow(size=0)), 342 | (r"{\yshad0}", AssTagYShadow(size=0)), 343 | (r"{\shad4.4}", AssTagShadow(size=4.4)), 344 | (r"{\xshad4.4}", AssTagXShadow(size=4.4)), 345 | (r"{\yshad4.4}", AssTagYShadow(size=4.4)), 346 | (r"{\shad}", AssTagShadow(size=None)), 347 | (r"{\xshad}", AssTagXShadow(size=None)), 348 | (r"{\yshad}", AssTagYShadow(size=None)), 349 | (r"{\be2}", AssTagBlurEdges(times=2.0)), 350 | (r"{\be2.2}", AssTagBlurEdges(times=2.2)), 351 | (r"{\be}", AssTagBlurEdges(times=None)), 352 | (r"{\blur4}", AssTagBlurEdgesGauss(weight=4)), 353 | (r"{\blur4.4}", AssTagBlurEdgesGauss(weight=4.4)), 354 | (r"{\blur}", AssTagBlurEdgesGauss(weight=None)), 355 | (r"{\fn}", AssTagFontName(name=None)), 356 | (r"{\fnArial}", AssTagFontName(name="Arial")), 357 | (r"{\fnComic Sans}", AssTagFontName(name="Comic Sans")), 358 | (r"{\fe5}", AssTagFontEncoding(encoding=5)), 359 | (r"{\fe}", AssTagFontEncoding(encoding=None)), 360 | (r"{\fs15}", AssTagFontSize(size=15.0)), 361 | (r"{\fs5.5}", AssTagFontSize(size=5.5)), 362 | (r"{\fs}", AssTagFontSize(size=None)), 363 | (r"{\fscx5.5}", AssTagFontXScale(scale=5.5)), 364 | (r"{\fscy5.5}", AssTagFontYScale(scale=5.5)), 365 | (r"{\fscx}", AssTagFontXScale(scale=None)), 366 | (r"{\fscy}", AssTagFontYScale(scale=None)), 367 | (r"{\fsp5.5}", AssTagLetterSpacing(spacing=5.5)), 368 | (r"{\fsp-5.5}", AssTagLetterSpacing(spacing=-5.5)), 369 | (r"{\fsp}", AssTagLetterSpacing(spacing=None)), 370 | (r"{\frx5.5}", AssTagXRotation(angle=5.5)), 371 | (r"{\frx-5.5}", AssTagXRotation(angle=-5.5)), 372 | (r"{\frx}", AssTagXRotation(angle=None)), 373 | (r"{\fry5.5}", AssTagYRotation(angle=5.5)), 374 | (r"{\fry-5.5}", AssTagYRotation(angle=-5.5)), 375 | (r"{\fry}", AssTagYRotation(angle=None)), 376 | (r"{\frz5.5}", AssTagZRotation(angle=5.5)), 377 | (r"{\frz-5.5}", AssTagZRotation(angle=-5.5)), 378 | (r"{\frz}", AssTagZRotation(angle=None)), 379 | (r"{\fr5.5}", AssTagZRotation(angle=5.5, short=True)), 380 | (r"{\fr-5.5}", AssTagZRotation(angle=-5.5, short=True)), 381 | (r"{\fr}", AssTagZRotation(angle=None, short=True)), 382 | (r"{\org(1,2)}", AssTagRotationOrigin(x=1, y=2)), 383 | (r"{\org(-1,-2)}", AssTagRotationOrigin(x=-1, y=-2)), 384 | (r"{\fax-1.5}", AssTagXShear(value=-1.5)), 385 | (r"{\fay-1.5}", AssTagYShear(value=-1.5)), 386 | (r"{\fax}", AssTagXShear(value=None)), 387 | (r"{\fay}", AssTagYShear(value=None)), 388 | (r"{\c&H123456&}", AssTagColor(0x56, 0x34, 0x12, 1, short=True)), 389 | (r"{\1c&H123456&}", AssTagColor(0x56, 0x34, 0x12, 1)), 390 | (r"{\2c&H123456&}", AssTagColor(0x56, 0x34, 0x12, 2)), 391 | (r"{\3c&H123456&}", AssTagColor(0x56, 0x34, 0x12, 3)), 392 | (r"{\4c&H123456&}", AssTagColor(0x56, 0x34, 0x12, 4)), 393 | (r"{\c}", AssTagColor(None, None, None, 1, short=True)), 394 | (r"{\1c}", AssTagColor(None, None, None, 1)), 395 | (r"{\2c}", AssTagColor(None, None, None, 2)), 396 | (r"{\3c}", AssTagColor(None, None, None, 3)), 397 | (r"{\4c}", AssTagColor(None, None, None, 4)), 398 | (r"{\alpha&H12&}", AssTagAlpha(0x12, 0)), 399 | (r"{\1a&H12&}", AssTagAlpha(0x12, 1)), 400 | (r"{\2a&H12&}", AssTagAlpha(0x12, 2)), 401 | (r"{\3a&H12&}", AssTagAlpha(0x12, 3)), 402 | (r"{\4a&H12&}", AssTagAlpha(0x12, 4)), 403 | (r"{\alpha}", AssTagAlpha(None, 0)), 404 | (r"{\1a}", AssTagAlpha(None, 1)), 405 | (r"{\2a}", AssTagAlpha(None, 2)), 406 | (r"{\3a}", AssTagAlpha(None, 3)), 407 | (r"{\4a}", AssTagAlpha(None, 4)), 408 | (r"{\k50}", AssTagKaraoke(duration=500, karaoke_type=1)), 409 | (r"{\K50}", AssTagKaraoke(duration=500, karaoke_type=2)), 410 | (r"{\kf50}", AssTagKaraoke(duration=500, karaoke_type=3)), 411 | (r"{\ko50}", AssTagKaraoke(duration=500, karaoke_type=4)), 412 | (r"{\k50.5}", AssTagKaraoke(duration=505, karaoke_type=1)), 413 | (r"{\K50.5}", AssTagKaraoke(duration=505, karaoke_type=2)), 414 | (r"{\kf50.5}", AssTagKaraoke(duration=505, karaoke_type=3)), 415 | (r"{\ko50.5}", AssTagKaraoke(duration=505, karaoke_type=4)), 416 | (r"{\an5}", AssTagAlignment(alignment=5, legacy=False)), 417 | (r"{\an}", AssTagAlignment(alignment=None, legacy=False)), 418 | (r"{\a1}", AssTagAlignment(alignment=1, legacy=True)), 419 | (r"{\a2}", AssTagAlignment(alignment=2, legacy=True)), 420 | (r"{\a3}", AssTagAlignment(alignment=3, legacy=True)), 421 | (r"{\a5}", AssTagAlignment(alignment=4, legacy=True)), 422 | (r"{\a6}", AssTagAlignment(alignment=5, legacy=True)), 423 | (r"{\a7}", AssTagAlignment(alignment=6, legacy=True)), 424 | (r"{\a9}", AssTagAlignment(alignment=7, legacy=True)), 425 | (r"{\a10}", AssTagAlignment(alignment=8, legacy=True)), 426 | (r"{\a11}", AssTagAlignment(alignment=9, legacy=True)), 427 | (r"{\a}", AssTagAlignment(alignment=None, legacy=True)), 428 | (r"{\q0}", AssTagWrapStyle(style=0)), 429 | (r"{\q1}", AssTagWrapStyle(style=1)), 430 | (r"{\q2}", AssTagWrapStyle(style=2)), 431 | (r"{\q3}", AssTagWrapStyle(style=3)), 432 | (r"{\r}", AssTagResetStyle(style=None)), 433 | (r"{\rSome style}", AssTagResetStyle(style="Some style")), 434 | (r"{\p1}", AssTagDraw(scale=1, path=[])), 435 | (r"{\pbo-50}", AssTagBaselineOffset(y=-50)), 436 | (r"{\pbo1.1}", AssTagBaselineOffset(y=1.1)), 437 | (r"{\pos(1,2)}", AssTagPosition(x=1, y=2)), 438 | (r"{\pos(1.1,2.2)}", AssTagPosition(x=1.1, y=2.2)), 439 | (r"{\pos(-1,-2)}", AssTagPosition(x=-1, y=-2)), 440 | ( 441 | r"{\move(1,2,3,4)}", 442 | AssTagMove(x1=1, y1=2, x2=3, y2=4, time1=None, time2=None), 443 | ), 444 | ( 445 | r"{\move(1.1,2.2,3.3,4.4)}", 446 | AssTagMove(x1=1.1, y1=2.2, x2=3.3, y2=4.4, time1=None, time2=None), 447 | ), 448 | ( 449 | r"{\move(1.1,2.2,3.3,4.4,5.5,6.6)}", 450 | AssTagMove(x1=1.1, y1=2.2, x2=3.3, y2=4.4, time1=5.5, time2=6.6), 451 | ), 452 | ( 453 | r"{\move(-1,-2,-3,-4)}", 454 | AssTagMove(x1=-1, y1=-2, x2=-3, y2=-4, time1=None, time2=None), 455 | ), 456 | ( 457 | r"{\move(1,2,3,4,100,300)}", 458 | AssTagMove(x1=1, y1=2, x2=3, y2=4, time1=100, time2=300), 459 | ), 460 | (r"{\fad(1.1,2.2)}", AssTagFade(time1=1.1, time2=2.2)), 461 | (r"{\fad(100,200)}", AssTagFade(time1=100, time2=200)), 462 | ( 463 | r"{\fade(1,2,3,4,5,6,7)}", 464 | AssTagFadeComplex( 465 | alpha1=1, 466 | alpha2=2, 467 | alpha3=3, 468 | time1=4, 469 | time2=5, 470 | time3=6, 471 | time4=7, 472 | ), 473 | ), 474 | ( 475 | r"{\fade(1,2,3,4.4,5.5,6.6,7.7)}", 476 | AssTagFadeComplex( 477 | alpha1=1, 478 | alpha2=2, 479 | alpha3=3, 480 | time1=4.4, 481 | time2=5.5, 482 | time3=6.6, 483 | time4=7.7, 484 | ), 485 | ), 486 | ( 487 | r"{\t(50,100,1.2,\be5\fs40)}", 488 | AssTagAnimation( 489 | tags=[AssTagBlurEdges(5.0), AssTagFontSize(40)], 490 | time1=50, 491 | time2=100, 492 | acceleration=1.2, 493 | ), 494 | ), 495 | ( 496 | r"{\t(1.2,\be5\fs40)}", 497 | AssTagAnimation( 498 | tags=[AssTagBlurEdges(5.0), AssTagFontSize(40)], 499 | time1=None, 500 | time2=None, 501 | acceleration=1.2, 502 | ), 503 | ), 504 | ( 505 | r"{\t(50,100,\be5\fs40)}", 506 | AssTagAnimation( 507 | tags=[AssTagBlurEdges(5.0), AssTagFontSize(40)], 508 | time1=50, 509 | time2=100, 510 | acceleration=None, 511 | ), 512 | ), 513 | ( 514 | r"{\t(\be5\fs40)}", 515 | AssTagAnimation( 516 | tags=[AssTagBlurEdges(5.0), AssTagFontSize(40)], 517 | time1=None, 518 | time2=None, 519 | acceleration=None, 520 | ), 521 | ), 522 | ( 523 | r"{\clip(1,2,3,4)}", 524 | AssTagClipRectangle(x1=1, y1=2, x2=3, y2=4, inverse=False), 525 | ), 526 | ( 527 | r"{\iclip(1,2,3,4)}", 528 | AssTagClipRectangle(x1=1, y1=2, x2=3, y2=4, inverse=True), 529 | ), 530 | ( 531 | r"{\clip(1,m 50 0)}", 532 | AssTagClipVector( 533 | scale=1, 534 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 535 | inverse=False, 536 | ), 537 | ), 538 | ( 539 | r"{\iclip(1,m 50 0)}", 540 | AssTagClipVector( 541 | scale=1, 542 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 543 | inverse=True, 544 | ), 545 | ), 546 | ( 547 | r"{\clip(m 50 0)}", 548 | AssTagClipVector( 549 | scale=None, 550 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 551 | inverse=False, 552 | ), 553 | ), 554 | ( 555 | r"{\iclip(m 50 0)}", 556 | AssTagClipVector( 557 | scale=None, 558 | path=[AssDrawCmdMove(AssDrawPoint(50, 0), close=True)], 559 | inverse=True, 560 | ), 561 | ), 562 | ], 563 | ) 564 | def test_parsing_valid_single_tag( 565 | source_line: str, expected_tag: AssTag 566 | ) -> None: 567 | expected_tree = [AssTagListOpening(), expected_tag, AssTagListEnding()] 568 | assert expected_tree == parse_ass(source_line) 569 | 570 | 571 | @pytest.mark.parametrize( 572 | "source_line,error_msg", 573 | [ 574 | (r"{", "syntax error at pos 1: unterminated curly brace"), 575 | (r"}", "syntax error at pos 0: unexpected curly brace"), 576 | (r"{\t({)}", "syntax error at pos 4: unexpected curly brace"), 577 | (r"{\t(})}", "syntax error at pos 4: unexpected curly brace"), 578 | (r"test{", "syntax error at pos 5: unterminated curly brace"), 579 | (r"test}", "syntax error at pos 4: unexpected curly brace"), 580 | (r"}test{", "syntax error at pos 0: unexpected curly brace"), 581 | (r"{{asd}", "syntax error at pos 1: unexpected curly brace"), 582 | (r"{asd}}", "syntax error at pos 5: unexpected curly brace"), 583 | (r"{{asd}}", "syntax error at pos 1: unexpected curly brace"), 584 | (r"{\b-1}", r"syntax error at pos 5: \b takes only positive integers"), 585 | ( 586 | r"{\bord-4}", 587 | r"syntax error at pos 8: \bord takes only positive decimals", 588 | ), 589 | ( 590 | r"{\xbord-4}", 591 | r"syntax error at pos 9: \xbord takes only positive decimals", 592 | ), 593 | ( 594 | r"{\ybord-4}", 595 | r"syntax error at pos 9: \ybord takes only positive decimals", 596 | ), 597 | ( 598 | r"{\shad-4}", 599 | r"syntax error at pos 8: \shad takes only positive decimals", 600 | ), 601 | ( 602 | r"{\xshad-4}", 603 | r"syntax error at pos 9: \xshad takes only positive decimals", 604 | ), 605 | ( 606 | r"{\yshad-4}", 607 | r"syntax error at pos 9: \yshad takes only positive decimals", 608 | ), 609 | ( 610 | r"{\be-2}", 611 | r"syntax error at pos 6: \be takes only positive decimals", 612 | ), 613 | ( 614 | r"{\blur-4}", 615 | r"syntax error at pos 8: \blur takes only positive decimals", 616 | ), 617 | ( 618 | r"{\fe-5}", 619 | r"syntax error at pos 6: \fe takes only positive integers", 620 | ), 621 | ( 622 | r"{\fs-5}", 623 | r"syntax error at pos 6: \fs takes only positive decimals", 624 | ), 625 | ( 626 | r"{\fscx-5.5}", 627 | r"syntax error at pos 10: \fscx takes only positive decimals", 628 | ), 629 | ( 630 | r"{\fscy-5.5}", 631 | r"syntax error at pos 10: \fscy takes only positive decimals", 632 | ), 633 | (r"{\org0,0}", "syntax error at pos 6: expected brace"), 634 | ( 635 | r"{\org(0)}", 636 | r"syntax error at pos 8: \org takes 2 arguments (got 1)", 637 | ), 638 | ( 639 | r"{\org(0,0,0)}", 640 | r"syntax error at pos 12: \org takes 2 arguments (got 3)", 641 | ), 642 | ( 643 | r"{\org(garbage,0)}", 644 | r"syntax error at pos 16: \org takes only decimal arguments", 645 | ), 646 | (r"{\pos0,0}", "syntax error at pos 6: expected brace"), 647 | ( 648 | r"{\pos(0)}", 649 | r"syntax error at pos 8: \pos takes 2 arguments (got 1)", 650 | ), 651 | ( 652 | r"{\pos(0,0,0)}", 653 | r"syntax error at pos 12: \pos takes 2 arguments (got 3)", 654 | ), 655 | ( 656 | r"{\pos(garbage,0)}", 657 | r"syntax error at pos 16: \pos takes only decimal arguments", 658 | ), 659 | (r"{\move0,0,0,0}", "syntax error at pos 7: expected brace"), 660 | ( 661 | r"{\move(0,0,0)}", 662 | r"syntax error at pos 13: \move takes 4 or 6 arguments (got 3)", 663 | ), 664 | ( 665 | r"{\move(garbage,0,0,0)}", 666 | r"syntax error at pos 21: \move requires decimal coordinates", 667 | ), 668 | ( 669 | r"{\move(0,garbage,0,0)}", 670 | r"syntax error at pos 21: \move requires decimal coordinates", 671 | ), 672 | ( 673 | r"{\move(0,0,garbage,0)}", 674 | r"syntax error at pos 21: \move requires decimal coordinates", 675 | ), 676 | ( 677 | r"{\move(0,0,0,garbage)}", 678 | r"syntax error at pos 21: \move requires decimal coordinates", 679 | ), 680 | ( 681 | r"{\move(garbage,0,0,0,0,0)}", 682 | r"syntax error at pos 25: \move requires decimal coordinates", 683 | ), 684 | ( 685 | r"{\move(0,garbage,0,0,0,0)}", 686 | r"syntax error at pos 25: \move requires decimal coordinates", 687 | ), 688 | ( 689 | r"{\move(0,0,garbage,0,0,0)}", 690 | r"syntax error at pos 25: \move requires decimal coordinates", 691 | ), 692 | ( 693 | r"{\move(0,0,0,garbage,0,0)}", 694 | r"syntax error at pos 25: \move requires decimal coordinates", 695 | ), 696 | ( 697 | r"{\move(0,0,0,0,garbage,0)}", 698 | r"syntax error at pos 25: \move requires decimal times", 699 | ), 700 | ( 701 | r"{\move(0,0,0,0,0,garbage)}", 702 | r"syntax error at pos 25: \move requires decimal times", 703 | ), 704 | ( 705 | r"{\move(0,0,0,0,0)}", 706 | r"syntax error at pos 17: \move takes 4 or 6 arguments (got 5)", 707 | ), 708 | ( 709 | r"{\move(0,0,0,0,-5,0)}", 710 | r"syntax error at pos 20: \move takes only positive times", 711 | ), 712 | ( 713 | r"{\move(0,0,0,0,0,-5)}", 714 | r"syntax error at pos 20: \move takes only positive times", 715 | ), 716 | ( 717 | r"{\fad(-1,2)}", 718 | r"syntax error at pos 11: \fad takes only positive times", 719 | ), 720 | ( 721 | r"{\fad(1,-2)}", 722 | r"syntax error at pos 11: \fad takes only positive times", 723 | ), 724 | ( 725 | r"{\fade(1.1,2,3,4,5,6,7)}", 726 | r"syntax error at pos 23: \fade requires integer alpha values", 727 | ), 728 | ( 729 | r"{\fade(1,2.1,3,4,5,6,7)}", 730 | r"syntax error at pos 23: \fade requires integer alpha values", 731 | ), 732 | ( 733 | r"{\fade(1,2,3.1,4,5,6,7)}", 734 | r"syntax error at pos 23: \fade requires integer alpha values", 735 | ), 736 | ( 737 | r"{\fade(1,2,3,garbage,5,6,7)}", 738 | r"syntax error at pos 27: \fade requires decimal times", 739 | ), 740 | ( 741 | r"{\fade(1,2,3,4,garbage,6,7)}", 742 | r"syntax error at pos 27: \fade requires decimal times", 743 | ), 744 | ( 745 | r"{\fade(1,2,3,4,5,garbage,7)}", 746 | r"syntax error at pos 27: \fade requires decimal times", 747 | ), 748 | ( 749 | r"{\fade(1,2,3,4,5,6,garbage)}", 750 | r"syntax error at pos 27: \fade requires decimal times", 751 | ), 752 | ( 753 | r"{\fade(-1,2,3,4,5,6,7)}", 754 | r"syntax error at pos 22: \fade takes only positive alpha values", 755 | ), 756 | ( 757 | r"{\fade(1,-2,3,4,5,6,7)}", 758 | r"syntax error at pos 22: \fade takes only positive alpha values", 759 | ), 760 | ( 761 | r"{\fade(1,2,-3,4,5,6,7)}", 762 | r"syntax error at pos 22: \fade takes only positive alpha values", 763 | ), 764 | ( 765 | r"{\fade(1,2,3,-4,5,6,7)}", 766 | r"syntax error at pos 22: \fade takes only positive times", 767 | ), 768 | ( 769 | r"{\fade(1,2,3,4,-5,6,7)}", 770 | r"syntax error at pos 22: \fade takes only positive times", 771 | ), 772 | ( 773 | r"{\fade(1,2,3,4,5,-6,7)}", 774 | r"syntax error at pos 22: \fade takes only positive times", 775 | ), 776 | ( 777 | r"{\fade(1,2,3,4,5,6,-7)}", 778 | r"syntax error at pos 22: \fade takes only positive times", 779 | ), 780 | ( 781 | r"{\t(garbage,asd)}", 782 | "syntax error at pos 16: \\t requires decimal acceleration value", 783 | ), 784 | ( 785 | r"{\t(garbage,0,asd)}", 786 | "syntax error at pos 18: \\t requires decimal times", 787 | ), 788 | ( 789 | r"{\t(0,garbage,asd)}", 790 | "syntax error at pos 18: \\t requires decimal times", 791 | ), 792 | ( 793 | r"{\t(garbage,0,0,asd)}", 794 | "syntax error at pos 20: \\t requires decimal times", 795 | ), 796 | ( 797 | r"{\t(0,garbage,0,asd)}", 798 | "syntax error at pos 20: \\t requires decimal times", 799 | ), 800 | ( 801 | r"{\t(0,0,garbage,asd)}", 802 | "syntax error at pos 20: \\t requires decimal acceleration value", 803 | ), 804 | ( 805 | r"{\t(-1,asd)}", 806 | "syntax error at pos 11: \\t takes only positive acceleration value", 807 | ), 808 | ( 809 | r"{\t(-1,0,asd)}", 810 | "syntax error at pos 13: \\t takes only positive times", 811 | ), 812 | ( 813 | r"{\t(0,-1,asd)}", 814 | "syntax error at pos 13: \\t takes only positive times", 815 | ), 816 | ( 817 | r"{\t(-1,0,0,asd)}", 818 | "syntax error at pos 15: \\t takes only positive times", 819 | ), 820 | ( 821 | r"{\t(0,-1,0,asd)}", 822 | "syntax error at pos 15: \\t takes only positive times", 823 | ), 824 | ( 825 | r"{\t(0,0,-1,asd)}", 826 | "syntax error at pos 15: \\t takes only positive acceleration value", 827 | ), 828 | (r"{\cgarbage)}", r"syntax error at pos 4: expected ampersand"), 829 | (r"{\c&123456&}", "syntax error at pos 5: expected uppercase H"), 830 | ( 831 | r"{\1c&H12345&}", 832 | "syntax error at pos 12: expected hexadecimal number", 833 | ), 834 | (r"{\1c&H1234567&}", "syntax error at pos 13: expected ampersand"), 835 | ( 836 | r"{\1c&H12345G&}", 837 | "syntax error at pos 12: expected hexadecimal number", 838 | ), 839 | (r"{\alpha&12&}", "syntax error at pos 9: expected uppercase H"), 840 | (r"{\1a&H1&}", "syntax error at pos 8: expected hexadecimal number"), 841 | (r"{\1a&H123&}", "syntax error at pos 9: expected ampersand"), 842 | (r"{\1a&H1G&}", "syntax error at pos 8: expected hexadecimal number"), 843 | (r"{\k-5}", r"syntax error at pos 5: \k takes only positive decimals"), 844 | (r"{\K-5}", r"syntax error at pos 5: \K takes only positive decimals"), 845 | ( 846 | r"{\kf-5}", 847 | r"syntax error at pos 6: \kf takes only positive decimals", 848 | ), 849 | ( 850 | r"{\ko-5}", 851 | r"syntax error at pos 6: \ko takes only positive decimals", 852 | ), 853 | (r"{\k}", r"syntax error at pos 3: \k requires an argument"), 854 | (r"{\K}", r"syntax error at pos 3: \K requires an argument"), 855 | (r"{\kf}", r"syntax error at pos 4: \kf requires an argument"), 856 | (r"{\ko}", r"syntax error at pos 4: \ko requires an argument"), 857 | (r"{\an10}", r"syntax error at pos 6: \an expects 1-9"), 858 | (r"{\a4}", r"syntax error at pos 4: \a expects 1-3, 5-7 or 9-11"), 859 | (r"{\a8}", r"syntax error at pos 4: \a expects 1-3, 5-7 or 9-11"), 860 | (r"{\a12}", r"syntax error at pos 5: \a expects 1-3, 5-7 or 9-11"), 861 | (r"{\q5}", r"syntax error at pos 4: \q expects 0, 1, 2 or 3"), 862 | (r"{\garbage}", "syntax error at pos 1: unrecognized tag"), 863 | (r"{\5c&H123456&}", "syntax error at pos 1: unrecognized tag"), 864 | (r"{\5a&H12&}", "syntax error at pos 1: unrecognized tag"), 865 | (r"{\1c&HFFFFFF&derp}", "syntax error at pos 13: extra data"), 866 | (r"{\1a&HFF&derp}", "syntax error at pos 9: extra data"), 867 | (r"{\be2a}", r"syntax error at pos 6: \be requires a decimal"), 868 | (r"{\kgarbage}", r"syntax error at pos 10: \k requires a decimal"), 869 | (r"{\Kgarbage}", r"syntax error at pos 10: \K requires a decimal"), 870 | (r"{\kfgarbage}", r"syntax error at pos 11: \kf requires a decimal"), 871 | (r"{\kogarbage}", r"syntax error at pos 11: \ko requires a decimal"), 872 | (r"{\i-2}", r"syntax error at pos 5: \i requires a boolean"), 873 | (r"{\i2}", r"syntax error at pos 4: \i requires a boolean"), 874 | (r"{\u2}", r"syntax error at pos 4: \u requires a boolean"), 875 | (r"{\s2}", r"syntax error at pos 4: \s requires a boolean"), 876 | ( 877 | r"{\c(123456)}", 878 | r"syntax error at pos 3: \c doesn't take complex arguments", 879 | ), 880 | ( 881 | r"{\fn(Comic Sans)}", 882 | r"syntax error at pos 4: \fn doesn't take complex arguments", 883 | ), 884 | ( 885 | r"{\a(12)}", 886 | r"syntax error at pos 3: \a doesn't take complex arguments", 887 | ), 888 | (r"{\b1comment}", r"syntax error at pos 11: \b requires an integer"), 889 | (r"{asd\asd}", r"syntax error at pos 8: \a requires an integer"), 890 | (r"{\pbogarbage}", r"syntax error at pos 12: \pbo requires a decimal"), 891 | ], 892 | ) 893 | def test_parsing_invalid_ass_line(source_line: str, error_msg: str) -> None: 894 | with pytest.raises(ParseError) as exc_info: 895 | parse_ass(source_line) 896 | assert error_msg == str(exc_info.value) 897 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/test_documentation.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | from typing import Any, no_type_check 4 | 5 | import black 6 | import pytest 7 | 8 | 9 | @no_type_check 10 | def format_black(source: Any) -> str: 11 | return black.format_str(str(source), mode=black.Mode(line_length=79)) 12 | 13 | 14 | @pytest.fixture(name="readme_content") 15 | def fixture_readme_content(repo_dir: Path) -> str: 16 | path_to_readme = repo_dir / "README.md" 17 | assert path_to_readme.exists() 18 | return path_to_readme.read_text() 19 | 20 | 21 | @pytest.fixture(name="readme_code_snippet") 22 | def fixture_readme_code_snippet(readme_content: str) -> str: 23 | match = re.search(r"```python3([^`]*)```", readme_content, flags=re.DOTALL) 24 | assert match 25 | return match.group(1).lstrip() 26 | 27 | 28 | @pytest.fixture(name="readme_code_result") 29 | def fixture_readme_code_result(readme_content: str) -> str: 30 | match = re.search( 31 | r"```python3 console([^`]*)```", readme_content, flags=re.DOTALL 32 | ) 33 | assert match 34 | return match.group(1).lstrip() 35 | 36 | 37 | def test_readme_code_up_to_date( 38 | capsys: Any, readme_code_snippet: str, readme_code_result: str 39 | ) -> None: 40 | """Test that the code example in the README.md matches the actual output 41 | from the library. 42 | """ 43 | exec(readme_code_snippet) # pylint: disable=exec-used 44 | actual_result = format_black(capsys.readouterr().out.strip()) 45 | assert actual_result == readme_code_result 46 | 47 | 48 | def test_readme_code_formatting(readme_code_snippet: str) -> None: 49 | """Test that the code example in the README.md is well-formatted.""" 50 | assert readme_code_snippet == format_black(readme_code_snippet) 51 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/test_draw_composing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ass_tag_parser import ( 4 | AssDrawCmd, 5 | AssDrawCmdBezier, 6 | AssDrawCmdCloseSpline, 7 | AssDrawCmdExtendSpline, 8 | AssDrawCmdLine, 9 | AssDrawCmdMove, 10 | AssDrawCmdSpline, 11 | AssDrawPoint, 12 | compose_draw_commands, 13 | ) 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "source,expected", 18 | [ 19 | ([AssDrawCmdMove(AssDrawPoint(0, 0), close=True)], "m 0 0"), 20 | ([AssDrawCmdMove(AssDrawPoint(1, 2), close=False)], "n 1 2"), 21 | ([AssDrawCmdMove(AssDrawPoint(-1, 2), close=True)], "m -1 2"), 22 | ([AssDrawCmdMove(AssDrawPoint(1.0, 2.0), close=True)], "m 1 2"), 23 | ([AssDrawCmdMove(AssDrawPoint(1.1, 2.2), close=True)], "m 1.1 2.2"), 24 | ([AssDrawCmdLine([AssDrawPoint(1, 2)])], "l 1 2"), 25 | ([AssDrawCmdLine([AssDrawPoint(1.0, 2.0)])], "l 1 2"), 26 | ([AssDrawCmdLine([AssDrawPoint(1.1, 2.2)])], "l 1.1 2.2"), 27 | ( 28 | [AssDrawCmdLine([AssDrawPoint(1, 2), AssDrawPoint(3, 4)])], 29 | "l 1 2 3 4", 30 | ), 31 | ( 32 | [AssDrawCmdLine([AssDrawPoint(1.0, 2.0), AssDrawPoint(3.0, 4.0)])], 33 | "l 1 2 3 4", 34 | ), 35 | ( 36 | [AssDrawCmdLine([AssDrawPoint(1.1, 2.2), AssDrawPoint(3.3, 4.4)])], 37 | "l 1.1 2.2 3.3 4.4", 38 | ), 39 | ( 40 | [ 41 | AssDrawCmdBezier( 42 | ( 43 | AssDrawPoint(1, 2), 44 | AssDrawPoint(3, 4), 45 | AssDrawPoint(5, 6), 46 | ) 47 | ) 48 | ], 49 | "b 1 2 3 4 5 6", 50 | ), 51 | ( 52 | [ 53 | AssDrawCmdSpline( 54 | [ 55 | AssDrawPoint(1, 2), 56 | AssDrawPoint(3, 4), 57 | AssDrawPoint(5, 6), 58 | ] 59 | ) 60 | ], 61 | "s 1 2 3 4 5 6", 62 | ), 63 | ( 64 | [ 65 | AssDrawCmdSpline( 66 | [ 67 | AssDrawPoint(1, 2), 68 | AssDrawPoint(3, 4), 69 | AssDrawPoint(5, 6), 70 | AssDrawPoint(7, 8), 71 | AssDrawPoint(9, 10), 72 | ] 73 | ) 74 | ], 75 | "s 1 2 3 4 5 6 7 8 9 10", 76 | ), 77 | ([AssDrawCmdExtendSpline([AssDrawPoint(1, 2)])], "p 1 2"), 78 | ( 79 | [AssDrawCmdExtendSpline([AssDrawPoint(1, 2), AssDrawPoint(3, 4)])], 80 | "p 1 2 3 4", 81 | ), 82 | ([AssDrawCmdCloseSpline()], "c"), 83 | ( 84 | [ 85 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 86 | AssDrawCmdLine( 87 | [ 88 | AssDrawPoint(100, 0), 89 | AssDrawPoint(100, 100), 90 | AssDrawPoint(0, 100), 91 | ] 92 | ), 93 | ], 94 | "m 0 0 l 100 0 100 100 0 100", 95 | ), 96 | ( 97 | [ 98 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 99 | AssDrawCmdSpline( 100 | [ 101 | AssDrawPoint(100, 0), 102 | AssDrawPoint(100, 100), 103 | AssDrawPoint(0, 100), 104 | ] 105 | ), 106 | AssDrawCmdCloseSpline(), 107 | ], 108 | "m 0 0 s 100 0 100 100 0 100 c", 109 | ), 110 | ( 111 | [ 112 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 113 | AssDrawCmdSpline( 114 | [ 115 | AssDrawPoint(100, 0), 116 | AssDrawPoint(100, 100), 117 | AssDrawPoint(0, 100), 118 | ] 119 | ), 120 | AssDrawCmdExtendSpline( 121 | [ 122 | AssDrawPoint(0, 0), 123 | AssDrawPoint(100, 0), 124 | AssDrawPoint(100, 100), 125 | ] 126 | ), 127 | ], 128 | "m 0 0 s 100 0 100 100 0 100 p 0 0 100 0 100 100", 129 | ), 130 | ], 131 | ) 132 | def test_parsing_valid_commands( 133 | source: list[AssDrawCmd], expected: str 134 | ) -> None: 135 | assert expected == compose_draw_commands(source) 136 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/test_draw_parsing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ass_tag_parser import ( 4 | AssDrawCmd, 5 | AssDrawCmdBezier, 6 | AssDrawCmdCloseSpline, 7 | AssDrawCmdExtendSpline, 8 | AssDrawCmdLine, 9 | AssDrawCmdMove, 10 | AssDrawCmdSpline, 11 | AssDrawPoint, 12 | ParseError, 13 | parse_draw_commands, 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "source,expected", 19 | [ 20 | ("m 0 0", [AssDrawCmdMove(AssDrawPoint(0, 0), close=True)]), 21 | ("m0 0", [AssDrawCmdMove(AssDrawPoint(0, 0), close=True)]), 22 | ( 23 | "m0 0c", 24 | [ 25 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 26 | AssDrawCmdCloseSpline(), 27 | ], 28 | ), 29 | ( 30 | "m0 0m0 0", 31 | [ 32 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 33 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 34 | ], 35 | ), 36 | ("m 1.1 2.2", [AssDrawCmdMove(AssDrawPoint(1.1, 2.2), close=True)]), 37 | ("m -1 2", [AssDrawCmdMove(AssDrawPoint(-1, 2), close=True)]), 38 | ("n 1 2", [AssDrawCmdMove(AssDrawPoint(1, 2), close=False)]), 39 | ("l 1 2", [AssDrawCmdLine([AssDrawPoint(1, 2)])]), 40 | ( 41 | "l 1 2 3 4", 42 | [AssDrawCmdLine([AssDrawPoint(1, 2), AssDrawPoint(3, 4)])], 43 | ), 44 | ( 45 | "b 1 2 3 4 5 6", 46 | [ 47 | AssDrawCmdBezier( 48 | ( 49 | AssDrawPoint(1, 2), 50 | AssDrawPoint(3, 4), 51 | AssDrawPoint(5, 6), 52 | ) 53 | ) 54 | ], 55 | ), 56 | ( 57 | "s 1 2 3 4 5 6", 58 | [ 59 | AssDrawCmdSpline( 60 | [ 61 | AssDrawPoint(1, 2), 62 | AssDrawPoint(3, 4), 63 | AssDrawPoint(5, 6), 64 | ] 65 | ) 66 | ], 67 | ), 68 | ( 69 | "s 1 2 3 4 5 6 7 8 9 10", 70 | [ 71 | AssDrawCmdSpline( 72 | [ 73 | AssDrawPoint(1, 2), 74 | AssDrawPoint(3, 4), 75 | AssDrawPoint(5, 6), 76 | AssDrawPoint(7, 8), 77 | AssDrawPoint(9, 10), 78 | ] 79 | ) 80 | ], 81 | ), 82 | ("p 1 2", [AssDrawCmdExtendSpline([AssDrawPoint(1, 2)])]), 83 | ( 84 | "p 1 2 3 4", 85 | [AssDrawCmdExtendSpline([AssDrawPoint(1, 2), AssDrawPoint(3, 4)])], 86 | ), 87 | ("c", [AssDrawCmdCloseSpline()]), 88 | ( 89 | "m 0 0 l 100 0 100 100 0 100", 90 | [ 91 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 92 | AssDrawCmdLine( 93 | [ 94 | AssDrawPoint(100, 0), 95 | AssDrawPoint(100, 100), 96 | AssDrawPoint(0, 100), 97 | ] 98 | ), 99 | ], 100 | ), 101 | ( 102 | "m 0 0 s 100 0 100 100 0 100 c", 103 | [ 104 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 105 | AssDrawCmdSpline( 106 | [ 107 | AssDrawPoint(100, 0), 108 | AssDrawPoint(100, 100), 109 | AssDrawPoint(0, 100), 110 | ] 111 | ), 112 | AssDrawCmdCloseSpline(), 113 | ], 114 | ), 115 | ( 116 | "m 0 0 s 100 0 100 100 0 100 p 0 0 100 0 100 100", 117 | [ 118 | AssDrawCmdMove(AssDrawPoint(0, 0), close=True), 119 | AssDrawCmdSpline( 120 | [ 121 | AssDrawPoint(100, 0), 122 | AssDrawPoint(100, 100), 123 | AssDrawPoint(0, 100), 124 | ] 125 | ), 126 | AssDrawCmdExtendSpline( 127 | [ 128 | AssDrawPoint(0, 0), 129 | AssDrawPoint(100, 0), 130 | AssDrawPoint(100, 100), 131 | ] 132 | ), 133 | ], 134 | ), 135 | ], 136 | ) 137 | def test_parsing_valid_text(source: str, expected: list[AssDrawCmd]) -> None: 138 | assert expected == parse_draw_commands(source) 139 | 140 | 141 | @pytest.mark.parametrize( 142 | "source,error_msg", 143 | [ 144 | ("m 1", "syntax error at pos 3: expected number"), 145 | ("m 1 2 3", "syntax error at pos 6: unknown draw command 3"), 146 | ("l", "syntax error at pos 1: expected number"), 147 | ("l 1", "syntax error at pos 3: expected number"), 148 | ("l 1 2 3", "syntax error at pos 7: expected number"), 149 | ("l 1 2 3 4 5", "syntax error at pos 11: expected number"), 150 | ("b", "syntax error at pos 1: expected number"), 151 | ("b 1", "syntax error at pos 3: expected number"), 152 | ("b 1 2", "syntax error at pos 5: expected number"), 153 | ("b 1 2 3", "syntax error at pos 7: expected number"), 154 | ("b 1 2 3 4", "syntax error at pos 9: expected number"), 155 | ("b 1 2 3 4 5", "syntax error at pos 11: expected number"), 156 | ("b 1 2 3 4 5 6 7", "syntax error at pos 14: unknown draw command 7"), 157 | ( 158 | "b 1 2 3 4 5 6 7 8", 159 | "syntax error at pos 14: unknown draw command 7", 160 | ), 161 | ("s", "syntax error at pos 1: expected number"), 162 | ("s 1", "syntax error at pos 3: expected number"), 163 | ("s 1 2", "syntax error at pos 5: expected number"), 164 | ("s 1 2 3", "syntax error at pos 7: expected number"), 165 | ("s 1 2 3 4", "syntax error at pos 9: expected number"), 166 | ("s 1 2 3 4 5", "syntax error at pos 11: expected number"), 167 | ("p 1 2 3 4 5 6 7", "syntax error at pos 15: expected number"), 168 | ("p 1", "syntax error at pos 3: expected number"), 169 | ("p 1 2 3", "syntax error at pos 7: expected number"), 170 | ("p 1 2 3 4 5", "syntax error at pos 11: expected number"), 171 | ], 172 | ) 173 | def test_parsing_invalid_text(source: str, error_msg: str) -> None: 174 | with pytest.raises(ParseError) as exc_info: 175 | parse_draw_commands(source) 176 | assert error_msg == str(exc_info.value) 177 | -------------------------------------------------------------------------------- /ass_tag_parser/tests/test_module_exports.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import chain 3 | from pathlib import Path 4 | from typing import Iterable, Type 5 | 6 | import pytest 7 | 8 | from ass_tag_parser.ass_struct import AssItem 9 | from ass_tag_parser.draw_struct import AssDrawCmd 10 | 11 | 12 | def get_subclasses(cls: Type[object]) -> Iterable[Type[object]]: 13 | for subclass in cls.__subclasses__(): 14 | yield from get_subclasses(subclass) 15 | yield subclass 16 | 17 | 18 | @pytest.fixture(name="all_names") 19 | def fixture_all_names(project_dir: Path) -> list[str]: 20 | path_to_init = project_dir / "__init__.py" 21 | assert path_to_init.exists() 22 | 23 | match = re.search( 24 | r"__all__ = (\[.+\])", path_to_init.read_text(), flags=re.DOTALL 25 | ) 26 | assert match 27 | 28 | ret = eval(match.group(1)) # pylint: disable=eval-used 29 | assert isinstance(ret, list) 30 | return ret 31 | 32 | 33 | @pytest.mark.parametrize( 34 | "cls", chain(get_subclasses(AssItem), get_subclasses(AssDrawCmd)) 35 | ) 36 | def test_module_exports(all_names: list[str], cls: Type[object]) -> None: 37 | """Test that ass_tag_parser.__init__.__all__ includes all defined ASS tags 38 | and draw commands. 39 | """ 40 | assert cls.__name__ in all_names 41 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.2.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 22 | 23 | [[package]] 24 | name = "backports.entry-points-selectable" 25 | version = "1.1.1" 26 | description = "Compatibility shim providing selectable entry points for older implementations" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7" 30 | 31 | [package.extras] 32 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 33 | testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 34 | 35 | [[package]] 36 | name = "black" 37 | version = "21.12b0" 38 | description = "The uncompromising code formatter." 39 | category = "dev" 40 | optional = false 41 | python-versions = ">=3.6.2" 42 | 43 | [package.dependencies] 44 | click = ">=7.1.2" 45 | mypy-extensions = ">=0.4.3" 46 | pathspec = ">=0.9.0,<1" 47 | platformdirs = ">=2" 48 | tomli = ">=0.2.6,<2.0.0" 49 | typing-extensions = [ 50 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 51 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 52 | ] 53 | 54 | [package.extras] 55 | colorama = ["colorama (>=0.4.3)"] 56 | d = ["aiohttp (>=3.7.4)"] 57 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 58 | python2 = ["typed-ast (>=1.4.3)"] 59 | uvloop = ["uvloop (>=0.15.2)"] 60 | 61 | [[package]] 62 | name = "cfgv" 63 | version = "3.3.1" 64 | description = "Validate configuration and produce human readable error messages." 65 | category = "dev" 66 | optional = false 67 | python-versions = ">=3.6.1" 68 | 69 | [[package]] 70 | name = "click" 71 | version = "8.0.3" 72 | description = "Composable command line interface toolkit" 73 | category = "dev" 74 | optional = false 75 | python-versions = ">=3.6" 76 | 77 | [package.dependencies] 78 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 79 | 80 | [[package]] 81 | name = "colorama" 82 | version = "0.4.4" 83 | description = "Cross-platform colored terminal text." 84 | category = "dev" 85 | optional = false 86 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 87 | 88 | [[package]] 89 | name = "distlib" 90 | version = "0.3.4" 91 | description = "Distribution utilities" 92 | category = "dev" 93 | optional = false 94 | python-versions = "*" 95 | 96 | [[package]] 97 | name = "filelock" 98 | version = "3.4.0" 99 | description = "A platform independent file lock." 100 | category = "dev" 101 | optional = false 102 | python-versions = ">=3.6" 103 | 104 | [package.extras] 105 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 106 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 107 | 108 | [[package]] 109 | name = "identify" 110 | version = "2.4.0" 111 | description = "File identification library for Python" 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=3.6.1" 115 | 116 | [package.extras] 117 | license = ["ukkonen"] 118 | 119 | [[package]] 120 | name = "iniconfig" 121 | version = "1.1.1" 122 | description = "iniconfig: brain-dead simple config-ini parsing" 123 | category = "dev" 124 | optional = false 125 | python-versions = "*" 126 | 127 | [[package]] 128 | name = "mypy-extensions" 129 | version = "0.4.3" 130 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 131 | category = "dev" 132 | optional = false 133 | python-versions = "*" 134 | 135 | [[package]] 136 | name = "nodeenv" 137 | version = "1.6.0" 138 | description = "Node.js virtual environment builder" 139 | category = "dev" 140 | optional = false 141 | python-versions = "*" 142 | 143 | [[package]] 144 | name = "packaging" 145 | version = "21.3" 146 | description = "Core utilities for Python packages" 147 | category = "dev" 148 | optional = false 149 | python-versions = ">=3.6" 150 | 151 | [package.dependencies] 152 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 153 | 154 | [[package]] 155 | name = "pathspec" 156 | version = "0.9.0" 157 | description = "Utility library for gitignore style pattern matching of file paths." 158 | category = "dev" 159 | optional = false 160 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 161 | 162 | [[package]] 163 | name = "platformdirs" 164 | version = "2.4.0" 165 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 166 | category = "dev" 167 | optional = false 168 | python-versions = ">=3.6" 169 | 170 | [package.extras] 171 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 172 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 173 | 174 | [[package]] 175 | name = "pluggy" 176 | version = "1.0.0" 177 | description = "plugin and hook calling mechanisms for python" 178 | category = "dev" 179 | optional = false 180 | python-versions = ">=3.6" 181 | 182 | [package.extras] 183 | dev = ["pre-commit", "tox"] 184 | testing = ["pytest", "pytest-benchmark"] 185 | 186 | [[package]] 187 | name = "pre-commit" 188 | version = "2.16.0" 189 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=3.6.1" 193 | 194 | [package.dependencies] 195 | cfgv = ">=2.0.0" 196 | identify = ">=1.0.0" 197 | nodeenv = ">=0.11.1" 198 | pyyaml = ">=5.1" 199 | toml = "*" 200 | virtualenv = ">=20.0.8" 201 | 202 | [[package]] 203 | name = "py" 204 | version = "1.11.0" 205 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 206 | category = "dev" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 209 | 210 | [[package]] 211 | name = "pyparsing" 212 | version = "3.0.6" 213 | description = "Python parsing module" 214 | category = "dev" 215 | optional = false 216 | python-versions = ">=3.6" 217 | 218 | [package.extras] 219 | diagrams = ["jinja2", "railroad-diagrams"] 220 | 221 | [[package]] 222 | name = "pytest" 223 | version = "6.2.5" 224 | description = "pytest: simple powerful testing with Python" 225 | category = "dev" 226 | optional = false 227 | python-versions = ">=3.6" 228 | 229 | [package.dependencies] 230 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 231 | attrs = ">=19.2.0" 232 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 233 | iniconfig = "*" 234 | packaging = "*" 235 | pluggy = ">=0.12,<2.0" 236 | py = ">=1.8.2" 237 | toml = "*" 238 | 239 | [package.extras] 240 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 241 | 242 | [[package]] 243 | name = "pyyaml" 244 | version = "6.0" 245 | description = "YAML parser and emitter for Python" 246 | category = "dev" 247 | optional = false 248 | python-versions = ">=3.6" 249 | 250 | [[package]] 251 | name = "six" 252 | version = "1.16.0" 253 | description = "Python 2 and 3 compatibility utilities" 254 | category = "dev" 255 | optional = false 256 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 257 | 258 | [[package]] 259 | name = "toml" 260 | version = "0.10.2" 261 | description = "Python Library for Tom's Obvious, Minimal Language" 262 | category = "dev" 263 | optional = false 264 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 265 | 266 | [[package]] 267 | name = "tomli" 268 | version = "1.2.3" 269 | description = "A lil' TOML parser" 270 | category = "dev" 271 | optional = false 272 | python-versions = ">=3.6" 273 | 274 | [[package]] 275 | name = "typing-extensions" 276 | version = "4.0.1" 277 | description = "Backported and Experimental Type Hints for Python 3.6+" 278 | category = "dev" 279 | optional = false 280 | python-versions = ">=3.6" 281 | 282 | [[package]] 283 | name = "virtualenv" 284 | version = "20.10.0" 285 | description = "Virtual Python Environment builder" 286 | category = "dev" 287 | optional = false 288 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 289 | 290 | [package.dependencies] 291 | "backports.entry-points-selectable" = ">=1.0.4" 292 | distlib = ">=0.3.1,<1" 293 | filelock = ">=3.2,<4" 294 | platformdirs = ">=2,<3" 295 | six = ">=1.9.0,<2" 296 | 297 | [package.extras] 298 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 299 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 300 | 301 | [metadata] 302 | lock-version = "1.1" 303 | python-versions = ">=3.9" 304 | content-hash = "6c668f04b92f748addb7ded20738b2faa5115649efe82376f49e757c60f63b80" 305 | 306 | [metadata.files] 307 | atomicwrites = [ 308 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 309 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 310 | ] 311 | attrs = [ 312 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 313 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 314 | ] 315 | "backports.entry-points-selectable" = [ 316 | {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, 317 | {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, 318 | ] 319 | black = [ 320 | {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, 321 | {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, 322 | ] 323 | cfgv = [ 324 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 325 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 326 | ] 327 | click = [ 328 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 329 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 330 | ] 331 | colorama = [ 332 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 333 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 334 | ] 335 | distlib = [ 336 | {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, 337 | {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, 338 | ] 339 | filelock = [ 340 | {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, 341 | {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, 342 | ] 343 | identify = [ 344 | {file = "identify-2.4.0-py2.py3-none-any.whl", hash = "sha256:eba31ca80258de6bb51453084bff4a923187cd2193b9c13710f2516ab30732cc"}, 345 | {file = "identify-2.4.0.tar.gz", hash = "sha256:a33ae873287e81651c7800ca309dc1f84679b763c9c8b30680e16fbfa82f0107"}, 346 | ] 347 | iniconfig = [ 348 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 349 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 350 | ] 351 | mypy-extensions = [ 352 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 353 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 354 | ] 355 | nodeenv = [ 356 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 357 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 358 | ] 359 | packaging = [ 360 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 361 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 362 | ] 363 | pathspec = [ 364 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 365 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 366 | ] 367 | platformdirs = [ 368 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 369 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 370 | ] 371 | pluggy = [ 372 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 373 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 374 | ] 375 | pre-commit = [ 376 | {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, 377 | {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, 378 | ] 379 | py = [ 380 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 381 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 382 | ] 383 | pyparsing = [ 384 | {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, 385 | {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, 386 | ] 387 | pytest = [ 388 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 389 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 390 | ] 391 | pyyaml = [ 392 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 393 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 394 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 395 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 396 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 397 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 398 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 399 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 400 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 401 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 402 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 403 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 404 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 405 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 406 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 407 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 408 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 409 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 410 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 411 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 412 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 413 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 414 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 415 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 416 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 417 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 418 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 419 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 420 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 421 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 422 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 423 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 424 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 425 | ] 426 | six = [ 427 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 428 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 429 | ] 430 | toml = [ 431 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 432 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 433 | ] 434 | tomli = [ 435 | {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, 436 | {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, 437 | ] 438 | typing-extensions = [ 439 | {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, 440 | {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, 441 | ] 442 | virtualenv = [ 443 | {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, 444 | {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, 445 | ] 446 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ass_tag_parser" 3 | version = "2.4.1" 4 | description = "Parse ASS subtitle format tags markup." 5 | authors = ["Marcin Kurczewski "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/bubblesub/ass_tag_parser" 9 | classifiers = [ 10 | "Environment :: Other Environment", 11 | "Development Status :: 4 - Beta", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Natural Language :: English", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3.9", 17 | "Topic :: Text Processing :: Markup", 18 | "Topic :: Software Development :: Libraries :: Python Modules", 19 | ] 20 | packages = [ 21 | { include = "ass_tag_parser" } 22 | ] 23 | include = ["ass_tag_parser/py.typed"] 24 | 25 | [tool.poetry.dependencies] 26 | python = ">=3.9" 27 | 28 | [tool.poetry.dev-dependencies] 29 | pre-commit = "^2.16.0" 30 | black = "^21.12b0" 31 | pytest = "^6.2.5" 32 | 33 | [build-system] 34 | requires = ["poetry-core>=1.0.0"] 35 | build-backend = "poetry.core.masonry.api" 36 | 37 | [tool.black] 38 | line-length = 79 39 | py36 = true 40 | 41 | [tool.isort] 42 | multi_line_output = 3 43 | include_trailing_comma = true 44 | 45 | [tool.mypy] 46 | strict = true 47 | disallow_untyped_decorators = false 48 | 49 | [tool.pylint.master] 50 | jobs = 0 51 | 52 | [tool.pylint.message_control] 53 | disable = [ 54 | "import-error", 55 | "no-self-use", 56 | "missing-docstring", 57 | "too-few-public-methods", 58 | "duplicate-code", 59 | "too-many-instance-attributes", 60 | ] 61 | attr-rgx = "^io|[xy]\\d*|[a-z_][a-z0-9_]{2,}$" 62 | argument-rgx = "^io|[xy]\\d*|[a-z_][a-z0-9_]{2,30}$" 63 | variable-rgx = "^io|[xy]\\d*|[a-z_][a-z0-9_]{2,30}$" 64 | method-rgx = "^visit_\\w+|[a-z_][a-z0-9_]{2,}$" 65 | --------------------------------------------------------------------------------