pycaption 2.3.9__tar.gz → 2.3.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pycaption-2.3.9/pycaption.egg-info → pycaption-2.3.10}/PKG-INFO +1 -1
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/dfxp/writer.py +280 -15
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/webvtt/writer.py +262 -45
- {pycaption-2.3.9 → pycaption-2.3.10/pycaption.egg-info}/PKG-INFO +1 -1
- {pycaption-2.3.9 → pycaption-2.3.10}/setup.py +1 -1
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/conftest.py +1 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/scc.py +17 -0
- pycaption-2.3.10/tests/test_dfxp.py +867 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_scc_conversion.py +22 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_webvtt_conversion.py +596 -0
- pycaption-2.3.9/tests/test_dfxp.py +0 -441
- {pycaption-2.3.9 → pycaption-2.3.10}/LICENSE +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/MANIFEST.in +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/README.rst +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/base.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/dfxp/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/dfxp/constants.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/dfxp/extras.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/dfxp/reader.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/exceptions.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/geometry.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/microdvd.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/sami/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/sami/constants.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/sami/parser.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/sami/reader.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/sami/writer.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/constants.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/reader.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/specialized_collections.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/state_machines.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/translator.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/scc/writer.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/srt.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/transcript.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/utils.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/webvtt/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/webvtt/constants.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption/webvtt/reader.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption.egg-info/SOURCES.txt +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption.egg-info/dependency_links.txt +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption.egg-info/requires.txt +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/pycaption.egg-info/top_level.txt +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/setup.cfg +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/__init__.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/dfxp.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/microdvd.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/sami.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/srt.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/translated_scc.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/fixtures/webvtt.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/mixins.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_base.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_bytes_input.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_dfxp_conversion.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_dfxp_extras.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_double_encoding.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_functions.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_geometry.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_microdvd.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_microdvd_conversion.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_sami.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_sami_conversion.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_scc.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_scc_translator.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_scc_writer.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_srt.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_srt_conversion.py +0 -0
- {pycaption-2.3.9 → pycaption-2.3.10}/tests/test_webvtt.py +0 -0
|
@@ -216,36 +216,275 @@ class DFXPWriter(BaseWriter):
|
|
|
216
216
|
def _recreate_text(self, caption, dfxp, caption_set=None, lang=None):
|
|
217
217
|
"""Serialize all nodes of a caption into DFXP inline markup.
|
|
218
218
|
|
|
219
|
-
Handles text nodes, line breaks (<br/>), and style spans.
|
|
219
|
+
Handles text nodes, line breaks (<br/>), and style spans. Nodes are
|
|
220
|
+
first split into runs sharing one region; a run whose region is not
|
|
221
|
+
the one already in effect is wrapped in a <span> carrying it, so text
|
|
222
|
+
renders at its own position instead of inheriting the one on the
|
|
223
|
+
enclosing <p>, and a <br/> between two nodes of one region stays
|
|
224
|
+
inside that region. A run holding no visible text takes no wrapper:
|
|
225
|
+
there is nothing for the region to place.
|
|
220
226
|
|
|
221
227
|
:rtype: str
|
|
222
228
|
"""
|
|
223
229
|
line = ""
|
|
224
230
|
self._span_stack = []
|
|
231
|
+
paragraph = self.region_creator.get_positioning_info(lang, caption_set, caption)
|
|
232
|
+
paragraph_region, _ = paragraph
|
|
233
|
+
|
|
234
|
+
for region_id, region_attribs, nodes, wrappable in self._group_nodes_by_region(
|
|
235
|
+
caption, paragraph, caption_set, lang
|
|
236
|
+
):
|
|
237
|
+
wrap = (
|
|
238
|
+
wrappable
|
|
239
|
+
and region_id != self._innermost_region(paragraph_region)
|
|
240
|
+
and self._holds_visible_text(nodes)
|
|
241
|
+
)
|
|
242
|
+
if wrap:
|
|
243
|
+
attrs = {"region": region_id}
|
|
244
|
+
if self.write_inline_positioning:
|
|
245
|
+
attrs.update(region_attribs)
|
|
246
|
+
attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
|
247
|
+
line += f"<span {attr_str}>"
|
|
225
248
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
for node in nodes:
|
|
250
|
+
if node.type_ == CaptionNode.TEXT:
|
|
251
|
+
line += escape(node.content)
|
|
229
252
|
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
elif node.type_ == CaptionNode.BREAK:
|
|
254
|
+
line = line.rstrip() + "<br/>\n "
|
|
232
255
|
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
elif node.type_ == CaptionNode.STYLE:
|
|
257
|
+
line = self._recreate_span(
|
|
258
|
+
line, node, dfxp, caption_set, caption, lang
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if wrap:
|
|
262
|
+
line = line.rstrip() + "</span> "
|
|
235
263
|
|
|
236
264
|
while self._span_stack:
|
|
237
|
-
|
|
238
|
-
|
|
265
|
+
emitted, _ = self._span_stack.pop()
|
|
266
|
+
if emitted:
|
|
267
|
+
line = line.rstrip() + "</span> "
|
|
239
268
|
|
|
240
269
|
return line.rstrip()
|
|
241
270
|
|
|
271
|
+
def _group_nodes_by_region(self, caption, paragraph, caption_set, lang):
|
|
272
|
+
"""Split a caption's nodes into consecutive runs sharing one region.
|
|
273
|
+
|
|
274
|
+
Line breaks are neutral: a run absorbs one only when it continues
|
|
275
|
+
past it, so a break between two nodes of the same region ends up
|
|
276
|
+
inside that region's span rather than outside it. Breaks left over
|
|
277
|
+
between two runs form a run of their own, which is never wrapped.
|
|
278
|
+
A run is split again around any style tag whose partner falls
|
|
279
|
+
outside it, so one such tag costs only itself a wrapper.
|
|
280
|
+
|
|
281
|
+
:param paragraph: (region_id, positioning attributes) of the enclosing <p>
|
|
282
|
+
:return: (region_id, positioning_attributes, nodes, wrappable) per run
|
|
283
|
+
:rtype: list[tuple[str | None, dict, list[CaptionNode], bool]]
|
|
284
|
+
"""
|
|
285
|
+
resolved, partners = self._resolve_node_regions(
|
|
286
|
+
caption, paragraph, caption_set, lang
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
groups = []
|
|
290
|
+
current = None
|
|
291
|
+
pending = []
|
|
292
|
+
|
|
293
|
+
for index, (region_id, region_attribs) in enumerate(resolved):
|
|
294
|
+
if region_id is None:
|
|
295
|
+
pending.append(index)
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
if current and region_id == current[0]:
|
|
299
|
+
current[2].extend(pending)
|
|
300
|
+
else:
|
|
301
|
+
if current:
|
|
302
|
+
groups.append(current)
|
|
303
|
+
if pending:
|
|
304
|
+
groups.append((None, {}, pending))
|
|
305
|
+
current = [region_id, region_attribs, []]
|
|
306
|
+
pending = []
|
|
307
|
+
current[2].append(index)
|
|
308
|
+
|
|
309
|
+
if current:
|
|
310
|
+
groups.append(current)
|
|
311
|
+
if pending:
|
|
312
|
+
groups.append((None, {}, pending))
|
|
313
|
+
|
|
314
|
+
return [
|
|
315
|
+
(
|
|
316
|
+
region_id,
|
|
317
|
+
region_attribs,
|
|
318
|
+
[caption.nodes[index] for index in segment],
|
|
319
|
+
region_id is not None
|
|
320
|
+
and self._is_wrappable(segment, partners, caption.nodes),
|
|
321
|
+
)
|
|
322
|
+
for region_id, region_attribs, indices in groups
|
|
323
|
+
for segment in self._split_at_crossing_styles(
|
|
324
|
+
indices, partners, caption.nodes
|
|
325
|
+
)
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _holds_visible_text(nodes):
|
|
330
|
+
"""Check whether a run has any text for a region to place.
|
|
331
|
+
|
|
332
|
+
A run of nothing but style tags and breaks renders nothing, so a
|
|
333
|
+
wrapper carrying its region would be an empty span.
|
|
334
|
+
|
|
335
|
+
:rtype: bool
|
|
336
|
+
"""
|
|
337
|
+
return any(
|
|
338
|
+
node.type_ == CaptionNode.TEXT and node.content.strip() for node in nodes
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def _crosses_run(index, partners, nodes, run):
|
|
343
|
+
"""Check whether the node at an index leaves the run it belongs to.
|
|
344
|
+
|
|
345
|
+
A style tag whose partner sits outside the run does: a span wrapping
|
|
346
|
+
it would cross that tag's own span and produce invalid XML. A
|
|
347
|
+
closing tag with no partner at all is the exception — nothing was
|
|
348
|
+
opened for it, so it writes no tag and crosses nothing.
|
|
349
|
+
|
|
350
|
+
:param run: the indices making up the run
|
|
351
|
+
:type run: set[int]
|
|
352
|
+
:rtype: bool
|
|
353
|
+
"""
|
|
354
|
+
node = nodes[index]
|
|
355
|
+
if node.type_ != CaptionNode.STYLE:
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
if not node.start and index not in partners:
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
return partners.get(index) not in run
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def _split_at_crossing_styles(cls, indices, partners, nodes):
|
|
365
|
+
"""Split a run around style tags whose partner sits outside it.
|
|
366
|
+
|
|
367
|
+
A span wrapping such a tag would cross another span and produce
|
|
368
|
+
invalid XML, but only that tag is in the way — the nodes beside it
|
|
369
|
+
still deserve their own region, so they come back as runs of their
|
|
370
|
+
own instead of losing their position along with it.
|
|
371
|
+
|
|
372
|
+
:rtype: list[list[int]]
|
|
373
|
+
"""
|
|
374
|
+
run = set(indices)
|
|
375
|
+
segments = [[]]
|
|
376
|
+
|
|
377
|
+
for index in indices:
|
|
378
|
+
if cls._crosses_run(index, partners, nodes, run):
|
|
379
|
+
segments += [[index], []]
|
|
380
|
+
else:
|
|
381
|
+
segments[-1].append(index)
|
|
382
|
+
|
|
383
|
+
return [segment for segment in segments if segment]
|
|
384
|
+
|
|
385
|
+
def _resolve_node_regions(self, caption, paragraph, caption_set, lang):
|
|
386
|
+
"""Resolve the region every node of a caption belongs to.
|
|
387
|
+
|
|
388
|
+
A node's own layout wins; lacking one it takes the region in effect
|
|
389
|
+
around it — the innermost enclosing style span's, else the
|
|
390
|
+
paragraph's. For a closing style node that is its own opener's
|
|
391
|
+
region, so a span is never separated from its own end tag. Line
|
|
392
|
+
breaks belong to no region and resolve to None, as does a closing
|
|
393
|
+
tag with nothing open — it writes no tag at all. A region travels
|
|
394
|
+
with the attributes that describe it, so a span reinstating one can
|
|
395
|
+
also write it inline.
|
|
396
|
+
|
|
397
|
+
:param paragraph: (region_id, positioning attributes) of the <p>
|
|
398
|
+
:return: a (region_id, positioning_attributes) pair per node, plus a
|
|
399
|
+
map pairing the index of each style tag with its partner's
|
|
400
|
+
:rtype: tuple[list[tuple[str | None, dict]], dict]
|
|
401
|
+
"""
|
|
402
|
+
creator = self.region_creator
|
|
403
|
+
resolved = []
|
|
404
|
+
partners = {}
|
|
405
|
+
open_styles = []
|
|
406
|
+
|
|
407
|
+
for index, node in enumerate(caption.nodes):
|
|
408
|
+
in_effect = open_styles[-1][1] if open_styles else paragraph
|
|
409
|
+
|
|
410
|
+
if node.type_ == CaptionNode.BREAK:
|
|
411
|
+
resolved.append((None, {}))
|
|
412
|
+
|
|
413
|
+
elif node.type_ == CaptionNode.TEXT:
|
|
414
|
+
own = (None, {})
|
|
415
|
+
if node.content.strip():
|
|
416
|
+
own = creator.get_node_positioning_info(node.layout_info)
|
|
417
|
+
resolved.append(own if own[0] else in_effect)
|
|
418
|
+
|
|
419
|
+
elif node.start:
|
|
420
|
+
if node.layout_info:
|
|
421
|
+
region = creator.get_positioning_info(
|
|
422
|
+
lang, caption_set, caption, node
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
region = in_effect
|
|
426
|
+
open_styles.append((index, region))
|
|
427
|
+
resolved.append(region)
|
|
428
|
+
|
|
429
|
+
elif open_styles:
|
|
430
|
+
opener, _ = open_styles.pop()
|
|
431
|
+
partners[opener] = index
|
|
432
|
+
partners[index] = opener
|
|
433
|
+
resolved.append(in_effect)
|
|
434
|
+
|
|
435
|
+
else:
|
|
436
|
+
resolved.append((None, {}))
|
|
437
|
+
|
|
438
|
+
return resolved, partners
|
|
439
|
+
|
|
440
|
+
@classmethod
|
|
441
|
+
def _is_wrappable(cls, indices, partners, nodes):
|
|
442
|
+
"""Check whether a run of nodes can take a <span> holding its region.
|
|
443
|
+
|
|
444
|
+
No style tag in the run may leave it, or the wrapping span would
|
|
445
|
+
cross a style span and produce invalid XML — which is what rules out
|
|
446
|
+
a tag split off on its own. A run that is nothing but one style span
|
|
447
|
+
already writes any region of its own on that span, so it needs no
|
|
448
|
+
wrapper.
|
|
449
|
+
|
|
450
|
+
:rtype: bool
|
|
451
|
+
"""
|
|
452
|
+
run = set(indices)
|
|
453
|
+
for index in indices:
|
|
454
|
+
if cls._crosses_run(index, partners, nodes, run):
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
first = nodes[indices[0]]
|
|
458
|
+
|
|
459
|
+
return not (
|
|
460
|
+
first.type_ == CaptionNode.STYLE
|
|
461
|
+
and first.start
|
|
462
|
+
and partners.get(indices[0]) == indices[-1]
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def _innermost_region(self, default):
|
|
466
|
+
"""Return the region in effect at the current point in the caption.
|
|
467
|
+
|
|
468
|
+
The innermost open span carrying a region wins; with no such span,
|
|
469
|
+
the region on the enclosing <p> applies.
|
|
470
|
+
|
|
471
|
+
:param default: region of the enclosing <p>
|
|
472
|
+
:rtype: str
|
|
473
|
+
"""
|
|
474
|
+
for _, region_id in reversed(self._span_stack):
|
|
475
|
+
if region_id:
|
|
476
|
+
return region_id
|
|
477
|
+
|
|
478
|
+
return default
|
|
479
|
+
|
|
242
480
|
def _recreate_span(
|
|
243
481
|
self, line, node, dfxp, caption_set=None, caption=None, lang=None
|
|
244
482
|
):
|
|
245
483
|
"""Open or close a <span> element for a style node.
|
|
246
484
|
|
|
247
485
|
Supports nested spans via a stack. Opening pushes onto the stack;
|
|
248
|
-
closing pops the most recent span.
|
|
486
|
+
closing pops the most recent span. Each entry records whether a tag
|
|
487
|
+
was actually emitted and which region it carries, if any.
|
|
249
488
|
|
|
250
489
|
:param line: the accumulated markup string so far
|
|
251
490
|
:type line: str
|
|
@@ -254,6 +493,7 @@ class DFXPWriter(BaseWriter):
|
|
|
254
493
|
"""
|
|
255
494
|
if node.start:
|
|
256
495
|
attrs = _recreate_style(node.content, dfxp)
|
|
496
|
+
region_id = None
|
|
257
497
|
if node.layout_info:
|
|
258
498
|
region_id, region_attribs = self.region_creator.get_positioning_info(
|
|
259
499
|
lang, caption_set, caption, node
|
|
@@ -265,13 +505,13 @@ class DFXPWriter(BaseWriter):
|
|
|
265
505
|
if attrs:
|
|
266
506
|
attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
|
267
507
|
line += f"<span {attr_str}>"
|
|
268
|
-
self._span_stack.append(True)
|
|
508
|
+
self._span_stack.append((True, region_id))
|
|
269
509
|
else:
|
|
270
|
-
self._span_stack.append(False)
|
|
510
|
+
self._span_stack.append((False, None))
|
|
271
511
|
|
|
272
512
|
else:
|
|
273
513
|
if self._span_stack:
|
|
274
|
-
had_span = self._span_stack.pop()
|
|
514
|
+
had_span, _ = self._span_stack.pop()
|
|
275
515
|
if had_span:
|
|
276
516
|
line = line.rstrip() + "</span> "
|
|
277
517
|
|
|
@@ -331,7 +571,12 @@ class RegionCreator:
|
|
|
331
571
|
"""Create <region> tags in the <layout> section for each Layout.
|
|
332
572
|
|
|
333
573
|
Skips Layout objects that have no positioning data (no origin,
|
|
334
|
-
extent, padding, alignment, or writing_direction).
|
|
574
|
+
extent, padding, alignment, or writing_direction). A line alignment
|
|
575
|
+
on its own is not positioning data: it is what a WebVTT ``line``
|
|
576
|
+
setting leaves behind when its value yields no line to place the cue
|
|
577
|
+
at — ``line:auto,start``, or a value the grammar discards outright —
|
|
578
|
+
so the cue keeps the default placement rather than taking a vertical
|
|
579
|
+
one from the qualifier alone.
|
|
335
580
|
|
|
336
581
|
:param unique_layouts: iterable of geometry.Layout instances
|
|
337
582
|
:type dfxp: BeautifulSoup
|
|
@@ -450,6 +695,26 @@ class RegionCreator:
|
|
|
450
695
|
|
|
451
696
|
return region_id, positioning_attributes
|
|
452
697
|
|
|
698
|
+
def get_node_positioning_info(self, layout_info):
|
|
699
|
+
"""Return (region_id, positioning_attributes) for a node's own layout.
|
|
700
|
+
|
|
701
|
+
Unlike get_positioning_info, this neither cascades to the caption and
|
|
702
|
+
the caption set nor falls back to the default region: a layout without
|
|
703
|
+
a region of its own yields (None, {}).
|
|
704
|
+
|
|
705
|
+
:type layout_info: geometry.Layout | None
|
|
706
|
+
:rtype: tuple[str | None, dict]
|
|
707
|
+
"""
|
|
708
|
+
region_id = self._region_map.get(layout_info)
|
|
709
|
+
if not region_id:
|
|
710
|
+
return None, {}
|
|
711
|
+
|
|
712
|
+
self._assigned_region_ids.add(region_id)
|
|
713
|
+
|
|
714
|
+
return region_id, _convert_layout_to_attributes(
|
|
715
|
+
layout_info, self._fallback_alignment
|
|
716
|
+
)
|
|
717
|
+
|
|
453
718
|
def cleanup_regions(self):
|
|
454
719
|
"""Remove <region> tags that were never assigned to any element."""
|
|
455
720
|
layout_tag = self._dfxp.find("layout")
|