Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/parser.py: 80%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

321 statements  

1"""This module parses and generates contentlines as defined in RFC 5545 

2(iCalendar), but will probably work for other MIME types with similar syntax. 

3Eg. RFC 2426 (vCard) 

4 

5It is stupid in the sense that it treats the content purely as strings. No type 

6conversion is attempted. 

7""" 

8 

9from __future__ import annotations 

10 

11import functools 

12import os 

13import re 

14from datetime import datetime, time 

15from typing import TYPE_CHECKING, Any, Callable 

16 

17from icalendar.caselessdict import CaselessDict 

18from icalendar.error import JCalParsingError 

19from icalendar.parser_tools import ( 

20 DEFAULT_ENCODING, 

21 ICAL_TYPE, 

22 SEQUENCE_TYPES, 

23 to_unicode, 

24) 

25from icalendar.timezone.tzid import tzid_from_dt 

26 

27if TYPE_CHECKING: 

28 from icalendar.enums import VALUE 

29 from icalendar.prop import VPROPERTY 

30 

31 

32def escape_char(text): 

33 """Format value according to iCalendar TEXT escaping rules.""" 

34 assert isinstance(text, (str, bytes)) 

35 # NOTE: ORDER MATTERS! 

36 return ( 

37 text.replace(r"\N", "\n") 

38 .replace("\\", "\\\\") 

39 .replace(";", r"\;") 

40 .replace(",", r"\,") 

41 .replace("\r\n", r"\n") 

42 .replace("\n", r"\n") 

43 ) 

44 

45 

46def unescape_char(text): 

47 assert isinstance(text, (str, bytes)) 

48 # NOTE: ORDER MATTERS! 

49 if isinstance(text, str): 

50 return ( 

51 text.replace("\\N", "\\n") 

52 .replace("\r\n", "\n") 

53 .replace("\\n", "\n") 

54 .replace("\\,", ",") 

55 .replace("\\;", ";") 

56 .replace("\\\\", "\\") 

57 ) 

58 if isinstance(text, bytes): 

59 return ( 

60 text.replace(b"\\N", b"\\n") 

61 .replace(b"\r\n", b"\n") 

62 .replace(b"\\n", b"\n") 

63 .replace(b"\\,", b",") 

64 .replace(b"\\;", b";") 

65 .replace(b"\\\\", b"\\") 

66 ) 

67 return None 

68 

69 

70def foldline(line, limit=75, fold_sep="\r\n "): 

71 """Make a string folded as defined in RFC5545 

72 Lines of text SHOULD NOT be longer than 75 octets, excluding the line 

73 break. Long content lines SHOULD be split into a multiple line 

74 representations using a line "folding" technique. That is, a long 

75 line can be split between any two characters by inserting a CRLF 

76 immediately followed by a single linear white-space character (i.e., 

77 SPACE or HTAB). 

78 """ 

79 assert isinstance(line, str) 

80 assert "\n" not in line 

81 

82 # Use a fast and simple variant for the common case that line is all ASCII. 

83 try: 

84 line.encode("ascii") 

85 except (UnicodeEncodeError, UnicodeDecodeError): 

86 pass 

87 else: 

88 return fold_sep.join( 

89 line[i : i + limit - 1] for i in range(0, len(line), limit - 1) 

90 ) 

91 

92 ret_chars = [] 

93 byte_count = 0 

94 for char in line: 

95 char_byte_len = len(char.encode(DEFAULT_ENCODING)) 

96 byte_count += char_byte_len 

97 if byte_count >= limit: 

98 ret_chars.append(fold_sep) 

99 byte_count = char_byte_len 

100 ret_chars.append(char) 

101 

102 return "".join(ret_chars) 

103 

104 

105################################################################# 

106# Property parameter stuff 

107 

108 

109def param_value(value, always_quote=False): 

110 """Returns a parameter value.""" 

111 if isinstance(value, SEQUENCE_TYPES): 

112 return q_join(map(rfc_6868_escape, value), always_quote=always_quote) 

113 if isinstance(value, str): 

114 return dquote(rfc_6868_escape(value), always_quote=always_quote) 

115 return dquote(rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING))) 

116 

117 

118# Could be improved 

119 

120# [\w-] because of the iCalendar RFC 

121# . because of the vCard RFC 

122NAME = re.compile(r"[\w.-]+") 

123 

124UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f",:;]') 

125QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]') 

126FOLD = re.compile(b"(\r?\n)+[ \t]") 

127UFOLD = re.compile("(\r?\n)+[ \t]") 

128NEWLINE = re.compile(r"\r?\n") 

129 

130 

131def validate_token(name): 

132 match = NAME.findall(name) 

133 if len(match) == 1 and name == match[0]: 

134 return 

135 raise ValueError(name) 

136 

137 

138def validate_param_value(value, quoted=True): 

139 validator = QUNSAFE_CHAR if quoted else UNSAFE_CHAR 

140 if validator.findall(value): 

141 raise ValueError(value) 

142 

143 

144# chars presence of which in parameter value will be cause the value 

145# to be enclosed in double-quotes 

146QUOTABLE = re.compile("[,;:’]") # noqa: RUF001 

147 

148 

149def dquote(val, always_quote=False): 

150 """Enclose parameter values containing [,;:] in double quotes.""" 

151 # a double-quote character is forbidden to appear in a parameter value 

152 # so replace it with a single-quote character 

153 val = val.replace('"', "'") 

154 if QUOTABLE.search(val) or always_quote: 

155 return f'"{val}"' 

156 return val 

157 

158 

159# parsing helper 

160def q_split(st, sep=",", maxsplit=-1): 

161 """Splits a string on char, taking double (q)uotes into considderation.""" 

162 if maxsplit == 0: 

163 return [st] 

164 

165 result = [] 

166 cursor = 0 

167 length = len(st) 

168 inquote = 0 

169 splits = 0 

170 for i, ch in enumerate(st): 

171 if ch == '"': 

172 inquote = not inquote 

173 if not inquote and ch == sep: 

174 result.append(st[cursor:i]) 

175 cursor = i + 1 

176 splits += 1 

177 if i + 1 == length or splits == maxsplit: 

178 result.append(st[cursor:]) 

179 break 

180 return result 

181 

182 

183def q_join(lst, sep=",", always_quote=False): 

184 """Joins a list on sep, quoting strings with QUOTABLE chars.""" 

185 return sep.join(dquote(itm, always_quote=always_quote) for itm in lst) 

186 

187 

188def single_string_parameter(func: Callable | None = None, upper=False): 

189 """Create a parameter getter/setter for a single string parameter. 

190 

191 Args: 

192 upper: Convert the value to uppercase 

193 func: The function to decorate. 

194 

195 Returns: 

196 The property for the parameter or a decorator for the parameter 

197 if func is ``None``. 

198 """ 

199 

200 def decorator(func): 

201 name = func.__name__ 

202 

203 @functools.wraps(func) 

204 def fget(self: Parameters): 

205 """Get the value.""" 

206 value = self.get(name) 

207 if value is not None and upper: 

208 value = value.upper() 

209 return value 

210 

211 def fset(self: Parameters, value: str | None): 

212 """Set the value""" 

213 if value is None: 

214 fdel(self) 

215 else: 

216 if upper: 

217 value = value.upper() 

218 self[name] = value 

219 

220 def fdel(self: Parameters): 

221 """Delete the value.""" 

222 self.pop(name, None) 

223 

224 return property(fget, fset, fdel, doc=func.__doc__) 

225 

226 if func is None: 

227 return decorator 

228 return decorator(func) 

229 

230 

231class Parameters(CaselessDict): 

232 """Parser and generator of Property parameter strings. 

233 

234 It knows nothing of datatypes. 

235 Its main concern is textual structure. 

236 

237 Examples: 

238 

239 Modify parameters: 

240 

241 .. code-block:: pycon 

242 

243 >>> from icalendar import Parameters 

244 >>> params = Parameters() 

245 >>> params['VALUE'] = 'TEXT' 

246 >>> params.value 

247 'TEXT' 

248 >>> params 

249 Parameters({'VALUE': 'TEXT'}) 

250 

251 Create new parameters: 

252 

253 .. code-block:: pycon 

254 

255 >>> params = Parameters(value="BINARY") 

256 >>> params.value 

257 'BINARY' 

258 

259 Set a default: 

260 

261 .. code-block:: pycon 

262 

263 >>> params = Parameters(value="BINARY", default_value="TEXT") 

264 >>> params 

265 Parameters({'VALUE': 'BINARY'}) 

266 

267 """ 

268 

269 def __init__(self, *args, **kwargs): 

270 """Create new parameters.""" 

271 if args and args[0] is None: 

272 # allow passing None 

273 args = args[1:] 

274 defaults = { 

275 key[8:]: kwargs.pop(key) 

276 for key in list(kwargs.keys()) 

277 if key.lower().startswith("default_") 

278 } 

279 super().__init__(*args, **kwargs) 

280 for key, value in defaults.items(): 

281 self.setdefault(key, value) 

282 

283 # The following paremeters must always be enclosed in double quotes 

284 always_quoted = ( 

285 "ALTREP", 

286 "DELEGATED-FROM", 

287 "DELEGATED-TO", 

288 "DIR", 

289 "MEMBER", 

290 "SENT-BY", 

291 # Part of X-APPLE-STRUCTURED-LOCATION 

292 "X-ADDRESS", 

293 "X-TITLE", 

294 # RFC 9253 

295 "LINKREL", 

296 ) 

297 # this is quoted should one of the values be present 

298 quote_also = { 

299 # This is escaped in the RFC 

300 "CN": " '", 

301 } 

302 

303 def params(self): 

304 """In RFC 5545 keys are called parameters, so this is to be consitent 

305 with the naming conventions. 

306 """ 

307 return self.keys() 

308 

309 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001 

310 """Returns an :rfc:`5545` representation of the parameters. 

311 

312 Args: 

313 sorted (bool): Sort the parameters before encoding. 

314 exclude_utc (bool): Exclude TZID if it is set to ``"UTC"`` 

315 """ 

316 result = [] 

317 items = list(self.items()) 

318 if sorted: 

319 items.sort() 

320 

321 for key, value in items: 

322 if key == "TZID" and value == "UTC": 

323 # The "TZID" property parameter MUST NOT be applied to DATE-TIME 

324 # properties whose time values are specified in UTC. 

325 continue 

326 upper_key = key.upper() 

327 check_quoteable_characters = self.quote_also.get(key.upper()) 

328 always_quote = upper_key in self.always_quoted or ( 

329 check_quoteable_characters 

330 and any(c in value for c in check_quoteable_characters) 

331 ) 

332 quoted_value = param_value(value, always_quote=always_quote) 

333 if isinstance(quoted_value, str): 

334 quoted_value = quoted_value.encode(DEFAULT_ENCODING) 

335 # CaselessDict keys are always unicode 

336 result.append(upper_key.encode(DEFAULT_ENCODING) + b"=" + quoted_value) 

337 return b";".join(result) 

338 

339 @classmethod 

340 def from_ical(cls, st, strict=False): 

341 """Parses the parameter format from ical text format.""" 

342 

343 # parse into strings 

344 result = cls() 

345 for param in q_split(st, ";"): 

346 try: 

347 key, val = q_split(param, "=", maxsplit=1) 

348 validate_token(key) 

349 # Property parameter values that are not in quoted 

350 # strings are case insensitive. 

351 vals = [] 

352 for v in q_split(val, ","): 

353 if v.startswith('"') and v.endswith('"'): 

354 v2 = v.strip('"') 

355 validate_param_value(v2, quoted=True) 

356 vals.append(rfc_6868_unescape(v2)) 

357 else: 

358 validate_param_value(v, quoted=False) 

359 if strict: 

360 vals.append(rfc_6868_unescape(v.upper())) 

361 else: 

362 vals.append(rfc_6868_unescape(v)) 

363 if not vals: 

364 result[key] = val 

365 elif len(vals) == 1: 

366 result[key] = vals[0] 

367 else: 

368 result[key] = vals 

369 except ValueError as exc: # noqa: PERF203 

370 raise ValueError( 

371 f"{param!r} is not a valid parameter string: {exc}" 

372 ) from exc 

373 return result 

374 

375 @single_string_parameter(upper=True) 

376 def value(self) -> VALUE | str | None: 

377 """The VALUE parameter from :rfc:`5545`. 

378 

379 Description: 

380 This parameter specifies the value type and format of 

381 the property value. The property values MUST be of a single value 

382 type. For example, a "RDATE" property cannot have a combination 

383 of DATE-TIME and TIME value types. 

384 

385 If the property's value is the default value type, then this 

386 parameter need not be specified. However, if the property's 

387 default value type is overridden by some other allowable value 

388 type, then this parameter MUST be specified. 

389 

390 Applications MUST preserve the value data for x-name and iana- 

391 token values that they don't recognize without attempting to 

392 interpret or parse the value data. 

393 

394 For convenience, using this property, the value will be converted to 

395 an uppercase string. 

396 

397 .. code-block:: pycon 

398 

399 >>> from icalendar import Parameters 

400 >>> params = Parameters() 

401 >>> params.value = "unknown" 

402 >>> params 

403 Parameters({'VALUE': 'UNKNOWN'}) 

404 

405 """ 

406 

407 def _parameter_value_to_jcal( 

408 self, value: str | float | list | VPROPERTY 

409 ) -> str | int | float | list[str] | list[int] | list[float]: 

410 """Convert a parameter value to jCal format. 

411 

412 Args: 

413 value: The parameter value 

414 

415 Returns: 

416 The jCal representation of the parameter value 

417 """ 

418 if isinstance(value, list): 

419 return [self._parameter_value_to_jcal(v) for v in value] 

420 if hasattr(value, "to_jcal"): 

421 # proprty values respond to this 

422 jcal = value.to_jcal() 

423 # we only need the value part 

424 if len(jcal) == 4: 

425 return jcal[3] 

426 return jcal[3:] 

427 for t in (int, float, str): 

428 if isinstance(value, t): 

429 return t(value) 

430 raise TypeError( 

431 "Unsupported parameter value type for jCal conversion: " 

432 f"{type(value)} {value!r}" 

433 ) 

434 

435 def to_jcal(self, exclude_utc=False) -> dict[str, str]: 

436 """Return the jCal representation of the parameters. 

437 

438 Args: 

439 exclude_utc (bool): Exclude the TZID parameter if it is UTC 

440 """ 

441 jcal = { 

442 k.lower(): self._parameter_value_to_jcal(v) 

443 for k, v in self.items() 

444 if k.lower() != "value" 

445 } 

446 if exclude_utc and jcal.get("tzid") == "UTC": 

447 del jcal["tzid"] 

448 return jcal 

449 

450 @single_string_parameter 

451 def tzid(self) -> str | None: 

452 """The TZID parameter from :rfc:`5545`.""" 

453 

454 def is_utc(self): 

455 """Whether the TZID parameter is UTC.""" 

456 return self.tzid == "UTC" 

457 

458 def update_tzid_from(self, dt: datetime | time | Any) -> None: 

459 """Update the TZID parameter from a datetime object. 

460 

461 This sets the TZID parameter or deletes it according to the datetime. 

462 """ 

463 if isinstance(dt, (datetime, time)): 

464 self.tzid = tzid_from_dt(dt) 

465 

466 @classmethod 

467 def from_jcal(cls, jcal: dict[str : str | list[str]]): 

468 """Parse jCal parameters.""" 

469 if not isinstance(jcal, dict): 

470 raise JCalParsingError("The parameters must be a mapping.", cls) 

471 for name, value in jcal.items(): 

472 if not isinstance(name, str): 

473 raise JCalParsingError( 

474 "All parameter names must be strings.", cls, value=name 

475 ) 

476 if not ( 

477 ( 

478 isinstance(value, list) 

479 and all(isinstance(v, (str, int, float)) for v in value) 

480 and value 

481 ) 

482 or isinstance(value, (str, int, float)) 

483 ): 

484 raise JCalParsingError( 

485 "Parameter values must be a string, integer or " 

486 "float or a list of those.", 

487 cls, 

488 name, 

489 value=value, 

490 ) 

491 return cls(jcal) 

492 

493 @classmethod 

494 def from_jcal_property(cls, jcal_property: list): 

495 """Create the parameters for a jCal property. 

496 

497 Args: 

498 jcal_property (list): The jCal property [name, params, value, ...] 

499 default_value (str, optional): The default value of the property. 

500 If this is given, the default value will not be set. 

501 """ 

502 if not isinstance(jcal_property, list) or len(jcal_property) < 4: 

503 raise JCalParsingError( 

504 "The property must be a list with at least 4 items.", cls 

505 ) 

506 jcal_params = jcal_property[1] 

507 with JCalParsingError.reraise_with_path_added(1): 

508 self = cls.from_jcal(jcal_params) 

509 if self.is_utc(): 

510 del self.tzid # we do not want this parameter 

511 return self 

512 

513 

514def escape_string(val): 

515 # f'{i:02X}' 

516 return ( 

517 val.replace(r"\,", "%2C") 

518 .replace(r"\:", "%3A") 

519 .replace(r"\;", "%3B") 

520 .replace(r"\\", "%5C") 

521 ) 

522 

523 

524def unescape_string(val): 

525 return ( 

526 val.replace("%2C", ",") 

527 .replace("%3A", ":") 

528 .replace("%3B", ";") 

529 .replace("%5C", "\\") 

530 ) 

531 

532 

533_unescape_backslash_regex = re.compile(r"\\([\\,;:nN])") 

534 

535 

536def unescape_backslash(val: str): 

537 r"""Unescape backslash sequences in iCalendar text. 

538 

539 Unlike :py:meth:`unescape_string`, this only handles actual backslash escapes 

540 per :rfc:`5545`, not URL encoding. This preserves URL-encoded values 

541 like ``%3A`` in URLs. 

542 

543 Processes backslash escape sequences in a single pass using regex matching. 

544 """ 

545 return _unescape_backslash_regex.sub( 

546 lambda m: "\n" if m.group(1) in "nN" else m.group(1), val 

547 ) 

548 

549 

550def split_on_unescaped_semicolon(text: str) -> list[str]: 

551 r"""Split text on unescaped semicolons and unescape each part. 

552 

553 Splits only on semicolons not preceded by a backslash. 

554 After splitting, unescapes backslash sequences in each part. 

555 Used by vCard structured properties (ADR, N, ORG) per :rfc:`6350`. 

556 

557 Args: 

558 text: Text with potential escaped semicolons (e.g., "field1\\;with;field2") 

559 

560 Returns: 

561 List of unescaped field strings 

562 

563 Examples: 

564 .. code-block:: pycon 

565 

566 >>> from icalendar.parser import split_on_unescaped_semicolon 

567 >>> split_on_unescaped_semicolon(r"field1\;with;field2") 

568 ['field1;with', 'field2'] 

569 >>> split_on_unescaped_semicolon("a;b;c") 

570 ['a', 'b', 'c'] 

571 >>> split_on_unescaped_semicolon(r"a\;b\;c") 

572 ['a;b;c'] 

573 >>> split_on_unescaped_semicolon(r"PO Box 123\;Suite 200;City") 

574 ['PO Box 123;Suite 200', 'City'] 

575 """ 

576 if not text: 

577 return [""] 

578 

579 result = [] 

580 current = [] 

581 i = 0 

582 

583 while i < len(text): 

584 if text[i] == "\\" and i + 1 < len(text): 

585 # Escaped character - keep both backslash and next char 

586 current.append(text[i]) 

587 current.append(text[i + 1]) 

588 i += 2 

589 elif text[i] == ";": 

590 # Unescaped semicolon - split point 

591 result.append(unescape_backslash("".join(current))) 

592 current = [] 

593 i += 1 

594 else: 

595 current.append(text[i]) 

596 i += 1 

597 

598 # Add final part 

599 result.append(unescape_backslash("".join(current))) 

600 

601 return result 

602 

603 

604RFC_6868_UNESCAPE_REGEX = re.compile(r"\^\^|\^n|\^'") 

605 

606 

607def rfc_6868_unescape(param_value: str) -> str: 

608 """Take care of :rfc:`6868` unescaping. 

609 

610 - ^^ -> ^ 

611 - ^n -> system specific newline 

612 - ^' -> " 

613 - ^ with others stay intact 

614 """ 

615 replacements = { 

616 "^^": "^", 

617 "^n": os.linesep, 

618 "^'": '"', 

619 } 

620 return RFC_6868_UNESCAPE_REGEX.sub( 

621 lambda m: replacements.get(m.group(0), m.group(0)), param_value 

622 ) 

623 

624 

625RFC_6868_ESCAPE_REGEX = re.compile(r'\^|\r\n|\r|\n|"') 

626 

627 

628def rfc_6868_escape(param_value: str) -> str: 

629 """Take care of :rfc:`6868` escaping. 

630 

631 - ^ -> ^^ 

632 - " -> ^' 

633 - newline -> ^n 

634 """ 

635 replacements = { 

636 "^": "^^", 

637 "\n": "^n", 

638 "\r": "^n", 

639 "\r\n": "^n", 

640 '"': "^'", 

641 } 

642 return RFC_6868_ESCAPE_REGEX.sub( 

643 lambda m: replacements.get(m.group(0), m.group(0)), param_value 

644 ) 

645 

646 

647def unescape_list_or_string(val): 

648 if isinstance(val, list): 

649 return [unescape_string(s) for s in val] 

650 return unescape_string(val) 

651 

652 

653######################################### 

654# parsing and generation of content lines 

655 

656 

657class Contentline(str): 

658 """A content line is basically a string that can be folded and parsed into 

659 parts. 

660 """ 

661 

662 __slots__ = ("strict",) 

663 

664 def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING): 

665 value = to_unicode(value, encoding=encoding) 

666 assert "\n" not in value, ( 

667 "Content line can not contain unescaped new line characters." 

668 ) 

669 self = super().__new__(cls, value) 

670 self.strict = strict 

671 return self 

672 

673 @classmethod 

674 def from_parts( 

675 cls, 

676 name: ICAL_TYPE, 

677 params: Parameters, 

678 values, 

679 sorted: bool = True, # noqa: A002, FBT001 

680 ): 

681 """Turn a parts into a content line.""" 

682 assert isinstance(params, Parameters) 

683 if hasattr(values, "to_ical"): 

684 values = values.to_ical() 

685 else: 

686 from icalendar.prop import vText 

687 

688 values = vText(values).to_ical() 

689 # elif isinstance(values, basestring): 

690 # values = escape_char(values) 

691 

692 # TODO: after unicode only, remove this 

693 # Convert back to unicode, after to_ical encoded it. 

694 name = to_unicode(name) 

695 values = to_unicode(values) 

696 if params: 

697 params = to_unicode(params.to_ical(sorted=sorted)) 

698 if params: 

699 # some parameter values can be skipped during serialization 

700 return cls(f"{name};{params}:{values}") 

701 return cls(f"{name}:{values}") 

702 

703 def parts(self) -> tuple[str, Parameters, str]: 

704 """Split the content line into ``name``, ``parameters``, and ``values`` parts. 

705 

706 Properly handles escaping with backslashes and double-quote sections 

707 to avoid corrupting URL-encoded characters in values. 

708 

709 Example with parameter: 

710 

711 .. code-block:: text 

712 

713 DESCRIPTION;ALTREP="cid:part1.0001@example.org":The Fall'98 Wild 

714 

715 Example without parameters: 

716 

717 .. code-block:: text 

718 

719 DESCRIPTION:The Fall'98 Wild 

720 """ 

721 try: 

722 name_split: int | None = None 

723 value_split: int | None = None 

724 in_quotes: bool = False 

725 escaped: bool = False 

726 

727 for i, ch in enumerate(self): 

728 if ch == '"' and not escaped: 

729 in_quotes = not in_quotes 

730 elif ch == "\\" and not in_quotes: 

731 escaped = True 

732 continue 

733 elif not in_quotes and not escaped: 

734 # Find first delimiter for name 

735 if ch in ":;" and name_split is None: 

736 name_split = i 

737 # Find value delimiter (first colon) 

738 if ch == ":" and value_split is None: 

739 value_split = i 

740 

741 escaped = False 

742 

743 # Validate parsing results 

744 if not value_split: 

745 # No colon found - value is empty, use end of string 

746 value_split = len(self) 

747 

748 # Extract name - if no delimiter, 

749 # take whole string for validate_token to reject 

750 name = self[:name_split] if name_split else self 

751 validate_token(name) 

752 

753 if not name_split or name_split + 1 == value_split: 

754 # No delimiter or empty parameter section 

755 raise ValueError("Invalid content line") # noqa: TRY301 

756 # Parse parameters - they still need to be escaped/unescaped 

757 # for proper handling of commas, semicolons, etc. in parameter values 

758 param_str = escape_string(self[name_split + 1 : value_split]) 

759 params = Parameters.from_ical(param_str, strict=self.strict) 

760 params = Parameters( 

761 (unescape_string(key), unescape_list_or_string(value)) 

762 for key, value in iter(params.items()) 

763 ) 

764 # Unescape backslash sequences in values but preserve URL encoding 

765 values = unescape_backslash(self[value_split + 1 :]) 

766 except ValueError as exc: 

767 raise ValueError( 

768 f"Content line could not be parsed into parts: '{self}': {exc}" 

769 ) from exc 

770 return (name, params, values) 

771 

772 @classmethod 

773 def from_ical(cls, ical, strict=False): 

774 """Unfold the content lines in an iCalendar into long content lines.""" 

775 ical = to_unicode(ical) 

776 # a fold is carriage return followed by either a space or a tab 

777 return cls(UFOLD.sub("", ical), strict=strict) 

778 

779 def to_ical(self): 

780 """Long content lines are folded so they are less than 75 characters 

781 wide. 

782 """ 

783 return foldline(self).encode(DEFAULT_ENCODING) 

784 

785 

786class Contentlines(list): 

787 """I assume that iCalendar files generally are a few kilobytes in size. 

788 Then this should be efficient. for Huge files, an iterator should probably 

789 be used instead. 

790 """ 

791 

792 def to_ical(self): 

793 """Simply join self.""" 

794 return b"\r\n".join(line.to_ical() for line in self if line) + b"\r\n" 

795 

796 @classmethod 

797 def from_ical(cls, st): 

798 """Parses a string into content lines.""" 

799 st = to_unicode(st) 

800 try: 

801 # a fold is carriage return followed by either a space or a tab 

802 unfolded = UFOLD.sub("", st) 

803 lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line) 

804 lines.append("") # '\r\n' at the end of every content line 

805 except Exception as e: 

806 raise ValueError("Expected StringType with content lines") from e 

807 return lines 

808 

809 

810__all__ = [ 

811 "FOLD", 

812 "NAME", 

813 "NEWLINE", 

814 "QUNSAFE_CHAR", 

815 "QUOTABLE", 

816 "UFOLD", 

817 "UNSAFE_CHAR", 

818 "Contentline", 

819 "Contentlines", 

820 "Parameters", 

821 "dquote", 

822 "escape_char", 

823 "escape_string", 

824 "foldline", 

825 "param_value", 

826 "q_join", 

827 "q_split", 

828 "rfc_6868_escape", 

829 "rfc_6868_unescape", 

830 "split_on_unescaped_semicolon", 

831 "unescape_backslash", 

832 "unescape_char", 

833 "unescape_list_or_string", 

834 "unescape_string", 

835 "validate_param_value", 

836 "validate_token", 

837]