Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tinycss2/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

188 statements  

1from itertools import chain 

2 

3from .ast import AtRule, Declaration, ParseError, QualifiedRule 

4from .tokenizer import parse_component_value_list 

5 

6 

7def _to_token_iterator(input, skip_comments=False): 

8 """Iterate component values out of string or component values iterable. 

9 

10 :type input: :obj:`str` or :term:`iterable` 

11 :param input: A string or an iterable of :term:`component values`. 

12 :type skip_comments: :obj:`bool` 

13 :param skip_comments: If the input is a string, ignore all CSS comments. 

14 :returns: An iterator yielding :term:`component values`. 

15 

16 """ 

17 if isinstance(input, str): 

18 input = parse_component_value_list(input, skip_comments) 

19 return iter(input) 

20 

21 

22def _next_significant(tokens): 

23 """Return the next significant (neither whitespace or comment) token. 

24 

25 :type tokens: :term:`iterator` 

26 :param tokens: An iterator yielding :term:`component values`. 

27 :returns: A :term:`component value`, or :obj:`None`. 

28 

29 """ 

30 for token in tokens: 

31 if token.type not in ('whitespace', 'comment'): 

32 return token 

33 

34 

35def parse_one_component_value(input, skip_comments=False): 

36 """Parse a single :diagram:`component value`. 

37 

38 This is used e.g. for an attribute value 

39 referred to by ``attr(foo length)``. 

40 

41 :type input: :obj:`str` or :term:`iterable` 

42 :param input: A string or an iterable of :term:`component values`. 

43 :type skip_comments: :obj:`bool` 

44 :param skip_comments: If the input is a string, ignore all CSS comments. 

45 :returns: 

46 A :term:`component value` (that is neither whitespace or comment), 

47 or a :class:`~tinycss2.ast.ParseError`. 

48 

49 """ 

50 tokens = _to_token_iterator(input, skip_comments) 

51 first = _next_significant(tokens) 

52 second = _next_significant(tokens) 

53 if first is None: 

54 return ParseError(1, 1, 'empty', 'Input is empty') 

55 if second is not None: 

56 return ParseError( 

57 second.source_line, second.source_column, 'extra-input', 

58 'Got more than one token') 

59 else: 

60 return first 

61 

62 

63def parse_one_declaration(input, skip_comments=False): 

64 """Parse a single :diagram:`declaration`. 

65 

66 This is used e.g. for a declaration in an `@supports 

67 <https://drafts.csswg.org/css-conditional/#at-supports>`_ test. 

68 

69 :type input: :obj:`str` or :term:`iterable` 

70 :param input: A string or an iterable of :term:`component values`. 

71 :type skip_comments: :obj:`bool` 

72 :param skip_comments: If the input is a string, ignore all CSS comments. 

73 :returns: 

74 A :class:`~tinycss2.ast.Declaration` 

75 or :class:`~tinycss2.ast.ParseError`. 

76 

77 Any whitespace or comment before the ``:`` colon is dropped. 

78 

79 """ 

80 tokens = _to_token_iterator(input, skip_comments) 

81 first_token = _next_significant(tokens) 

82 if first_token is None: 

83 return ParseError(1, 1, 'empty', 'Input is empty') 

84 return _parse_declaration(first_token, tokens) 

85 

86 

87def _consume_remnants(input, nested): 

88 for token in input: 

89 if token == ';': 

90 return 

91 elif nested and token == '}': 

92 return 

93 

94 

95def _parse_declaration(first_token, tokens, nested=True): 

96 """Parse a declaration. 

97 

98 Consume :obj:`tokens` until the end of the declaration or the first error. 

99 

100 :type first_token: :term:`component value` 

101 :param first_token: The first component value of the rule. 

102 :type tokens: :term:`iterator` 

103 :param tokens: An iterator yielding :term:`component values`. 

104 :type nested: :obj:`bool` 

105 :param nested: Whether the declaration is nested or top-level. 

106 :returns: 

107 A :class:`~tinycss2.ast.Declaration` 

108 or :class:`~tinycss2.ast.ParseError`. 

109 

110 """ 

111 name = first_token 

112 if name.type != 'ident': 

113 _consume_remnants(tokens, nested) 

114 return ParseError( 

115 name.source_line, name.source_column, 'invalid', 

116 f'Expected <ident> for declaration name, got {name.type}.') 

117 

118 colon = _next_significant(tokens) 

119 if colon is None: 

120 _consume_remnants(tokens, nested) 

121 return ParseError( 

122 name.source_line, name.source_column, 'invalid', 

123 "Expected ':' after declaration name, got EOF") 

124 elif colon != ':': 

125 _consume_remnants(tokens, nested) 

126 return ParseError( 

127 colon.source_line, colon.source_column, 'invalid', 

128 "Expected ':' after declaration name, got {colon.type}.") 

129 

130 value = [] 

131 state = 'value' 

132 contains_non_whitespace = False 

133 contains_simple_block = False 

134 for i, token in enumerate(tokens): 

135 if state == 'value' and token == '!': 

136 state = 'bang' 

137 bang_position = i 

138 elif (state == 'bang' and token.type == 'ident' 

139 and token.lower_value == 'important'): 

140 state = 'important' 

141 elif token.type not in ('whitespace', 'comment'): 

142 state = 'value' 

143 if token.type == '{} block': 

144 if contains_non_whitespace: 

145 contains_simple_block = True 

146 else: 

147 contains_non_whitespace = True 

148 else: 

149 contains_non_whitespace = True 

150 value.append(token) 

151 

152 if state == 'important': 

153 del value[bang_position:] 

154 

155 # TODO: Handle custom property names 

156 

157 if contains_simple_block and contains_non_whitespace: 

158 return ParseError( 

159 colon.source_line, colon.source_column, 'invalid', 

160 'Declaration contains {} block') 

161 

162 # TODO: Handle unicode-range 

163 

164 return Declaration( 

165 name.source_line, name.source_column, name.value, name.lower_value, 

166 value, state == 'important') 

167 

168 

169def _consume_blocks_content(first_token, tokens): 

170 """Consume declaration or nested rule.""" 

171 declaration_tokens = [] 

172 semicolon_token = [] 

173 if first_token != ';' and first_token.type != '{} block': 

174 for token in tokens: 

175 if token == ';': 

176 semicolon_token.append(token) 

177 break 

178 declaration_tokens.append(token) 

179 if token.type == '{} block': 

180 break 

181 declaration = _parse_declaration( 

182 first_token, iter(declaration_tokens), nested=True) 

183 if declaration.type == 'declaration': 

184 return declaration 

185 else: 

186 tokens = chain(declaration_tokens, semicolon_token, tokens) 

187 return _consume_qualified_rule(first_token, tokens, stop_token=';', nested=True) 

188 

189 

190def _consume_declaration_in_list(first_token, tokens): 

191 """Like :func:`_parse_declaration`, but stop at the first ``;``. 

192 

193 Deprecated, use :func:`_consume_blocks_content` instead. 

194 

195 """ 

196 other_declaration_tokens = [] 

197 for token in tokens: 

198 if token == ';': 

199 break 

200 other_declaration_tokens.append(token) 

201 return _parse_declaration(first_token, iter(other_declaration_tokens)) 

202 

203 

204def parse_blocks_contents(input, skip_comments=False, skip_whitespace=False): 

205 """Parse a block’s contents. 

206 

207 This is used e.g. for the :attr:`~tinycss2.ast.QualifiedRule.content` 

208 of a style rule or ``@page`` rule, or for the ``style`` attribute of an 

209 HTML element. 

210 

211 In contexts that don’t expect any at-rule and/or qualified rule, 

212 all :class:`~tinycss2.ast.AtRule` and/or 

213 :class:`~tinycss2.ast.QualifiedRule` objects should simply be rejected as 

214 invalid. 

215 

216 :type input: :obj:`str` or :term:`iterable` 

217 :param input: A string or an iterable of :term:`component values`. 

218 :type skip_comments: :obj:`bool` 

219 :param skip_comments: 

220 Ignore CSS comments at the top-level of the list. 

221 If the input is a string, ignore all comments. 

222 :type skip_whitespace: :obj:`bool` 

223 :param skip_whitespace: 

224 Ignore whitespace at the top-level of the list. 

225 Whitespace is still preserved 

226 in the :attr:`~tinycss2.ast.Declaration.value` of declarations 

227 and the :attr:`~tinycss2.ast.AtRule.prelude` 

228 and :attr:`~tinycss2.ast.AtRule.content` of at-rules. 

229 :returns: 

230 A list of 

231 :class:`~tinycss2.ast.Declaration`, 

232 :class:`~tinycss2.ast.AtRule`, 

233 :class:`~tinycss2.ast.QualifiedRule`, 

234 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false), 

235 :class:`~tinycss2.ast.WhitespaceToken` 

236 (if ``skip_whitespace`` is false), 

237 and :class:`~tinycss2.ast.ParseError` objects 

238 

239 """ 

240 tokens = _to_token_iterator(input, skip_comments) 

241 result = [] 

242 for token in tokens: 

243 if token.type == 'whitespace': 

244 if not skip_whitespace: 

245 result.append(token) 

246 elif token.type == 'comment': 

247 if not skip_comments: 

248 result.append(token) 

249 elif token.type == 'at-keyword': 

250 result.append(_consume_at_rule(token, tokens)) 

251 elif token != ';': 

252 result.append(_consume_blocks_content(token, tokens)) 

253 return result 

254 

255 

256def parse_declaration_list(input, skip_comments=False, skip_whitespace=False): 

257 """Parse a :diagram:`declaration list` (which may also contain at-rules). 

258 

259 Deprecated and removed from CSS Syntax Level 3. Use 

260 :func:`parse_blocks_contents` instead. 

261 

262 This is used e.g. for the :attr:`~tinycss2.ast.QualifiedRule.content` 

263 of a style rule or ``@page`` rule, or for the ``style`` attribute of an 

264 HTML element. 

265 

266 In contexts that don’t expect any at-rule, all 

267 :class:`~tinycss2.ast.AtRule` objects should simply be rejected as invalid. 

268 

269 :type input: :obj:`str` or :term:`iterable` 

270 :param input: A string or an iterable of :term:`component values`. 

271 :type skip_comments: :obj:`bool` 

272 :param skip_comments: 

273 Ignore CSS comments at the top-level of the list. 

274 If the input is a string, ignore all comments. 

275 :type skip_whitespace: :obj:`bool` 

276 :param skip_whitespace: 

277 Ignore whitespace at the top-level of the list. 

278 Whitespace is still preserved 

279 in the :attr:`~tinycss2.ast.Declaration.value` of declarations 

280 and the :attr:`~tinycss2.ast.AtRule.prelude` 

281 and :attr:`~tinycss2.ast.AtRule.content` of at-rules. 

282 :returns: 

283 A list of 

284 :class:`~tinycss2.ast.Declaration`, 

285 :class:`~tinycss2.ast.AtRule`, 

286 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false), 

287 :class:`~tinycss2.ast.WhitespaceToken` 

288 (if ``skip_whitespace`` is false), 

289 and :class:`~tinycss2.ast.ParseError` objects 

290 

291 """ 

292 tokens = _to_token_iterator(input, skip_comments) 

293 result = [] 

294 for token in tokens: 

295 if token.type == 'whitespace': 

296 if not skip_whitespace: 

297 result.append(token) 

298 elif token.type == 'comment': 

299 if not skip_comments: 

300 result.append(token) 

301 elif token.type == 'at-keyword': 

302 result.append(_consume_at_rule(token, tokens)) 

303 elif token != ';': 

304 result.append(_consume_declaration_in_list(token, tokens)) 

305 return result 

306 

307 

308def parse_one_rule(input, skip_comments=False): 

309 """Parse a single :diagram:`qualified rule` or :diagram:`at-rule`. 

310 

311 This would be used e.g. by `insertRule() 

312 <https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule>`_ 

313 in an implementation of CSSOM. 

314 

315 :type input: :obj:`str` or :term:`iterable` 

316 :param input: A string or an iterable of :term:`component values`. 

317 :type skip_comments: :obj:`bool` 

318 :param skip_comments: 

319 If the input is a string, ignore all CSS comments. 

320 :returns: 

321 A :class:`~tinycss2.ast.QualifiedRule`, 

322 :class:`~tinycss2.ast.AtRule`, 

323 or :class:`~tinycss2.ast.ParseError` objects. 

324 

325 Any whitespace or comment before or after the rule is dropped. 

326 

327 """ 

328 tokens = _to_token_iterator(input, skip_comments) 

329 first = _next_significant(tokens) 

330 if first is None: 

331 return ParseError(1, 1, 'empty', 'Input is empty') 

332 

333 rule = _consume_rule(first, tokens) 

334 next = _next_significant(tokens) 

335 if next is not None: 

336 return ParseError( 

337 next.source_line, next.source_column, 'extra-input', 

338 'Expected a single rule, got %s after the first rule.' % next.type) 

339 return rule 

340 

341 

342def parse_rule_list(input, skip_comments=False, skip_whitespace=False): 

343 """Parse a non-top-level :diagram:`rule list`. 

344 

345 Deprecated and removed from CSS Syntax. Use :func:`parse_blocks_contents` 

346 instead. 

347 

348 This is used for parsing the :attr:`~tinycss2.ast.AtRule.content` 

349 of nested rules like ``@media``. 

350 This differs from :func:`parse_stylesheet` in that 

351 top-level ``<!--`` and ``-->`` tokens are not ignored. 

352 

353 :type input: :obj:`str` or :term:`iterable` 

354 :param input: A string or an iterable of :term:`component values`. 

355 :type skip_comments: :obj:`bool` 

356 :param skip_comments: 

357 Ignore CSS comments at the top-level of the list. 

358 If the input is a string, ignore all comments. 

359 :type skip_whitespace: :obj:`bool` 

360 :param skip_whitespace: 

361 Ignore whitespace at the top-level of the list. 

362 Whitespace is still preserved 

363 in the :attr:`~tinycss2.ast.QualifiedRule.prelude` 

364 and the :attr:`~tinycss2.ast.QualifiedRule.content` of rules. 

365 :returns: 

366 A list of 

367 :class:`~tinycss2.ast.QualifiedRule`, 

368 :class:`~tinycss2.ast.AtRule`, 

369 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false), 

370 :class:`~tinycss2.ast.WhitespaceToken` 

371 (if ``skip_whitespace`` is false), 

372 and :class:`~tinycss2.ast.ParseError` objects. 

373 

374 """ 

375 tokens = _to_token_iterator(input, skip_comments) 

376 result = [] 

377 for token in tokens: 

378 if token.type == 'whitespace': 

379 if not skip_whitespace: 

380 result.append(token) 

381 elif token.type == 'comment': 

382 if not skip_comments: 

383 result.append(token) 

384 else: 

385 result.append(_consume_rule(token, tokens)) 

386 return result 

387 

388 

389def parse_stylesheet(input, skip_comments=False, skip_whitespace=False): 

390 """Parse :diagram:`stylesheet` from text. 

391 

392 This is used e.g. for a ``<style>`` HTML element. 

393 

394 This differs from :func:`parse_rule_list` in that 

395 top-level ``<!--`` and ``-->`` tokens are ignored. 

396 This is a legacy quirk for the ``<style>`` HTML element. 

397 

398 :type input: :obj:`str` or :term:`iterable` 

399 :param input: A string or an iterable of :term:`component values`. 

400 :type skip_comments: :obj:`bool` 

401 :param skip_comments: 

402 Ignore CSS comments at the top-level of the stylesheet. 

403 If the input is a string, ignore all comments. 

404 :type skip_whitespace: :obj:`bool` 

405 :param skip_whitespace: 

406 Ignore whitespace at the top-level of the stylesheet. 

407 Whitespace is still preserved 

408 in the :attr:`~tinycss2.ast.QualifiedRule.prelude` 

409 and the :attr:`~tinycss2.ast.QualifiedRule.content` of rules. 

410 :returns: 

411 A list of 

412 :class:`~tinycss2.ast.QualifiedRule`, 

413 :class:`~tinycss2.ast.AtRule`, 

414 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false), 

415 :class:`~tinycss2.ast.WhitespaceToken` 

416 (if ``skip_whitespace`` is false), 

417 and :class:`~tinycss2.ast.ParseError` objects. 

418 

419 """ 

420 tokens = _to_token_iterator(input, skip_comments) 

421 result = [] 

422 for token in tokens: 

423 if token.type == 'whitespace': 

424 if not skip_whitespace: 

425 result.append(token) 

426 elif token.type == 'comment': 

427 if not skip_comments: 

428 result.append(token) 

429 elif token not in ('<!--', '-->'): 

430 result.append(_consume_rule(token, tokens)) 

431 return result 

432 

433 

434def _consume_rule(first_token, tokens): 

435 """Parse a qualified rule or at-rule. 

436 

437 Consume just enough of :obj:`tokens` for this rule. 

438 

439 :type first_token: :term:`component value` 

440 :param first_token: The first component value of the rule. 

441 :type tokens: :term:`iterator` 

442 :param tokens: An iterator yielding :term:`component values`. 

443 :returns: 

444 A :class:`~tinycss2.ast.QualifiedRule`, 

445 :class:`~tinycss2.ast.AtRule`, 

446 or :class:`~tinycss2.ast.ParseError`. 

447 

448 """ 

449 if first_token.type == 'at-keyword': 

450 return _consume_at_rule(first_token, tokens) 

451 return _consume_qualified_rule(first_token, tokens) 

452 

453 

454def _consume_at_rule(at_keyword, tokens): 

455 """Parse an at-rule. 

456 

457 Consume just enough of :obj:`tokens` for this rule. 

458 

459 :type at_keyword: :class:`AtKeywordToken` 

460 :param at_keyword: The at-rule keyword token starting this rule. 

461 :type tokens: :term:`iterator` 

462 :param tokens: An iterator yielding :term:`component values`. 

463 :type nested: :obj:`bool` 

464 :param nested: Whether the at-rule is nested or top-level. 

465 :returns: 

466 A :class:`~tinycss2.ast.QualifiedRule`, 

467 or :class:`~tinycss2.ast.ParseError`. 

468 

469 """ 

470 prelude = [] 

471 content = None 

472 for token in tokens: 

473 if token.type == '{} block': 

474 # TODO: handle nested at-rules 

475 # https://drafts.csswg.org/css-syntax-3/#consume-at-rule 

476 content = token.content 

477 break 

478 elif token == ';': 

479 break 

480 prelude.append(token) 

481 return AtRule( 

482 at_keyword.source_line, at_keyword.source_column, at_keyword.value, 

483 at_keyword.lower_value, prelude, content) 

484 

485 

486def _rule_error(token, name): 

487 """Create rule parse error raised because of given token.""" 

488 return ParseError( 

489 token.source_line, token.source_column, 'invalid', 

490 f'{name} reached before {{}} block for a qualified rule.') 

491 

492 

493def _consume_qualified_rule(first_token, tokens, nested=False, 

494 stop_token=None): 

495 """Consume a qualified rule. 

496 

497 Consume just enough of :obj:`tokens` for this rule. 

498 

499 :type first_token: :term:`component value` 

500 :param first_token: The first component value of the rule. 

501 :type tokens: :term:`iterator` 

502 :param tokens: An iterator yielding :term:`component values`. 

503 :type nested: :obj:`bool` 

504 :param nested: Whether the rule is nested or top-level. 

505 :type stop_token: :class:`~tinycss2.ast.Node` 

506 :param stop_token: A token that ends rule parsing when met. 

507 

508 """ 

509 if first_token == stop_token: 

510 return _rule_error(first_token, 'Stop token') 

511 if first_token.type == '{} block': 

512 prelude = [] 

513 block = first_token 

514 else: 

515 prelude = [first_token] 

516 for token in tokens: 

517 if token == stop_token: 

518 return _rule_error(token, 'Stop token') 

519 if token.type == '{} block': 

520 block = token 

521 # TODO: handle special case for CSS variables (using "nested") 

522 # https://drafts.csswg.org/css-syntax-3/#consume-qualified-rule 

523 break 

524 prelude.append(token) 

525 else: 

526 return _rule_error(prelude[-1], 'EOF') 

527 return QualifiedRule( 

528 first_token.source_line, first_token.source_column, prelude, block.content)