docling 2.1.0__py3-none-any.whl → 2.4.1__py3-none-any.whl

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.
Files changed (35) hide show
  1. docling/backend/abstract_backend.py +1 -0
  2. docling/backend/asciidoc_backend.py +431 -0
  3. docling/backend/docling_parse_backend.py +4 -4
  4. docling/backend/docling_parse_v2_backend.py +12 -4
  5. docling/backend/html_backend.py +61 -57
  6. docling/backend/md_backend.py +346 -0
  7. docling/backend/mspowerpoint_backend.py +62 -39
  8. docling/backend/msword_backend.py +12 -25
  9. docling/backend/pypdfium2_backend.py +1 -1
  10. docling/cli/main.py +38 -8
  11. docling/datamodel/base_models.py +16 -10
  12. docling/datamodel/document.py +36 -6
  13. docling/datamodel/pipeline_options.py +3 -3
  14. docling/datamodel/settings.py +15 -1
  15. docling/document_converter.py +38 -12
  16. docling/models/base_model.py +4 -1
  17. docling/models/base_ocr_model.py +21 -4
  18. docling/models/ds_glm_model.py +27 -11
  19. docling/models/easyocr_model.py +49 -39
  20. docling/models/layout_model.py +87 -61
  21. docling/models/page_assemble_model.py +102 -100
  22. docling/models/page_preprocessing_model.py +25 -7
  23. docling/models/table_structure_model.py +125 -90
  24. docling/models/tesseract_ocr_cli_model.py +62 -52
  25. docling/models/tesseract_ocr_model.py +76 -52
  26. docling/pipeline/base_pipeline.py +68 -69
  27. docling/pipeline/simple_pipeline.py +8 -11
  28. docling/pipeline/standard_pdf_pipeline.py +59 -56
  29. docling/utils/profiling.py +62 -0
  30. {docling-2.1.0.dist-info → docling-2.4.1.dist-info}/METADATA +27 -22
  31. docling-2.4.1.dist-info/RECORD +45 -0
  32. docling-2.1.0.dist-info/RECORD +0 -42
  33. {docling-2.1.0.dist-info → docling-2.4.1.dist-info}/LICENSE +0 -0
  34. {docling-2.1.0.dist-info → docling-2.4.1.dist-info}/WHEEL +0 -0
  35. {docling-2.1.0.dist-info → docling-2.4.1.dist-info}/entry_points.txt +0 -0
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
13
13
  class AbstractDocumentBackend(ABC):
14
14
  @abstractmethod
15
15
  def __init__(self, in_doc: "InputDocument", path_or_stream: Union[BytesIO, Path]):
16
+ self.file = in_doc.file
16
17
  self.path_or_stream = path_or_stream
17
18
  self.document_hash = in_doc.document_hash
18
19
  self.input_format = in_doc.format
@@ -0,0 +1,431 @@
1
+ import logging
2
+ import re
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from typing import Set, Union
6
+
7
+ from docling_core.types.doc import (
8
+ DocItemLabel,
9
+ DoclingDocument,
10
+ DocumentOrigin,
11
+ GroupItem,
12
+ GroupLabel,
13
+ ImageRef,
14
+ Size,
15
+ TableCell,
16
+ TableData,
17
+ )
18
+
19
+ from docling.backend.abstract_backend import DeclarativeDocumentBackend
20
+ from docling.datamodel.base_models import InputFormat
21
+ from docling.datamodel.document import InputDocument
22
+
23
+ _log = logging.getLogger(__name__)
24
+
25
+
26
+ class AsciiDocBackend(DeclarativeDocumentBackend):
27
+
28
+ def __init__(self, in_doc: InputDocument, path_or_stream: Union[BytesIO, Path]):
29
+ super().__init__(in_doc, path_or_stream)
30
+
31
+ self.path_or_stream = path_or_stream
32
+
33
+ try:
34
+ if isinstance(self.path_or_stream, BytesIO):
35
+ text_stream = self.path_or_stream.getvalue().decode("utf-8")
36
+ self.lines = text_stream.split("\n")
37
+ if isinstance(self.path_or_stream, Path):
38
+ with open(self.path_or_stream, "r", encoding="utf-8") as f:
39
+ self.lines = f.readlines()
40
+ self.valid = True
41
+
42
+ except Exception as e:
43
+ raise RuntimeError(
44
+ f"Could not initialize AsciiDoc backend for file with hash {self.document_hash}."
45
+ ) from e
46
+ return
47
+
48
+ def is_valid(self) -> bool:
49
+ return self.valid
50
+
51
+ @classmethod
52
+ def supports_pagination(cls) -> bool:
53
+ return False
54
+
55
+ def unload(self):
56
+ return
57
+
58
+ @classmethod
59
+ def supported_formats(cls) -> Set[InputFormat]:
60
+ return {InputFormat.ASCIIDOC}
61
+
62
+ def convert(self) -> DoclingDocument:
63
+ """
64
+ Parses the ASCII into a structured document model.
65
+ """
66
+
67
+ origin = DocumentOrigin(
68
+ filename=self.file.name or "file",
69
+ mimetype="text/asciidoc",
70
+ binary_hash=self.document_hash,
71
+ )
72
+
73
+ doc = DoclingDocument(name=self.file.stem or "file", origin=origin)
74
+
75
+ doc = self._parse(doc)
76
+
77
+ return doc
78
+
79
+ def _parse(self, doc: DoclingDocument):
80
+ """
81
+ Main function that orchestrates the parsing by yielding components:
82
+ title, section headers, text, lists, and tables.
83
+ """
84
+
85
+ content = ""
86
+
87
+ in_list = False
88
+ in_table = False
89
+
90
+ text_data: list[str] = []
91
+ table_data: list[str] = []
92
+ caption_data: list[str] = []
93
+
94
+ # parents: dict[int, Union[DocItem, GroupItem, None]] = {}
95
+ parents: dict[int, Union[GroupItem, None]] = {}
96
+ # indents: dict[int, Union[DocItem, GroupItem, None]] = {}
97
+ indents: dict[int, Union[GroupItem, None]] = {}
98
+
99
+ for i in range(0, 10):
100
+ parents[i] = None
101
+ indents[i] = None
102
+
103
+ for line in self.lines:
104
+ # line = line.strip()
105
+
106
+ # Title
107
+ if self._is_title(line):
108
+ item = self._parse_title(line)
109
+ level = item["level"]
110
+
111
+ parents[level] = doc.add_text(
112
+ text=item["text"], label=DocItemLabel.TITLE
113
+ )
114
+
115
+ # Section headers
116
+ elif self._is_section_header(line):
117
+ item = self._parse_section_header(line)
118
+ level = item["level"]
119
+
120
+ parents[level] = doc.add_heading(
121
+ text=item["text"], level=item["level"], parent=parents[level - 1]
122
+ )
123
+ for k, v in parents.items():
124
+ if k > level:
125
+ parents[k] = None
126
+
127
+ # Lists
128
+ elif self._is_list_item(line):
129
+
130
+ _log.debug(f"line: {line}")
131
+ item = self._parse_list_item(line)
132
+ _log.debug(f"parsed list-item: {item}")
133
+
134
+ level = self._get_current_level(parents)
135
+
136
+ if not in_list:
137
+ in_list = True
138
+
139
+ parents[level + 1] = doc.add_group(
140
+ parent=parents[level], name="list", label=GroupLabel.LIST
141
+ )
142
+ indents[level + 1] = item["indent"]
143
+
144
+ elif in_list and item["indent"] > indents[level]:
145
+ parents[level + 1] = doc.add_group(
146
+ parent=parents[level], name="list", label=GroupLabel.LIST
147
+ )
148
+ indents[level + 1] = item["indent"]
149
+
150
+ elif in_list and item["indent"] < indents[level]:
151
+
152
+ # print(item["indent"], " => ", indents[level])
153
+ while item["indent"] < indents[level]:
154
+ # print(item["indent"], " => ", indents[level])
155
+ parents[level] = None
156
+ indents[level] = None
157
+ level -= 1
158
+
159
+ doc.add_list_item(
160
+ item["text"], parent=self._get_current_parent(parents)
161
+ )
162
+
163
+ elif in_list and not self._is_list_item(line):
164
+ in_list = False
165
+
166
+ level = self._get_current_level(parents)
167
+ parents[level] = None
168
+
169
+ # Tables
170
+ elif line.strip() == "|===" and not in_table: # start of table
171
+ in_table = True
172
+
173
+ elif self._is_table_line(line): # within a table
174
+ in_table = True
175
+ table_data.append(self._parse_table_line(line))
176
+
177
+ elif in_table and (
178
+ (not self._is_table_line(line)) or line.strip() == "|==="
179
+ ): # end of table
180
+
181
+ caption = None
182
+ if len(caption_data) > 0:
183
+ caption = doc.add_text(
184
+ text=" ".join(caption_data), label=DocItemLabel.CAPTION
185
+ )
186
+
187
+ caption_data = []
188
+
189
+ data = self._populate_table_as_grid(table_data)
190
+ doc.add_table(
191
+ data=data, parent=self._get_current_parent(parents), caption=caption
192
+ )
193
+
194
+ in_table = False
195
+ table_data = []
196
+
197
+ # Picture
198
+ elif self._is_picture(line):
199
+
200
+ caption = None
201
+ if len(caption_data) > 0:
202
+ caption = doc.add_text(
203
+ text=" ".join(caption_data), label=DocItemLabel.CAPTION
204
+ )
205
+
206
+ caption_data = []
207
+
208
+ item = self._parse_picture(line)
209
+
210
+ size = None
211
+ if "width" in item and "height" in item:
212
+ size = Size(width=int(item["width"]), height=int(item["height"]))
213
+
214
+ uri = None
215
+ if (
216
+ "uri" in item
217
+ and not item["uri"].startswith("http")
218
+ and item["uri"].startswith("//")
219
+ ):
220
+ uri = "file:" + item["uri"]
221
+ elif (
222
+ "uri" in item
223
+ and not item["uri"].startswith("http")
224
+ and item["uri"].startswith("/")
225
+ ):
226
+ uri = "file:/" + item["uri"]
227
+ elif "uri" in item and not item["uri"].startswith("http"):
228
+ uri = "file://" + item["uri"]
229
+
230
+ image = ImageRef(mimetype="image/png", size=size, dpi=70, uri=uri)
231
+ doc.add_picture(image=image, caption=caption)
232
+
233
+ # Caption
234
+ elif self._is_caption(line) and len(caption_data) == 0:
235
+ item = self._parse_caption(line)
236
+ caption_data.append(item["text"])
237
+
238
+ elif (
239
+ len(line.strip()) > 0 and len(caption_data) > 0
240
+ ): # allow multiline captions
241
+ item = self._parse_text(line)
242
+ caption_data.append(item["text"])
243
+
244
+ # Plain text
245
+ elif len(line.strip()) == 0 and len(text_data) > 0:
246
+ doc.add_text(
247
+ text=" ".join(text_data),
248
+ label=DocItemLabel.PARAGRAPH,
249
+ parent=self._get_current_parent(parents),
250
+ )
251
+ text_data = []
252
+
253
+ elif len(line.strip()) > 0: # allow multiline texts
254
+
255
+ item = self._parse_text(line)
256
+ text_data.append(item["text"])
257
+
258
+ if len(text_data) > 0:
259
+ doc.add_text(
260
+ text=" ".join(text_data),
261
+ label=DocItemLabel.PARAGRAPH,
262
+ parent=self._get_current_parent(parents),
263
+ )
264
+ text_data = []
265
+
266
+ if in_table and len(table_data) > 0:
267
+ data = self._populate_table_as_grid(table_data)
268
+ doc.add_table(data=data, parent=self._get_current_parent(parents))
269
+
270
+ in_table = False
271
+ table_data = []
272
+
273
+ return doc
274
+
275
+ def _get_current_level(self, parents):
276
+ for k, v in parents.items():
277
+ if v == None and k > 0:
278
+ return k - 1
279
+
280
+ return 0
281
+
282
+ def _get_current_parent(self, parents):
283
+ for k, v in parents.items():
284
+ if v == None and k > 0:
285
+ return parents[k - 1]
286
+
287
+ return None
288
+
289
+ # ========= Title
290
+ def _is_title(self, line):
291
+ return re.match(r"^= ", line)
292
+
293
+ def _parse_title(self, line):
294
+ return {"type": "title", "text": line[2:].strip(), "level": 0}
295
+
296
+ # ========= Section headers
297
+ def _is_section_header(self, line):
298
+ return re.match(r"^==+", line)
299
+
300
+ def _parse_section_header(self, line):
301
+ match = re.match(r"^(=+)\s+(.*)", line)
302
+
303
+ marker = match.group(1) # The list marker (e.g., "*", "-", "1.")
304
+ text = match.group(2) # The actual text of the list item
305
+
306
+ header_level = marker.count("=") # number of '=' represents level
307
+ return {
308
+ "type": "header",
309
+ "level": header_level - 1,
310
+ "text": text.strip(),
311
+ }
312
+
313
+ # ========= Lists
314
+ def _is_list_item(self, line):
315
+ return re.match(r"^(\s)*(\*|-|\d+\.|\w+\.) ", line)
316
+
317
+ def _parse_list_item(self, line):
318
+ """Extract the item marker (number or bullet symbol) and the text of the item."""
319
+
320
+ match = re.match(r"^(\s*)(\*|-|\d+\.)\s+(.*)", line)
321
+ if match:
322
+ indent = match.group(1)
323
+ marker = match.group(2) # The list marker (e.g., "*", "-", "1.")
324
+ text = match.group(3) # The actual text of the list item
325
+
326
+ if marker == "*" or marker == "-":
327
+ return {
328
+ "type": "list_item",
329
+ "marker": marker,
330
+ "text": text.strip(),
331
+ "numbered": False,
332
+ "indent": 0 if indent == None else len(indent),
333
+ }
334
+ else:
335
+ return {
336
+ "type": "list_item",
337
+ "marker": marker,
338
+ "text": text.strip(),
339
+ "numbered": True,
340
+ "indent": 0 if indent == None else len(indent),
341
+ }
342
+ else:
343
+ # Fallback if no match
344
+ return {
345
+ "type": "list_item",
346
+ "marker": "-",
347
+ "text": line,
348
+ "numbered": False,
349
+ "indent": 0,
350
+ }
351
+
352
+ # ========= Tables
353
+ def _is_table_line(self, line):
354
+ return re.match(r"^\|.*\|", line)
355
+
356
+ def _parse_table_line(self, line):
357
+ # Split table cells and trim extra spaces
358
+ return [cell.strip() for cell in line.split("|") if cell.strip()]
359
+
360
+ def _populate_table_as_grid(self, table_data):
361
+
362
+ num_rows = len(table_data)
363
+
364
+ # Adjust the table data into a grid format
365
+ num_cols = max(len(row) for row in table_data)
366
+
367
+ data = TableData(num_rows=num_rows, num_cols=num_cols, table_cells=[])
368
+ for row_idx, row in enumerate(table_data):
369
+ # Pad rows with empty strings to match column count
370
+ # grid.append(row + [''] * (max_cols - len(row)))
371
+
372
+ for col_idx, text in enumerate(row):
373
+ row_span = 1
374
+ col_span = 1
375
+
376
+ cell = TableCell(
377
+ text=text,
378
+ row_span=row_span,
379
+ col_span=col_span,
380
+ start_row_offset_idx=row_idx,
381
+ end_row_offset_idx=row_idx + row_span,
382
+ start_col_offset_idx=col_idx,
383
+ end_col_offset_idx=col_idx + col_span,
384
+ col_header=False,
385
+ row_header=False,
386
+ )
387
+ data.table_cells.append(cell)
388
+
389
+ return data
390
+
391
+ # ========= Pictures
392
+ def _is_picture(self, line):
393
+ return re.match(r"^image::", line)
394
+
395
+ def _parse_picture(self, line):
396
+ """
397
+ Parse an image macro, extracting its path and attributes.
398
+ Syntax: image::path/to/image.png[Alt Text, width=200, height=150, align=center]
399
+ """
400
+ mtch = re.match(r"^image::(.+)\[(.*)\]$", line)
401
+ if mtch:
402
+ picture_path = mtch.group(1).strip()
403
+ attributes = mtch.group(2).split(",")
404
+ picture_info = {"type": "picture", "uri": picture_path}
405
+
406
+ # Extract optional attributes (alt text, width, height, alignment)
407
+ if attributes:
408
+ picture_info["alt"] = attributes[0].strip() if attributes[0] else ""
409
+ for attr in attributes[1:]:
410
+ key, value = attr.split("=")
411
+ picture_info[key.strip()] = value.strip()
412
+
413
+ return picture_info
414
+
415
+ return {"type": "picture", "uri": line}
416
+
417
+ # ========= Captions
418
+ def _is_caption(self, line):
419
+ return re.match(r"^\.(.+)", line)
420
+
421
+ def _parse_caption(self, line):
422
+ mtch = re.match(r"^\.(.+)", line)
423
+ if mtch:
424
+ text = mtch.group(1)
425
+ return {"type": "caption", "text": text}
426
+
427
+ return {"type": "caption", "text": ""}
428
+
429
+ # ========= Plain text
430
+ def _parse_text(self, line):
431
+ return {"type": "text", "text": line.strip()}
@@ -6,7 +6,7 @@ from typing import Iterable, List, Optional, Union
6
6
 
7
7
  import pypdfium2 as pdfium
8
8
  from docling_core.types.doc import BoundingBox, CoordOrigin, Size
9
- from docling_parse.docling_parse import pdf_parser
9
+ from docling_parse.docling_parse import pdf_parser_v1
10
10
  from PIL import Image, ImageDraw
11
11
  from pypdfium2 import PdfPage
12
12
 
@@ -19,7 +19,7 @@ _log = logging.getLogger(__name__)
19
19
 
20
20
  class DoclingParsePageBackend(PdfPageBackend):
21
21
  def __init__(
22
- self, parser: pdf_parser, document_hash: str, page_no: int, page_obj: PdfPage
22
+ self, parser: pdf_parser_v1, document_hash: str, page_no: int, page_obj: PdfPage
23
23
  ):
24
24
  self._ppage = page_obj
25
25
  parsed_page = parser.parse_pdf_from_key_on_page(document_hash, page_no)
@@ -29,7 +29,7 @@ class DoclingParsePageBackend(PdfPageBackend):
29
29
  self._dpage = parsed_page["pages"][0]
30
30
  else:
31
31
  _log.info(
32
- f"An error occured when loading page {page_no} of document {document_hash}."
32
+ f"An error occurred when loading page {page_no} of document {document_hash}."
33
33
  )
34
34
 
35
35
  def is_valid(self) -> bool:
@@ -192,7 +192,7 @@ class DoclingParseDocumentBackend(PdfDocumentBackend):
192
192
  super().__init__(in_doc, path_or_stream)
193
193
 
194
194
  self._pdoc = pdfium.PdfDocument(self.path_or_stream)
195
- self.parser = pdf_parser()
195
+ self.parser = pdf_parser_v1()
196
196
 
197
197
  success = False
198
198
  if isinstance(self.path_or_stream, BytesIO):
@@ -26,12 +26,12 @@ class DoclingParseV2PageBackend(PdfPageBackend):
26
26
  self._ppage = page_obj
27
27
  parsed_page = parser.parse_pdf_from_key_on_page(document_hash, page_no)
28
28
 
29
- self.valid = "pages" in parsed_page
29
+ self.valid = "pages" in parsed_page and len(parsed_page["pages"]) == 1
30
30
  if self.valid:
31
- self._dpage = parsed_page["pages"][page_no]
31
+ self._dpage = parsed_page["pages"][0]
32
32
  else:
33
33
  _log.info(
34
- f"An error occured when loading page {page_no} of document {document_hash}."
34
+ f"An error occurred when loading page {page_no} of document {document_hash}."
35
35
  )
36
36
 
37
37
  def is_valid(self) -> bool:
@@ -223,7 +223,15 @@ class DoclingParseV2DocumentBackend(PdfDocumentBackend):
223
223
  )
224
224
 
225
225
  def page_count(self) -> int:
226
- return len(self._pdoc) # To be replaced with docling-parse API
226
+ # return len(self._pdoc) # To be replaced with docling-parse API
227
+
228
+ len_1 = len(self._pdoc)
229
+ len_2 = self.parser.number_of_pages(self.document_hash)
230
+
231
+ if len_1 != len_2:
232
+ _log.error(f"Inconsistent number of pages: {len_1}!={len_2}")
233
+
234
+ return len_2
227
235
 
228
236
  def load_page(self, page_no: int) -> DoclingParseV2PageBackend:
229
237
  return DoclingParseV2PageBackend(
@@ -7,6 +7,7 @@ from bs4 import BeautifulSoup
7
7
  from docling_core.types.doc import (
8
8
  DocItemLabel,
9
9
  DoclingDocument,
10
+ DocumentOrigin,
10
11
  GroupLabel,
11
12
  TableCell,
12
13
  TableData,
@@ -66,7 +67,13 @@ class HTMLDocumentBackend(DeclarativeDocumentBackend):
66
67
 
67
68
  def convert(self) -> DoclingDocument:
68
69
  # access self.path_or_stream to load stuff
69
- doc = DoclingDocument(name="dummy")
70
+ origin = DocumentOrigin(
71
+ filename=self.file.name or "file",
72
+ mimetype="text/html",
73
+ binary_hash=self.document_hash,
74
+ )
75
+
76
+ doc = DoclingDocument(name=self.file.stem or "file", origin=origin)
70
77
  _log.debug("Trying to convert HTML...")
71
78
 
72
79
  if self.is_valid():
@@ -129,7 +136,6 @@ class HTMLDocumentBackend(DeclarativeDocumentBackend):
129
136
  def get_direct_text(self, item):
130
137
  """Get the direct text of the <li> element (ignoring nested lists)."""
131
138
  text = item.find(string=True, recursive=False)
132
-
133
139
  if isinstance(text, str):
134
140
  return text.strip()
135
141
 
@@ -142,21 +148,20 @@ class HTMLDocumentBackend(DeclarativeDocumentBackend):
142
148
  if isinstance(item, str):
143
149
  return [item]
144
150
 
145
- result.append(self.get_direct_text(item))
146
-
147
- try:
148
- # Iterate over the children (and their text and tails)
149
- for child in item:
150
- try:
151
- # Recursively get the child's text content
152
- result.extend(self.extract_text_recursively(child))
153
- except:
154
- pass
155
- except:
156
- _log.warn("item has no children")
157
- pass
158
-
159
- return " ".join(result)
151
+ if item.name not in ["ul", "ol"]:
152
+ try:
153
+ # Iterate over the children (and their text and tails)
154
+ for child in item:
155
+ try:
156
+ # Recursively get the child's text content
157
+ result.extend(self.extract_text_recursively(child))
158
+ except:
159
+ pass
160
+ except:
161
+ _log.warn("item has no children")
162
+ pass
163
+
164
+ return "".join(result) + " "
160
165
 
161
166
  def handle_header(self, element, idx, doc):
162
167
  """Handles header tags (h1, h2, etc.)."""
@@ -174,38 +179,31 @@ class HTMLDocumentBackend(DeclarativeDocumentBackend):
174
179
  self.parents[self.level] = doc.add_text(
175
180
  parent=self.parents[0], label=DocItemLabel.TITLE, text=text
176
181
  )
177
-
178
- elif hlevel == self.level:
179
- self.parents[hlevel] = doc.add_text(
180
- parent=self.parents[hlevel - 1], label=label, text=text
181
- )
182
-
183
- elif hlevel > self.level:
184
-
185
- # add invisible group
186
- for i in range(self.level + 1, hlevel):
187
- self.parents[i] = doc.add_group(
188
- name=f"header-{i}",
189
- label=GroupLabel.SECTION,
190
- parent=self.parents[i - 1],
191
- )
192
-
193
- self.parents[hlevel] = doc.add_text(
194
- parent=self.parents[hlevel - 1], label=label, text=text
195
- )
196
- self.level = hlevel
197
-
198
- elif hlevel < self.level:
199
-
200
- # remove the tail
201
- for key, val in self.parents.items():
202
- if key > hlevel:
203
- self.parents[key] = None
204
-
205
- self.parents[hlevel] = doc.add_text(
206
- parent=self.parents[hlevel - 1], label=label, text=text
182
+ else:
183
+ if hlevel > self.level:
184
+
185
+ # add invisible group
186
+ for i in range(self.level + 1, hlevel):
187
+ self.parents[i] = doc.add_group(
188
+ name=f"header-{i}",
189
+ label=GroupLabel.SECTION,
190
+ parent=self.parents[i - 1],
191
+ )
192
+ self.level = hlevel
193
+
194
+ elif hlevel < self.level:
195
+
196
+ # remove the tail
197
+ for key, val in self.parents.items():
198
+ if key > hlevel:
199
+ self.parents[key] = None
200
+ self.level = hlevel
201
+
202
+ self.parents[hlevel] = doc.add_heading(
203
+ parent=self.parents[hlevel - 1],
204
+ text=text,
205
+ level=hlevel,
207
206
  )
208
- self.level = hlevel
209
207
 
210
208
  def handle_paragraph(self, element, idx, doc):
211
209
  """Handles paragraph tags (p)."""
@@ -248,7 +246,12 @@ class HTMLDocumentBackend(DeclarativeDocumentBackend):
248
246
 
249
247
  if nested_lists:
250
248
  name = element.name
251
- text = self.get_direct_text(element)
249
+ # Text in list item can be hidden within hierarchy, hence
250
+ # we need to extract it recursively
251
+ text = self.extract_text_recursively(element)
252
+ # Flatten text, remove break lines:
253
+ text = text.replace("\n", "").replace("\r", "")
254
+ text = " ".join(text.split()).strip()
252
255
 
253
256
  marker = ""
254
257
  enumerated = False
@@ -256,14 +259,15 @@ class HTMLDocumentBackend(DeclarativeDocumentBackend):
256
259
  marker = str(index_in_list)
257
260
  enumerated = True
258
261
 
259
- # create a list-item
260
- self.parents[self.level + 1] = doc.add_list_item(
261
- text=text,
262
- enumerated=enumerated,
263
- marker=marker,
264
- parent=self.parents[self.level],
265
- )
266
- self.level += 1
262
+ if len(text) > 0:
263
+ # create a list-item
264
+ self.parents[self.level + 1] = doc.add_list_item(
265
+ text=text,
266
+ enumerated=enumerated,
267
+ marker=marker,
268
+ parent=self.parents[self.level],
269
+ )
270
+ self.level += 1
267
271
 
268
272
  self.walk(element, doc)
269
273