@@ -147,13 +147,15 @@ def _split_text_by_markdown_headers(self, text: str, doc_id: str) -> list[dict]:
147147 # process headers and build chunks
148148 chunks : list [dict ] = []
149149 header_stack : list [str | None ] = [None ] * 6
150- pending_headers : list [str ] = [] # store empty headers to prepend to next content
150+ pending_start : int | None = None # start offset in text of the first buffered empty header
151+ pending_header_text : str | None = None # text of the last buffered empty header
152+ pending_level = 0 # level of the last buffered empty header
151153 has_content = False # flag to track if any header has content
152154
153155 for i , match in enumerate (matches ):
154156 # extract header info
155157 header_prefix = match .group (1 )
156- header_text = match .group (2 )
158+ header_text = match .group (2 ). strip () # keep surrounding whitespace out of the metadata
157159 level = len (header_prefix )
158160
159161 # get content
@@ -169,8 +171,12 @@ def _split_text_by_markdown_headers(self, text: str, doc_id: str) -> list[dict]:
169171 # skip splits w/o content
170172 if not content .strip (): # this strip is needed to avoid counting whitespace as content
171173 if self .keep_headers :
172- header_line = f"{ header_prefix } { header_text } "
173- pending_headers .append (header_line )
174+ # buffer this header so it gets prepended to the next contentful chunk; only the
175+ # offset of the first header in the run is needed since the chunk is sliced from text
176+ if pending_start is None :
177+ pending_start = match .start ()
178+ pending_header_text = header_text
179+ pending_level = level
174180 continue
175181
176182 has_content = True # at least one header has content
@@ -183,16 +189,13 @@ def _split_text_by_markdown_headers(self, text: str, doc_id: str) -> list[dict]:
183189 )
184190
185191 if self .keep_headers :
186- header_line = f"{ header_prefix } { header_text } "
187- # add pending & current header to content
188- chunk_content = ""
189- if pending_headers :
190- chunk_content += "\n " .join (pending_headers ) + "\n "
191- chunk_content += f"{ header_line } { content } "
192+ # Slice from the first buffered empty header (if any) so the buffered header lines and
193+ # the whitespace between them are preserved byte-exactly.
194+ chunk_content = text [(pending_start if pending_start is not None else match .start ()) : end ]
192195 chunks .append (
193196 {"content" : chunk_content , "meta" : {"header" : header_text , "parent_headers" : parent_headers }}
194197 )
195- pending_headers = [] # reset pending headers
198+ pending_start = None # reset buffered headers
196199 else :
197200 chunks .append ({"content" : content , "meta" : {"header" : header_text , "parent_headers" : parent_headers }})
198201
@@ -203,6 +206,18 @@ def _split_text_by_markdown_headers(self, text: str, doc_id: str) -> list[dict]:
203206 )
204207 return [{"content" : text , "meta" : {}}]
205208
209+ # Flush any trailing headers that had no body text of their own. A header at the very end of the
210+ # document (or a run of such headers) never gets a following content chunk to be prepended to, so
211+ # without this it would be silently dropped. Slicing the original text keeps reconstruction exact.
212+ if pending_start is not None :
213+ parent_headers = [h for h in header_stack [: pending_level - 1 ] if h is not None ]
214+ chunks .append (
215+ {
216+ "content" : text [pending_start :],
217+ "meta" : {"header" : pending_header_text , "parent_headers" : parent_headers },
218+ }
219+ )
220+
206221 return chunks
207222
208223 def _apply_secondary_splitting (self , documents : list [Document ]) -> list [Document ]:
0 commit comments